package net.i2p.router.transport.udp;

import java.io.IOException;
import java.io.Writer;
import java.net.InetAddress;
import java.net.Inet4Address;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

import net.i2p.crypto.SigType;
import net.i2p.data.DatabaseEntry;
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.SessionKey;
import net.i2p.data.i2np.DatabaseStoreMessage;
import net.i2p.data.i2np.I2NPMessage;
import net.i2p.router.CommSystemFacade;
import net.i2p.router.CommSystemFacade.Status;
import net.i2p.router.OutNetMessage;
import net.i2p.router.Router;
import net.i2p.router.RouterContext;
import net.i2p.router.transport.Transport;
import static net.i2p.router.transport.Transport.AddressSource.*;
import net.i2p.router.transport.TransportBid;
import net.i2p.router.transport.TransportImpl;
import net.i2p.router.transport.TransportUtil;
import static net.i2p.router.transport.TransportUtil.IPv6Config.*;
import net.i2p.router.transport.crypto.DHSessionKeyBuilder;
import static net.i2p.router.transport.udp.PeerTestState.Role.*;
import static net.i2p.router.transport.udp.Sorters.*;
import net.i2p.router.util.EventLog;
import net.i2p.router.util.RandomIterator;
import net.i2p.util.Addresses;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.Log;
import net.i2p.util.OrderedProperties;
import net.i2p.util.SimpleTimer;
import net.i2p.util.SimpleTimer2;
import net.i2p.util.VersionComparator;

/**
 *  The SSU transport
 */
public class UDPTransport extends TransportImpl implements TimedWeightedPriorityMessageQueue.FailedListener {
    private final Log _log;
    private final List<UDPEndpoint> _endpoints;
    private final Object _addDropLock = new Object();
    /** Peer (Hash) to PeerState */
    private final Map<Hash, PeerState> _peersByIdent;
    /** RemoteHostId to PeerState */
    private final Map<RemoteHostId, PeerState> _peersByRemoteHost;
    private PacketHandler _handler;
    private EstablishmentManager _establisher;
    private final MessageQueue _outboundMessages;
    private final OutboundMessageFragments _fragments;
    private final OutboundMessageFragments.ActiveThrottle _activeThrottle;
    private OutboundRefiller _refiller;
    private volatile PacketPusher _pusher;
    private final InboundMessageFragments _inboundFragments;
    //private UDPFlooder _flooder;
    private final PeerTestManager _testManager;
    private final IntroductionManager _introManager;
    private final ExpirePeerEvent _expireEvent;
    private final PeerTestEvent _testEvent;
    private final PacketBuilder _destroyBuilder;
    private Status _reachabilityStatus;
    private Status _reachabilityStatusPending;
    // only for logging, to be removed
    private long _reachabilityStatusLastUpdated;
    private int _reachabilityStatusUnchanged;
    private long _introducersSelectedOn;
    private long _lastInboundReceivedOn;
    private final DHSessionKeyBuilder.Factory _dhFactory;
    private int _mtu;
    private int _mtu_ipv6;
    private boolean _mismatchLogged;
    private final int _networkID;

    /**
     *  Do we have a public IPv6 address?
     *  TODO periodically update via CSFI.NetMonitor?
     */
    private volatile boolean _haveIPv6Address;
    private long _lastInboundIPv6;
    private final int _min_peers;
    private final int _min_v6_peers;
    
    /** do we need to rebuild our external router address asap? */
    private boolean _needsRebuild;
    private final Object _rebuildLock = new Object();
    
    /** introduction key */
    private SessionKey _introKey;
    
    /**
     *  List of RemoteHostId for peers whose packets we want to drop outright
     *  This is only for old network IDs (pre-0.6.1.10), so it isn't really used now.
     */
    private final Set<RemoteHostId> _dropList;
    
    private volatile long _expireTimeout;

    /** last report from a peer of our IP */
    private Hash _lastFrom;
    private byte[] _lastOurIP;
    private int _lastOurPort;
    /** since we don't publish our IP/port if introduced anymore, we need
        to store it somewhere. */
    private RouterAddress _currentOurV4Address;
    private RouterAddress _currentOurV6Address;

    private static final int DROPLIST_PERIOD = 10*60*1000;
    public static final String STYLE = "SSU";
    public static final String PROP_INTERNAL_PORT = "i2np.udp.internalPort";

    /** now unused, we pick a random port
     *  @deprecated unused
     */
    @Deprecated
    public static final int DEFAULT_INTERNAL_PORT = 8887;

    /** define this to explicitly set an external IP address */
    public static final String PROP_EXTERNAL_HOST = "i2np.udp.host";
    /** define this to explicitly set an external port */
    public static final String PROP_EXTERNAL_PORT = "i2np.udp.port";
    /** 
     * If i2np.udp.preferred is set to "always", the UDP bids will always be under 
     * the bid from the TCP transport - even if a TCP connection already 
     * exists.  If it is set to "true",
     * it will prefer UDP unless no UDP session exists and a TCP connection 
     * already exists.
     * If it is set to "false" (the default),
     * it will prefer TCP unless no TCP session exists and a UDP connection 
     * already exists.
     */
    public static final String PROP_PREFER_UDP = "i2np.udp.preferred";
    private static final String DEFAULT_PREFER_UDP = "false";
    
    /** Override whether we will change our advertised port no matter what our peers tell us
     *  See getIsPortFixed() for default behaviour.
     */
    private static final String PROP_FIXED_PORT = "i2np.udp.fixedPort";

    /** allowed sources of address updates */
    public static final String PROP_SOURCES = "i2np.udp.addressSources";
    public static final String DEFAULT_SOURCES = SOURCE_INTERFACE.toConfigString() + ',' +
                                                 SOURCE_UPNP.toConfigString() + ',' +
                                                 SOURCE_SSU.toConfigString();
    /** remember IP changes */
    public static final String PROP_IP= "i2np.lastIP";
    public static final String PROP_IP_CHANGE = "i2np.lastIPChange";
    public static final String PROP_LAPTOP_MODE = "i2np.laptopMode";

    /** do we require introducers, regardless of our status? */
    public static final String PROP_FORCE_INTRODUCERS = "i2np.udp.forceIntroducers";
    /** do we allow direct SSU connections, sans introducers?  */
    public static final String PROP_ALLOW_DIRECT = "i2np.udp.allowDirect";
    /** this is rarely if ever used, default is to bind to wildcard address */
    public static final String PROP_BIND_INTERFACE = "i2np.udp.bindInterface";
    /** override the "large" (max) MTU, default is PeerState.LARGE_MTU */
    private static final String PROP_DEFAULT_MTU = "i2np.udp.mtu";
    private static final String PROP_ADVANCED = "routerconsole.advanced";
        
    private static final String CAP_TESTING = "" + UDPAddress.CAPACITY_TESTING;
    private static final String CAP_TESTING_INTRO = "" + UDPAddress.CAPACITY_TESTING + UDPAddress.CAPACITY_INTRODUCER;

    /** how many relays offered to us will we use at a time? */
    public static final int PUBLIC_RELAY_COUNT = 3;
    
    private static final boolean USE_PRIORITY = false;

    /** configure the priority queue with the given split points */
    private static final int PRIORITY_LIMITS[] = new int[] { 100, 200, 300, 400, 500, 1000 };
    /** configure the priority queue with the given weighting per priority group */
    private static final int PRIORITY_WEIGHT[] = new int[] { 1, 1, 1, 1, 1, 2 };

    /** should we flood all UDP peers with the configured rate? This is for testing only! */
    //private static final boolean SHOULD_FLOOD_PEERS = false;
    
    private static final int MAX_CONSECUTIVE_FAILED = 5;
    
    public static final int DEFAULT_COST = 5;
    static final long[] RATES = { 10*60*1000 };
    /** minimum active peers to maintain IP detection, etc. */
    private static final int MIN_PEERS = 5;
    private static final int MIN_PEERS_IF_HAVE_V6 = 30;
    /** minimum peers volunteering to be introducers if we need that */
    private static final int MIN_INTRODUCER_POOL = 5;
    static final long INTRODUCER_EXPIRATION_MARGIN = 20*60*1000L;
    
    private static final int[] BID_VALUES = { 15, 20, 50, 65, 80, 95, 100, 115, TransportBid.TRANSIENT_FAIL };
    private static final int FAST_PREFERRED_BID = 0;
    private static final int SLOW_PREFERRED_BID = 1;
    private static final int FAST_BID = 2;
    private static final int SLOW_BID = 3;
    private static final int SLOWEST_BID = 4;
    private static final int SLOWEST_COST_BID = 5;
    private static final int NEAR_CAPACITY_BID = 6;
    private static final int NEAR_CAPACITY_COST_BID = 7;
    private static final int TRANSIENT_FAIL_BID = 8;
    private final TransportBid[] _cachedBid;

    // Opera doesn't have the char, TODO check UA
    //private static final String THINSP = "&thinsp;/&thinsp;";
    private static final String THINSP = " / ";

    /**
     *  RI sigtypes supported in 0.9.16, but due to a bug in InboundEstablishState
     *  fixed in 0.9.17, we cannot connect out to routers before that version.
     */
    private static final String MIN_SIGTYPE_VERSION = "0.9.17";

    /**
     *  IPv6 Peer Testing supported
     */
    private static final String MIN_V6_PEER_TEST_VERSION = "0.9.27";


    public UDPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh) {
        super(ctx);
        _networkID = ctx.router().getNetworkID();
        _dhFactory = dh;
        _log = ctx.logManager().getLog(UDPTransport.class);
        _peersByIdent = new ConcurrentHashMap<Hash, PeerState>(128);
        _peersByRemoteHost = new ConcurrentHashMap<RemoteHostId, PeerState>(128);
        _dropList = new ConcurrentHashSet<RemoteHostId>(2);
        _endpoints = new CopyOnWriteArrayList<UDPEndpoint>();
        
        // See comments in DummyThrottle.java
        if (USE_PRIORITY) {
            TimedWeightedPriorityMessageQueue mq = new TimedWeightedPriorityMessageQueue(ctx, PRIORITY_LIMITS, PRIORITY_WEIGHT, this);
            _outboundMessages = mq;
            _activeThrottle = mq;
        } else {
            DummyThrottle mq = new DummyThrottle();
            _outboundMessages = null;
            _activeThrottle = mq;
        }

        _cachedBid = new SharedBid[BID_VALUES.length];
        for (int i = 0; i < BID_VALUES.length; i++) {
            _cachedBid[i] = new SharedBid(BID_VALUES[i]);
        }

        _destroyBuilder = new PacketBuilder(_context, this);
        _fragments = new OutboundMessageFragments(_context, this, _activeThrottle);
        _inboundFragments = new InboundMessageFragments(_context, _fragments, this);
        //if (SHOULD_FLOOD_PEERS)
        //    _flooder = new UDPFlooder(_context, this);
        _expireTimeout = EXPIRE_TIMEOUT;
        _expireEvent = new ExpirePeerEvent();
        _testManager = new PeerTestManager(_context, this);
        _testEvent = new PeerTestEvent(_context, this, _testManager);
        _reachabilityStatus = Status.UNKNOWN;
        _reachabilityStatusPending = Status.OK;
        _introManager = new IntroductionManager(_context, this);
        _introducersSelectedOn = -1;
        _lastInboundReceivedOn = -1;
        _mtu = PeerState.LARGE_MTU;
        _mtu_ipv6 = PeerState.MIN_IPV6_MTU;
        setupPort();
        _needsRebuild = true;
        _min_peers = _context.getProperty("i2np.udp.minpeers", MIN_PEERS);
        _min_v6_peers = _context.getProperty("i2np.udp.minv6peers", MIN_PEERS_IF_HAVE_V6);
        
        _context.statManager().createRateStat("udp.alreadyConnected", "What is the lifetime of a reestablished session", "udp", RATES);
        _context.statManager().createRateStat("udp.droppedPeer", "How long ago did we receive from a dropped peer (duration == session lifetime", "udp", RATES);
        _context.statManager().createRateStat("udp.droppedPeerInactive", "How long ago did we receive from a dropped peer (duration == session lifetime)", "udp", RATES);
        //_context.statManager().createRateStat("udp.statusOK", "How many times the peer test returned OK", "udp", RATES);
        //_context.statManager().createRateStat("udp.statusDifferent", "How many times the peer test returned different IP/ports", "udp", RATES);
        //_context.statManager().createRateStat("udp.statusReject", "How many times the peer test returned reject unsolicited", "udp", RATES);
        //_context.statManager().createRateStat("udp.statusUnknown", "How many times the peer test returned an unknown result", "udp", RATES);
        _context.statManager().createRateStat("udp.addressTestInsteadOfUpdate", "How many times we fire off a peer test of ourselves instead of adjusting our own reachable address?", "udp", RATES);
        _context.statManager().createRateStat("udp.addressUpdated", "How many times we adjust our own reachable IP address", "udp", RATES);
        _context.statManager().createRateStat("udp.proactiveReestablish", "How long a session was idle for when we proactively reestablished it", "udp", RATES);
        _context.statManager().createRateStat("udp.dropPeerDroplist", "How many peers currently have their packets dropped outright when a new peer is added to the list?", "udp", RATES);
        _context.statManager().createRateStat("udp.dropPeerConsecutiveFailures", "How many consecutive failed sends to a peer did we attempt before giving up and reestablishing a new session (lifetime is inactivity perood)", "udp", RATES);
        _context.statManager().createRateStat("udp.inboundIPv4Conn", "Inbound IPv4 UDP Connection", "udp", RATES);
        _context.statManager().createRateStat("udp.inboundIPv6Conn", "Inbound IPv4 UDP Connection", "udp", RATES);
        // following are for PacketBuider
        //_context.statManager().createRateStat("udp.packetAuthTime", "How long it takes to encrypt and MAC a packet for sending", "udp", RATES);
        //_context.statManager().createRateStat("udp.packetAuthTimeSlow", "How long it takes to encrypt and MAC a packet for sending (when its slow)", "udp", RATES);

        _context.simpleTimer2().addPeriodicEvent(new PingIntroducers(), MIN_EXPIRE_TIMEOUT * 3 / 4);
    }
    
    /**
     *  Pick a port if not previously configured, so that TransportManager may
     *  call getRequestedPort() before we've started to get a best-guess of what our
     *  port is going to be, and pass that to NTCP
     *
     *  @since IPv6
     */
    private void setupPort() {
        int port = getRequestedPort();
        if (port < 0) {
            port = UDPEndpoint.selectRandomPort(_context);
            Map<String, String> changes = new HashMap<String, String>();
            changes.put(PROP_INTERNAL_PORT, Integer.toString(port));
            changes.put(PROP_EXTERNAL_PORT, Integer.toString(port));
            _context.router().saveConfig(changes, null);
            _log.logAlways(Log.INFO, "UDP selected random port " + port);
        }
    }

    private synchronized void startup() {
        _fragments.shutdown();
        if (_pusher != null)
            _pusher.shutdown();
        if (_handler != null) 
            _handler.shutdown();
        for (UDPEndpoint endpoint : _endpoints) {
            endpoint.shutdown();
            // should we remove?
            _endpoints.remove(endpoint);
        }
        if (_establisher != null)
            _establisher.shutdown();
        if (_refiller != null)
            _refiller.shutdown();
        _inboundFragments.shutdown();
        //if (_flooder != null)
        //    _flooder.shutdown();
        _introManager.reset();
        UDPPacket.clearCache();
        
        if (_log.shouldLog(Log.WARN)) _log.warn("Starting SSU transport listening");
        _introKey = new SessionKey(new byte[SessionKey.KEYSIZE_BYTES]);
        System.arraycopy(_context.routerHash().getData(), 0, _introKey.getData(), 0, SessionKey.KEYSIZE_BYTES);
        
        // bind host
        // This is not exposed in the UI and in practice is always null.
        // We use PROP_EXTERNAL_HOST instead. See below.
        String bindTo = _context.getProperty(PROP_BIND_INTERFACE);

        if (bindTo == null) {
            // If we are configured with a fixed IP address,
            // AND it's one of our local interfaces,
            // bind only to that.
            String fixedHost = _context.getProperty(PROP_EXTERNAL_HOST);
            if (fixedHost != null && fixedHost.length() > 0) {
                // Generate a comma-separated list of valid IP addresses
                // that we can bind to,
                // from the comma-separated PROP_EXTERNAL_HOST config.
                // The config may contain IPs or hostnames; expand each
                // hostname to one or more (v4 or v6) IPs.
                TransportUtil.IPv6Config cfg = getIPv6Config();
                Set<String> myAddrs;
                if (cfg == IPV6_DISABLED)
                    myAddrs = Addresses.getAddresses(false, false);
                else
                    myAddrs = Addresses.getAddresses(false, true);
                StringBuilder buf = new StringBuilder();
                String[] bta = DataHelper.split(fixedHost, "[,; \r\n\t]");
                for (int i = 0; i < bta.length; i++) {
                    String bt = bta[i];
                    if (bt.length() <= 0)
                        continue;
                    try {
                        InetAddress[] all = InetAddress.getAllByName(bt);
                        for (int j = 0; j < all.length; j++) {
                            InetAddress ia = all[j];
                            if (cfg == IPV6_ONLY && (ia instanceof Inet4Address)) {
                                if (_log.shouldWarn())
                                    _log.warn("Configured for IPv6 only, not binding to configured IPv4 host " + bt);
                                continue;
                            }
                            String testAddr = ia.getHostAddress();
                            if (myAddrs.contains(testAddr)) {
                                if (buf.length() > 0)
                                    buf.append(',');
                                buf.append(testAddr);
                            } else {
                                if (_log.shouldWarn())
                                    _log.warn("Not a local address, not binding to configured IP " + testAddr);
                            }
                        }
                    } catch (UnknownHostException uhe) {
                        if (_log.shouldWarn())
                            _log.warn("Not binding to configured host " + bt + " - " + uhe);
                    }
                }
                if (buf.length() > 0) {
                    bindTo = buf.toString();
                    if (_log.shouldWarn() && !fixedHost.equals(bindTo))
                        _log.warn("Expanded external host config \"" + fixedHost + "\" to \"" + bindTo + '"');
                }
            }
        }

        // construct a set of addresses
        Set<InetAddress> bindToAddrs = new HashSet<InetAddress>(4);
        if (bindTo != null) {
            // Generate a set IP addresses
            // that we can bind to,
            // from the comma-separated PROP_BIND_INTERFACE config,
            // or as generated from the PROP_EXTERNAL_HOST config.
            // In theory, the config may contain IPs and/or hostnames.
            // However, in practice, it's only IPs, because any hostnames
            // in PROP_EXTERNAL_HOST were expanded above to one or more (v4 or v6) IPs.
            // PROP_BIND_INTERFACE is not exposed in the UI and is never set.
            String[] bta = DataHelper.split(bindTo, "[,; \r\n\t]");
            for (int i = 0; i < bta.length; i++) {
                String bt = bta[i];
                if (bt.length() <= 0)
                    continue;
                try {
                    bindToAddrs.add(InetAddress.getByName(bt));
                } catch (UnknownHostException uhe) {
                    _log.error("Invalid SSU bind interface specified [" + bt + "]", uhe);
                    //setReachabilityStatus(CommSystemFacade.STATUS_HOSED);
                    //return;
                    // fall thru...
                }
            }
        }
        
        // Requested bind port
        // This may be -1 or may not be honored if busy,
        // Priority: Configured internal, then already used, then configured external
        // we will check below after starting up the endpoint.
        int port;
        int oldIPort = _context.getProperty(PROP_INTERNAL_PORT, -1);
        int oldBindPort = getListenPort(false);
        int oldEPort = _context.getProperty(PROP_EXTERNAL_PORT, -1);
        if (oldIPort > 0)
            port = oldIPort;
        else if (oldBindPort > 0)
            port = oldBindPort;
        else
            port = oldEPort;
        if (!bindToAddrs.isEmpty() && _log.shouldLog(Log.WARN))
            _log.warn("Binding only to " + bindToAddrs);
        if (_log.shouldLog(Log.INFO))
            _log.info("Binding to the port: " + port);
        if (_endpoints.isEmpty()) {
            // _endpoints will always be empty since we removed them above
            if (bindToAddrs.isEmpty()) {
                UDPEndpoint endpoint = new UDPEndpoint(_context, this, port, null);
                _endpoints.add(endpoint);
                setMTU(null);
            } else {
                for (InetAddress bindToAddr : bindToAddrs) {
                    UDPEndpoint endpoint = new UDPEndpoint(_context, this, port, bindToAddr);
                    _endpoints.add(endpoint);
                    setMTU(bindToAddr);
                }
            }
        } else {
            // unused for now
            for (UDPEndpoint endpoint : _endpoints) {
                if (endpoint.isIPv4()) {
                    // hack, first IPv4 endpoint, FIXME
                    // todo, set bind address too
                    endpoint.setListenPort(port);
                    break;
                }
            }
        }

        if (_establisher == null)
            _establisher = new EstablishmentManager(_context, this);
        
        if (_handler == null)
            _handler = new PacketHandler(_context, this, _establisher, _inboundFragments, _testManager, _introManager);
        
        // See comments in DummyThrottle.java
        if (USE_PRIORITY && _refiller == null)
            _refiller = new OutboundRefiller(_context, _fragments, _outboundMessages);
        
        //if (SHOULD_FLOOD_PEERS && _flooder == null)
        //    _flooder = new UDPFlooder(_context, this);
        
        // Startup the endpoint with the requested port, check the actual port, and
        // take action if it failed or was different than requested or it needs to be saved
        int newPort = -1;
        for (UDPEndpoint endpoint : _endpoints) {
            try {
                endpoint.startup();
                // hack, first IPv4 endpoint, FIXME
                if (newPort < 0 && endpoint.isIPv4()) {
                    newPort = endpoint.getListenPort();
                }
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Started " + endpoint);
            } catch (SocketException se) {
                _endpoints.remove(endpoint);
                _log.error("Failed to start " + endpoint, se);
            }
        }
        if (_endpoints.isEmpty()) {
            _log.log(Log.CRIT, "Unable to open UDP port");
            setReachabilityStatus(Status.HOSED);
            return;
        }
        if (newPort > 0 &&
            (newPort != port || newPort != oldIPort || newPort != oldEPort)) {
            // attempt to use it as our external port - this will be overridden by
            // externalAddressReceived(...)
            Map<String, String> changes = new HashMap<String, String>();
            changes.put(PROP_INTERNAL_PORT, Integer.toString(newPort));
            changes.put(PROP_EXTERNAL_PORT, Integer.toString(newPort));
            _context.router().saveConfig(changes, null);
        }

        _handler.startup();
        _fragments.startup();
        _inboundFragments.startup();
        _pusher = new PacketPusher(_context, _fragments, _endpoints);
        _pusher.startup();
        // must be after pusher
        _establisher.startup();
        if (USE_PRIORITY)
            _refiller.startup();
        //if (SHOULD_FLOOD_PEERS)
        //    _flooder.startup();
        _expireEvent.setIsAlive(true);
        _reachabilityStatus = Status.UNKNOWN;
        _testEvent.setIsAlive(true); // this queues it for 3-6 minutes in the future...
        _testEvent.reschedule(10*1000); // lets requeue it for Real Soon

        // set up external addresses
        // REA param is false;
        // TransportManager.startListening() calls router.rebuildRouterInfo()
        if (newPort > 0 && bindToAddrs.isEmpty()) {
            for (InetAddress ia : getSavedLocalAddresses()) {
                // Discovered or configured addresses are presumed good at the start.
                // when externalAddressReceived() was called with SOURCE_INTERFACE,
                // isAlive() was false, so setReachabilityStatus() was not called
                // TODO should we set both to unknown and wait for an inbound v6 conn,
                // since there's no v6 testing?
                if (ia.getAddress().length == 16) {
                    // FIXME we need to check and time out after an hour of no inbound ipv6,
                    // change to firewalled maybe? but we don't have any test to restore
                    // a v6 address after it's removed.
                    _lastInboundIPv6 = _context.clock().now();
                    if (!isIPv6Firewalled())
                        setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
                } else {
                    if (!isIPv4Firewalled())
                        setReachabilityStatus(Status.IPV4_OK_IPV6_UNKNOWN);
                }
                rebuildExternalAddress(ia.getHostAddress(), newPort, false);
            }
        } else if (newPort > 0 && !bindToAddrs.isEmpty()) {
            for (InetAddress ia : bindToAddrs) {
                if (ia.getAddress().length == 16) {
                    _lastInboundIPv6 = _context.clock().now();
                    if (!isIPv6Firewalled())
                        setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
                } else {
                    if (!isIPv4Firewalled())
                        setReachabilityStatus(Status.IPV4_OK_IPV6_UNKNOWN);
                }
                rebuildExternalAddress(ia.getHostAddress(), newPort, false);
            }
            // TODO
            // If we are bound only to v4 addresses,
            // force _haveIPv6Address to false, or else we get 'no endpoint' errors
            // If we are bound only to v6 addresses,
            // override getIPv6Config() ?
        }
        if (isIPv4Firewalled()) {
            if (_lastInboundIPv6 > 0)
                setReachabilityStatus(Status.IPV4_FIREWALLED_IPV6_UNKNOWN);
            else
                setReachabilityStatus(Status.REJECT_UNSOLICITED);
        }
        rebuildExternalAddress(false);
    }
    
    public synchronized void shutdown() {
        destroyAll();
        for (UDPEndpoint endpoint : _endpoints) {
            endpoint.shutdown();
            // should we remove?
            _endpoints.remove(endpoint);
        }
        //if (_flooder != null)
        //    _flooder.shutdown();
        if (_refiller != null)
            _refiller.shutdown();
        if (_handler != null)
            _handler.shutdown();
        if (_pusher != null)
            _pusher.shutdown();
        _fragments.shutdown();
        if (_establisher != null)
            _establisher.shutdown();
        _inboundFragments.shutdown();
        _expireEvent.setIsAlive(false);
        _testEvent.setIsAlive(false);
        _peersByRemoteHost.clear();
        _peersByIdent.clear();
        _dropList.clear();
        _introManager.reset();
        UDPPacket.clearCache();
        UDPAddress.clearCache();
        _lastInboundIPv6 = 0;
    }

    /**
     *  The endpoint has failed. Remove it.
     *
     *  @since 0.9.16
     */
    public void fail(UDPEndpoint endpoint) {
        if (_endpoints.remove(endpoint)) {
            _log.log(Log.CRIT, "UDP port failure: " + endpoint);
            if (_endpoints.isEmpty()) {
                _log.log(Log.CRIT, "No more UDP sockets open");
                setReachabilityStatus(Status.HOSED);
                // TODO restart?
            }
            rebuildExternalAddress();
        }
    }

    /** @since IPv6 */
    private boolean isAlive() {
        return _inboundFragments.isAlive();
    }
    
    /**
     * Introduction key that people should use to contact us
     *
     */
    SessionKey getIntroKey() { return _introKey; }

    int getExternalPort(boolean ipv6) {
        RouterAddress addr = getCurrentAddress(ipv6);
        if (addr != null) {
            int rv = addr.getPort();
            if (rv > 0)
                return rv;
        }
        return getRequestedPort(ipv6);
    }

    /**
     *  IPv4 only
     *  @return IP or null
     *  @since 0.9.2
     */
    byte[] getExternalIP() {
        RouterAddress addr = getCurrentAddress(false);
        if (addr != null)
            return addr.getIP();
        return null;
    }

    /**
     *  For PeerTestManager
     *  @since 0.9.30
     */
    boolean hasIPv6Address() {
        return _haveIPv6Address;
    }

    /**
     *  Is this IP too close to ours to trust it for
     *  things like relaying?
     *  @param ip IPv4 or IPv6
     *  @since IPv6
     */
    boolean isTooClose(byte[] ip) {
        if (allowLocal())
            return false;
        for (RouterAddress addr : getCurrentAddresses()) {
            byte[] myip = addr.getIP();
            if (myip == null || ip.length != myip.length)
                continue;
            if (ip.length == 4) {
                if (DataHelper.eq(ip, 0, myip, 0, 2))
                    return true;
            } else if (ip.length == 16) {
                if (DataHelper.eq(ip, 0, myip, 0, 8))
                    return true;
            }
        }
        return false;
    }

    /**
     *  The current port of the first matching endpoint.
     *  To be enhanced to handle multiple endpoints of the same type.
     *  @return port or -1
     *  @since IPv6
     */
    private int getListenPort(boolean ipv6) {
        for (UDPEndpoint endpoint : _endpoints) {
            if (((!ipv6) && endpoint.isIPv4()) ||
                (ipv6 && endpoint.isIPv6()))
                return endpoint.getListenPort();
       }
       return -1;
    }

    /**
     *  The current or configured internal IPv4 port.
     *  UDPEndpoint should always be instantiated (and a random port picked if not configured)
     *  before this is called, so the returned value should be &gt; 0
     *  unless the endpoint failed to bind.
     */
    @Override
    public int getRequestedPort() {
        return getRequestedPort(false);
    }

    /**
     *  The current or configured internal port.
     *  UDPEndpoint should always be instantiated (and a random port picked if not configured)
     *  before this is called, so the returned value should be &gt; 0
     *  unless the endpoint failed to bind.
     */
    private int getRequestedPort(boolean ipv6) {
        int rv = getListenPort(ipv6);
        if (rv > 0)
            return rv;
        // fallbacks
        rv = _context.getProperty(PROP_INTERNAL_PORT, -1);
        if (rv > 0)
            return rv;
        return _context.getProperty(PROP_EXTERNAL_PORT, -1);
    }

    /**
     *  Set the MTU for the socket interface at addr.
     *  @param addr null ok
     *  @return the mtu
     *  @since 0.9.2
     */
    private int setMTU(InetAddress addr) {
        String p = _context.getProperty(PROP_DEFAULT_MTU);
        if (p != null) {
            try {
                int pmtu = Integer.parseInt(p);
                _mtu = MTU.rectify(false, pmtu);
                _mtu_ipv6 = MTU.rectify(true, pmtu);
                return _mtu;
            } catch (NumberFormatException nfe) {}
        }
        int mtu = MTU.getMTU(addr);
        if (addr != null && addr.getAddress().length == 16) {
            if (mtu <= 0)
                mtu = PeerState.MIN_IPV6_MTU;
            _mtu_ipv6 = mtu;
        } else {
            if (mtu <= 0)
                mtu = PeerState.LARGE_MTU;
            _mtu = mtu;
        }
        return mtu;
    }

    /**
     * The MTU for the socket interface.
     * To be used as the "large" MTU.
     * @return limited to range PeerState.MIN_MTU to PeerState.LARGE_MTU.
     * @since 0.9.2
     */
    int getMTU(boolean ipv6) {
        // TODO multiple interfaces of each type
        return ipv6 ? _mtu_ipv6 : _mtu;
    }

    /**
     * If we have received an inbound connection in the last 2 minutes, don't allow 
     * our IP to change.
     */
    private static final int ALLOW_IP_CHANGE_INTERVAL = 2*60*1000;
    
    void inboundConnectionReceived(boolean isIPv6) {
        if (isIPv6) {
            _lastInboundIPv6 = _context.clock().now();
            _context.statManager().addRateData("udp.inboundIPv6Conn", 1);
            // former workaround for lack of IPv6 peer testing
            //if (_currentOurV6Address != null)
            //    setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
        } else {
            // Introduced connections are still inbound, this is not evidence
            // that we are not firewalled.
            // use OS clock since its an ordering thing, not a time thing
            _lastInboundReceivedOn = System.currentTimeMillis(); 
            _context.statManager().addRateData("udp.inboundIPv4Conn", 1);
        }
    }
    
    // temp prevent multiples
    private boolean gotIPv4Addr = false;
    private boolean gotIPv6Addr = false;

    /**
     * From config, UPnP, local i/f, ...
     * Not for info received from peers - see externalAddressReceived(Hash, ip, port)
     *
     * @param source as defined in Transport.SOURCE_xxx
     * @param ip publicly routable IPv4 or IPv6, null ok
     * @param port 0 if unknown
     */
    @Override
    public void externalAddressReceived(Transport.AddressSource source, byte[] ip, int port) {
        if (_log.shouldLog(Log.WARN))
            _log.warn("Received address: " + Addresses.toString(ip, port) + " from: " + source);
        if (ip == null)
            return;
        // this is essentially isValid(ip), but we can't use that because
        // _haveIPv6Address is not set yet
        if (!(isPubliclyRoutable(ip) || allowLocal())) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Invalid address: " + Addresses.toString(ip, port) + " from: " + source);
            return;
        }
        if (source == SOURCE_INTERFACE && ip.length == 16) {
            // NOW we can set it, it's a valid v6 address
            // (we don't want to set this for Teredo, 6to4, ...)
            _haveIPv6Address = true;
        }
        if (explicitAddressSpecified())
            return;
        String sources = _context.getProperty(PROP_SOURCES, DEFAULT_SOURCES);
        if (!sources.contains(source.toConfigString()))
            return;
        if (!isAlive()) {
            if (source == SOURCE_INTERFACE || source == SOURCE_UPNP) {
                try {
                    InetAddress ia = InetAddress.getByAddress(ip);
                    saveLocalAddress(ia);
                } catch (UnknownHostException uhe) {}
            }
            return;
        }
        if (source == SOURCE_INTERFACE) {
            // temp prevent multiples
            if (ip.length == 4) {
                if (gotIPv4Addr)
                    return;
                else
                    gotIPv4Addr = true;
            } else if (ip.length == 16) {
                if (gotIPv6Addr)
                    return;
                else
                    gotIPv6Addr = true;
            }
        }
        boolean changed = changeAddress(ip, port);
        // Assume if we have an interface with a public IP that we aren't firewalled.
        // If this is wrong, the peer test will figure it out and change the status.
        if (changed && source == SOURCE_INTERFACE) {
            if (ip.length == 4) {
                if (!isIPv4Firewalled())
                    setReachabilityStatus(Status.IPV4_OK_IPV6_UNKNOWN);
            } else if (ip.length == 16) {
                // TODO should we set both to unknown and wait for an inbound v6 conn,
                // since there's no v6 testing?
                if (!isIPv6Firewalled())
                    setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
            }
        }
    }

    /**
     *  Callback from UPnP.
     *  If we we have an IP address and UPnP claims success, believe it.
     *  If this is wrong, the peer test will figure it out and change the status.
     *  Don't do anything if UPnP claims failure.
     */
    @Override
    public void forwardPortStatus(byte[] ip, int port, int externalPort, boolean success, String reason) {
        if (_log.shouldLog(Log.WARN)) {
            if (success)
                _log.warn("UPnP has opened the SSU port: " + port + " via " + Addresses.toString(ip, externalPort));
            else
                _log.warn("UPnP has failed to open the SSU port: " + port + " reason: " + reason);
        }
        if (success && ip != null && getExternalIP() != null) {
            if (!isIPv4Firewalled())
                setReachabilityStatus(Status.IPV4_OK_IPV6_UNKNOWN);
        }
    }

    /**
     * Someone we tried to contact gave us what they think our IP address is.
     * Right now, we just blindly trust them, changing our IP and port on a
     * whim.  this is not good ;)
     *
     * Slight enhancement - require two different peers in a row to agree
     *
     * Todo:
     *   - Much better tracking of troublemakers
     *   - Disable if we have good local address or UPnP
     *   - This gets harder if and when we publish multiple addresses, or IPv6
     * 
     * @param from Hash of inbound destination
     * @param ourIP publicly routable IPv4 only
     * @param ourPort &gt;= 1024
     */
    void externalAddressReceived(Hash from, byte ourIP[], int ourPort) {
        boolean isValid = isValid(ourIP) &&
                          TransportUtil.isValidPort(ourPort);
        boolean explicitSpecified = explicitAddressSpecified();
        boolean inboundRecent = _lastInboundReceivedOn + ALLOW_IP_CHANGE_INTERVAL > System.currentTimeMillis();
        if (_log.shouldLog(Log.INFO))
            _log.info("External address received: " + Addresses.toString(ourIP, ourPort) + " from " 
                      + from + ", isValid? " + isValid + ", explicitSpecified? " + explicitSpecified 
                      + ", receivedInboundRecent? " + inboundRecent + " status " + _reachabilityStatus);
        if (ourIP.length != 4) {
            return;
        }
        
        if (explicitSpecified) 
            return;
        String sources = _context.getProperty(PROP_SOURCES, DEFAULT_SOURCES);
        if (!sources.contains("ssu"))
            return;
        
        if (!isValid) {
            // ignore them 
            if (_log.shouldLog(Log.ERROR))
                _log.error("The router " + from + " told us we have an invalid IP - " 
                           + Addresses.toString(ourIP, ourPort) + ".  Lets throw tomatoes at them");
            markUnreachable(from);
            //_context.banlist().banlistRouter(from, "They said we had an invalid IP", STYLE);
            return;
        }

        RouterAddress addr = getCurrentExternalAddress(false);
        if (inboundRecent && addr != null && addr.getPort() > 0 && addr.getHost() != null) {
            // use OS clock since its an ordering thing, not a time thing
            // Note that this fails us if we switch from one IP to a second, then back to the first,
            // as some routers still have the first IP and will successfully connect,
            // leaving us thinking the second IP is still good.
            if (_log.shouldLog(Log.INFO))
                _log.info("Ignoring IP address suggestion, since we have received an inbound con recently");
        } else {
            // New IP
            boolean changeIt = false;
            synchronized(this) {
                if (from.equals(_lastFrom) || !eq(_lastOurIP, _lastOurPort, ourIP, ourPort)) {
                    _lastFrom = from;
                    _lastOurIP = ourIP;
                    _lastOurPort = ourPort;
                    if (_log.shouldLog(Log.INFO))
                        _log.info("The router " + from + " told us we have a new IP - " 
                                  + Addresses.toString(ourIP, ourPort) + ".  Wait until somebody else tells us the same thing.");
                } else {
                    _lastFrom = from;
                    _lastOurIP = ourIP;
                    _lastOurPort = ourPort;
                    changeIt = true;
                }
            }
            if (changeIt) {
                if (_log.shouldLog(Log.INFO))
                    _log.info(from + " and " + _lastFrom + " agree we have a new IP - " 
                              + Addresses.toString(ourIP, ourPort) + ".  Changing address.");
                changeAddress(ourIP, ourPort);
            }
        }
    }
    
    /**
     * Possibly change our external address to the IP/port.
     * IP/port are already validated, but not yet compared to current IP/port.
     * We compare here.
     *
     * @param ourIP MUST have been previously validated with isValid()
     *              IPv4 or IPv6 OK
     * @param ourPort &gt;= 1024 or 0 for no change
     */
    private boolean changeAddress(byte ourIP[], int ourPort) {
        // this defaults to true when we are firewalled and false otherwise.
        boolean fixedPort = getIsPortFixed();
        boolean updated = false;
        boolean fireTest = false;

        boolean isIPv6 = ourIP.length == 16;
        RouterAddress current = getCurrentExternalAddress(isIPv6);
        byte[] externalListenHost = current != null ? current.getIP() : null;
        int externalListenPort = current != null ? current.getPort() : getRequestedPort(isIPv6);

        if (_log.shouldLog(Log.INFO))
            _log.info("Change address? status = " + _reachabilityStatus +
                      " diff = " + (_context.clock().now() - _reachabilityStatusLastUpdated) +
                      " old = " + Addresses.toString(externalListenHost, externalListenPort) +
                      " new = " + Addresses.toString(ourIP, ourPort));

        if ((fixedPort && externalListenPort > 0) || ourPort <= 0)
            ourPort = externalListenPort;

            synchronized (this) {
                if (ourPort > 0 &&
                    !eq(externalListenHost, externalListenPort, ourIP, ourPort)) {
                    // This prevents us from changing our IP when we are not firewalled
                    //if ( (_reachabilityStatus != CommSystemFacade.STATUS_OK) ||
                    //     (_externalListenHost == null) || (_externalListenPort <= 0) ||
                    //     (_context.clock().now() - _reachabilityStatusLastUpdated > 2*TEST_FREQUENCY) ) {
                        // they told us something different and our tests are either old or failing
                            if (_log.shouldLog(Log.WARN))
                                _log.warn("Trying to change our external address to " +
                                          Addresses.toString(ourIP, ourPort));
                            RouterAddress newAddr = rebuildExternalAddress(ourIP, ourPort, true);
                            updated = newAddr != null;
                    //} else {
                    //    // they told us something different, but our tests are recent and positive,
                    //    // so lets test again
                    //    fireTest = true;
                    //    if (_log.shouldLog(Log.WARN))
                    //        _log.warn("Different address, but we're fine.. (" + _reachabilityStatus + ")");
                    //}
                } else {
                    // matched what we expect
                    if (_log.shouldLog(Log.INFO))
                        _log.info("Same address as the current one");
                }
            }

        if (fireTest) {
            // always false, commented out above
            _context.statManager().addRateData("udp.addressTestInsteadOfUpdate", 1);
            _testEvent.forceRunImmediately(isIPv6);
        } else if (updated) {
            _context.statManager().addRateData("udp.addressUpdated", 1);
            Map<String, String> changes = new HashMap<String, String>();
            if (ourIP.length == 4 && !fixedPort)
                changes.put(PROP_EXTERNAL_PORT, Integer.toString(ourPort));
            // queue a country code lookup of the new IP
            if (ourIP.length == 4)
                _context.commSystem().queueLookup(ourIP);
            // store these for laptop-mode (change ident on restart... or every time... when IP changes)
            // IPV4 ONLY
            String oldIP = _context.getProperty(PROP_IP);
            String newIP = Addresses.toString(ourIP);
            if (ourIP.length == 4 && !newIP.equals(oldIP)) {
                long lastChanged = 0;
                long now = _context.clock().now();
                String lcs = _context.getProperty(PROP_IP_CHANGE);
                if (lcs != null) {
                    try {
                        lastChanged = Long.parseLong(lcs);
                    } catch (NumberFormatException nfe) {}
                }

                changes.put(PROP_IP, newIP);
                changes.put(PROP_IP_CHANGE, Long.toString(now));
                _context.router().saveConfig(changes, null);

                if (oldIP != null) {
                    _context.router().eventLog().addEvent(EventLog.CHANGE_IP, newIP);
                }

                // laptop mode
                // For now, only do this at startup
                if (oldIP != null &&
                    System.getProperty("wrapper.version") != null &&
                    _context.getBooleanProperty(PROP_LAPTOP_MODE) &&
                    now - lastChanged > 10*60*1000 &&
                    _context.router().getUptime() < 10*60*1000) {
                    _log.log(Log.CRIT, "IP changed, restarting with a new identity and port");
                    // this removes the UDP port config
                    _context.router().killKeys();
                    // do we need WrapperManager.signalStopped() like in ConfigServiceHandler ???
                    // without it, the wrapper complains "shutdown unexpectedly"
                    // but we can't have that dependency in the router
                    _context.router().shutdown(Router.EXIT_HARD_RESTART);
                    // doesn't return
                }
            } else if (ourIP.length == 4 && !fixedPort) {
                // save PROP_EXTERNAL_PORT
                _context.router().saveConfig(changes, null);
            }
            // deadlock thru here ticket #1699
            _context.router().rebuildRouterInfo();
            _testEvent.forceRunImmediately(isIPv6);
        }
        return updated;
    }

    /**
     *  @param laddr and raddr may be null
     */
    private static final boolean eq(byte laddr[], int lport, byte raddr[], int rport) {
        return (rport == lport) && DataHelper.eq(laddr, raddr);
    }
    
    /**
     * An IPv6 address is only valid if we are configured to support IPv6
     * AND we have a public IPv6 address.
     *
     * @param addr may be null, returns false
     */
    public final boolean isValid(byte addr[]) {
        if (addr == null) return false;
        if (isPubliclyRoutable(addr) &&
            (addr.length != 16 || _haveIPv6Address))
            return true;
        return allowLocal();
    }

    /**
     *  Was true before 0.9.2
     *  Now false if we need introducers (as perhaps that's why we need them,
     *  our firewall is changing our port), unless overridden by the property.
     *  We must have an accurate external port when firewalled, or else
     *  our signature of the SessionCreated packet will be invalid.
     */
    private boolean getIsPortFixed() {
        String prop = _context.getProperty(PROP_FIXED_PORT);
        if (prop != null)
            return Boolean.parseBoolean(prop);
        Status status = getReachabilityStatus();
        return status != Status.REJECT_UNSOLICITED &&
               status != Status.IPV4_FIREWALLED_IPV6_OK &&
               status != Status.IPV4_FIREWALLED_IPV6_UNKNOWN;
    }

    /** 
     * get the state for the peer at the given remote host/port, or null 
     * if no state exists
     */
    PeerState getPeerState(RemoteHostId hostInfo) {
            return _peersByRemoteHost.get(hostInfo);
    }

    /** 
     *  Get the states for all peers at the given remote host, ignoring port.
     *  Used for a last-chance search for a peer that changed port, by PacketHandler.
     *  Always returns empty list for IPv6 hostInfo.
     *  @since 0.9.3
     */
    List<PeerState> getPeerStatesByIP(RemoteHostId hostInfo) {
        List<PeerState> rv = new ArrayList<PeerState>(4);
        byte[] ip = hostInfo.getIP();
        if (ip != null && ip.length == 4) {
            for (PeerState ps : _peersByIdent.values()) {
                if (DataHelper.eq(ip, ps.getRemoteIP()))
                    rv.add(ps);
            }
        }
        return rv;
    }
    
    /** 
     * get the state for the peer with the given ident, or null 
     * if no state exists
     */
    PeerState getPeerState(Hash remotePeer) { 
            return _peersByIdent.get(remotePeer);
    }
    
    /** 
     *  Remove and add to peersByRemoteHost map
     *  @since 0.9.3
     */
    void changePeerPort(PeerState peer, int newPort) {
        // this happens a lot
        int oldPort;
        synchronized (_addDropLock) {
            oldPort = peer.getRemotePort();
            if (oldPort != newPort) {
                _peersByRemoteHost.remove(peer.getRemoteHostId());
                peer.changePort(newPort);
                _peersByRemoteHost.put(peer.getRemoteHostId(), peer);
            }
        }
        if (_log.shouldInfo() && oldPort != newPort)
            _log.info("Changed port from " + oldPort + " to " + newPort + " for " + peer);
    }

    /**
     *  For IntroductionManager
     *  @return may be null if not started
     *  @since 0.9.2
     */
    EstablishmentManager getEstablisher() {
        return _establisher;
    }

    /**
     * Intercept RouterInfo entries received directly from a peer to inject them into
     * the PeersByCapacity listing.
     *
     */
    /*
    public void messageReceived(I2NPMessage inMsg, RouterIdentity remoteIdent, Hash remoteIdentHash, long msToReceive, int bytesReceived) {
        
        if (inMsg instanceof DatabaseStoreMessage) {
            DatabaseStoreMessage dsm = (DatabaseStoreMessage)inMsg;
            if (dsm.getValueType() == DatabaseStoreMessage.KEY_TYPE_ROUTERINFO) {
                Hash from = remoteIdentHash;
                if (from == null)
                    from = remoteIdent.getHash();

                if (from.equals(dsm.getKey())) {
                    // db info received directly from the peer - inject it into the peersByCapacity
                    RouterInfo info = dsm.getRouterInfo();
                    Set addresses = info.getAddresses();
                    for (Iterator iter = addresses.iterator(); iter.hasNext(); ) {
                        RouterAddress addr = (RouterAddress)iter.next();
                        if (!STYLE.equals(addr.getTransportStyle()))
                            continue;
                        Properties opts = addr.getOptions();
                        if ( (opts != null) && (info.isValid()) ) {
                            String capacities = opts.getProperty(UDPAddress.PROP_CAPACITY);
                            if (capacities != null) {
                                if (_log.shouldLog(Log.INFO))
                                    _log.info("Intercepting and storing the capacities for " + from.toBase64() + ": " + capacities);
                                PeerState peer = getPeerState(from);
                                for (int i = 0; i < capacities.length(); i++) {
                                    char capacity = capacities.charAt(i);
                                    int cap = capacity - 'A';
                                    if ( (cap < 0) || (cap >= _peersByCapacity.length) ) 
                                        continue;
                                    List peers = _peersByCapacity[cap];
                                    synchronized (peers) {
                                        if ( (peers.size() < MAX_PEERS_PER_CAPACITY) && (!peers.contains(peer)) )
                                            peers.add(peer);
                                    }
                                }
                            }
                        }
                        // this was an SSU address so we're done now
                        break;
                    }
                }
            }
        }
        super.messageReceived(inMsg, remoteIdent, remoteIdentHash, msToReceive, bytesReceived);
    }
    */
    
    /** 
     * add the peer info, returning true if it went in properly, false if
     * it was rejected (causes include peer ident already connected, or no
     * remote host info known
     *
     */
    boolean addRemotePeerState(PeerState peer) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Add remote peer state: " + peer);
        synchronized(_addDropLock) {
            return locked_addRemotePeerState(peer);
        }
    }

    private boolean locked_addRemotePeerState(PeerState peer) {
        Hash remotePeer = peer.getRemotePeer();
        long oldEstablishedOn = -1;
        PeerState oldPeer = null;
        if (remotePeer != null) {
            oldPeer = _peersByIdent.put(remotePeer, peer);
            if ( (oldPeer != null) && (oldPeer != peer) ) {
                // this happens a lot
                if (_log.shouldInfo())
                    _log.info("Peer already connected (PBID): old=" + oldPeer + " new=" + peer);
                // transfer over the old state/inbound message fragments/etc
                peer.loadFrom(oldPeer);
                oldEstablishedOn = oldPeer.getKeyEstablishedTime();
            }
        }
        
        RemoteHostId remoteId = peer.getRemoteHostId();
        if (oldPeer != null) {
            oldPeer.dropOutbound();
            _introManager.remove(oldPeer);
            _expireEvent.remove(oldPeer);
            RemoteHostId oldID = oldPeer.getRemoteHostId();
            if (!remoteId.equals(oldID)) {
                // leak fix, remove old address
                if (_log.shouldInfo())
                    _log.info(remotePeer + " changed address FROM " + oldID + " TO " + remoteId);
                PeerState oldPeer2 = _peersByRemoteHost.remove(oldID);
                // different ones in the two maps? shouldn't happen
                if (oldPeer2 != oldPeer && oldPeer2 != null) {
                    oldPeer2.dropOutbound();
                     _introManager.remove(oldPeer2);
                    _expireEvent.remove(oldPeer2);
                }
            }
        }

        // Should always be direct... except maybe for hidden mode?
        // or do we always know the IP by now?
        if (remoteId.getIP() == null && _log.shouldLog(Log.WARN))
            _log.warn("Add indirect: " + peer);

        // don't do this twice
        PeerState oldPeer2 = _peersByRemoteHost.put(remoteId, peer);
        if (oldPeer2 != null && oldPeer2 != peer && oldPeer2 != oldPeer) {
            // this shouldn't happen, should have been removed above
            if (_log.shouldLog(Log.WARN))
                _log.warn("Peer already connected (PBRH): old=" + oldPeer2 + " new=" + peer);
            // transfer over the old state/inbound message fragments/etc
            peer.loadFrom(oldPeer2);
            oldEstablishedOn = oldPeer2.getKeyEstablishedTime();
            oldPeer2.dropOutbound();
            _introManager.remove(oldPeer2);
            _expireEvent.remove(oldPeer2);
        }

        if (_log.shouldLog(Log.WARN) && !_mismatchLogged && _peersByIdent.size() != _peersByRemoteHost.size()) {
            _mismatchLogged = true;
            _log.warn("Size Mismatch after add: " + peer
                       + " byIDsz = " + _peersByIdent.size()
                       + " byHostsz = " + _peersByRemoteHost.size());
        }
        
        _activeThrottle.unchoke(peer.getRemotePeer());
        markReachable(peer.getRemotePeer(), peer.isInbound());
        //_context.banlist().unbanlistRouter(peer.getRemotePeer(), STYLE);

        //if (SHOULD_FLOOD_PEERS)
        //    _flooder.addPeer(peer);
        
        _expireEvent.add(peer);
        
        _introManager.add(peer);
        
        if (oldEstablishedOn > 0)
            _context.statManager().addRateData("udp.alreadyConnected", oldEstablishedOn);
        
        synchronized(_rebuildLock) {
            rebuildIfNecessary();
            Status status = getReachabilityStatus();
            if (status != Status.OK &&
                status != Status.IPV4_OK_IPV6_UNKNOWN &&
                status != Status.IPV4_OK_IPV6_FIREWALLED &&
                status != Status.IPV4_DISABLED_IPV6_OK &&
                status != Status.IPV4_DISABLED_IPV6_UNKNOWN &&
                status != Status.IPV4_DISABLED_IPV6_FIREWALLED &&
                status != Status.DISCONNECTED &&
                _reachabilityStatusUnchanged < 7) {
                _testEvent.forceRunSoon(peer.isIPv6());
            }
        }
        return true;
    }
    
/*** infinite loop
    public RouterAddress getCurrentAddress() {
        if (needsRebuild())
            rebuildExternalAddress(false);
        return super.getCurrentAddress();
    }
***/
    
    @Override
    public void messageReceived(I2NPMessage inMsg, RouterIdentity remoteIdent, Hash remoteIdentHash, long msToReceive, int bytesReceived) {
        if (inMsg.getType() == DatabaseStoreMessage.MESSAGE_TYPE) {
            DatabaseStoreMessage dsm = (DatabaseStoreMessage)inMsg;
            DatabaseEntry entry = dsm.getEntry();
            if (entry == null)
                return;
            if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO &&
                ((RouterInfo) entry).getNetworkId() != _networkID) {
                // this is pre-0.6.1.10, so it isn't going to happen any more

                /*
                if (remoteIdentHash != null) {
                    _context.banlist().banlistRouter(remoteIdentHash, "Sent us a peer from the wrong network");
                    dropPeer(remoteIdentHash);
                    if (_log.shouldLog(Log.ERROR))
                        _log.error("Dropping the peer " + remoteIdentHash
                                   + " because they are in the wrong net");
                } else if (remoteIdent != null) {
                    _context.banlist().banlistRouter(remoteIdent.calculateHash(), "Sent us a peer from the wrong network");
                    dropPeer(remoteIdent.calculateHash());
                    if (_log.shouldLog(Log.ERROR))
                        _log.error("Dropping the peer " + remoteIdent.calculateHash()
                                   + " because they are in the wrong net");
                }
                 */
                Hash peerHash = entry.getHash();
                PeerState peer = getPeerState(peerHash);
                if (peer != null) {
                    RemoteHostId remote = peer.getRemoteHostId();
                    _dropList.add(remote);
                    _context.statManager().addRateData("udp.dropPeerDroplist", 1);
                    _context.simpleTimer2().addEvent(new RemoveDropList(remote), DROPLIST_PERIOD);
                }
                markUnreachable(peerHash);
                _context.banlist().banlistRouter(peerHash, "Part of the wrong network, version = " + ((RouterInfo) entry).getVersion());
                //_context.banlist().banlistRouter(peerHash, "Part of the wrong network", STYLE);
                if (peer != null)
                    sendDestroy(peer);
                dropPeer(peerHash, false, "wrong network");
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Dropping the peer " + peerHash + " because they are in the wrong net: " + entry);
                return;
            } else {
                if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) {
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug("Received an RI from the same net");
                } else {
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug("Received a leaseSet: " + dsm);
                }
            }
        } else {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Received another message: " + inMsg.getClass().getName());
        }
        PeerState peer = getPeerState(remoteIdentHash);
        super.messageReceived(inMsg, remoteIdent, remoteIdentHash, msToReceive, bytesReceived);
        if (peer != null)
            peer.expireInboundMessages();
    }

    private class RemoveDropList implements SimpleTimer.TimedEvent {
        private final RemoteHostId _peer;
        public RemoveDropList(RemoteHostId peer) { _peer = peer; }
        public void timeReached() { 
            _dropList.remove(_peer);
        }
    }
    
    boolean isInDropList(RemoteHostId peer) { return _dropList.contains(peer); }
    
    /**
     *  This does not send a session destroy, caller must do that if desired.
     *
     *  @param shouldBanlist doesn't really, only sets unreachable
     */
    void dropPeer(Hash peer, boolean shouldBanlist, String why) {
        PeerState state = getPeerState(peer);
        if (state != null)
            dropPeer(state, shouldBanlist, why);
    }

    /**
     *  This does not send a session destroy, caller must do that if desired.
     *
     *  @param shouldBanlist doesn't really, only sets unreachable
     */
    void dropPeer(PeerState peer, boolean shouldBanlist, String why) {
        if (_log.shouldLog(Log.INFO)) {
            long now = _context.clock().now();
            StringBuilder buf = new StringBuilder(4096);
            long timeSinceSend = now - peer.getLastSendTime();
            long timeSinceRecv = now - peer.getLastReceiveTime();
            long timeSinceAck  = now - peer.getLastACKSend();
            long timeSinceSendOK = now - peer.getLastSendFullyTime();
            int consec = peer.getConsecutiveFailedSends();
            buf.append("Dropping remote peer: ").append(peer.toString()).append(" banlist? ").append(shouldBanlist);
            buf.append(" lifetime: ").append(now - peer.getKeyEstablishedTime());
            buf.append(" time since send/fully/recv/ack: ").append(timeSinceSend).append(" / ");
            buf.append(timeSinceSendOK).append(" / ");
            buf.append(timeSinceRecv).append(" / ").append(timeSinceAck);
            buf.append(" consec failures: ").append(consec);
            if (why != null)
                buf.append(" cause: ").append(why);
            /*
            buf.append("Existing peers: \n");
            synchronized (_peersByIdent) {
                for (Iterator iter = _peersByIdent.keySet().iterator(); iter.hasNext(); ) {
                    Hash c = (Hash)iter.next();
                    PeerState p = (PeerState)_peersByIdent.get(c);
                    if (c.equals(peer.getRemotePeer())) {
                        if (p != peer) {
                            buf.append(" SAME PEER, DIFFERENT STATE ");
                        } else {
                            buf.append(" same peer, same state ");
                        }
                    } else {
                        buf.append("Peer ").append(p.toString()).append(" ");
                    }
                    
                    buf.append(" lifetime: ").append(now - p.getKeyEstablishedTime());
                    
                    timeSinceSend = now - p.getLastSendTime();
                    timeSinceRecv = now - p.getLastReceiveTime();
                    timeSinceAck  = now - p.getLastACKSend();
                    
                    buf.append(" time since send/recv/ack: ").append(timeSinceSend).append(" / ");
                    buf.append(timeSinceRecv).append(" / ").append(timeSinceAck);
                    buf.append("\n");
                }
            }
             */
            _log.info(buf.toString(), new Exception("Dropped by"));
        }
        synchronized(_addDropLock) {
            locked_dropPeer(peer, shouldBanlist, why);
        }
        rebuildIfNecessary();
    }

    /**
     *  This does not send a session destroy, caller must do that if desired.
     *
     *  @param shouldBanlist doesn't really, only sets unreachable
     */
    private void locked_dropPeer(PeerState peer, boolean shouldBanlist, String why) {
        peer.dropOutbound();
        peer.expireInboundMessages();
        _introManager.remove(peer);
        _fragments.dropPeer(peer);
        
        PeerState altByIdent = null;
        if (peer.getRemotePeer() != null) {
            dropPeerCapacities(peer);
            
            if (shouldBanlist) {
                markUnreachable(peer.getRemotePeer());
                //_context.banlist().banlistRouter(peer.getRemotePeer(), "dropped after too many retries", STYLE);
            }
            long now = _context.clock().now();
            _context.statManager().addRateData("udp.droppedPeer", now - peer.getLastReceiveTime(), now - peer.getKeyEstablishedTime());
            altByIdent = _peersByIdent.remove(peer.getRemotePeer());
        }
        
        RemoteHostId remoteId = peer.getRemoteHostId();
        PeerState altByHost = _peersByRemoteHost.remove(remoteId);

        if (altByIdent != altByHost && _log.shouldLog(Log.WARN)) 
            _log.warn("Mismatch on remove, RHID = " + remoteId
                      + " byID = " + altByIdent
                      + " byHost = " + altByHost
                      + " byIDsz = " + _peersByIdent.size()
                      + " byHostsz = " + _peersByRemoteHost.size());
        
        // unchoke 'em, but just because we'll never talk again...
        _activeThrottle.unchoke(peer.getRemotePeer());
        
        //if (SHOULD_FLOOD_PEERS)
        //    _flooder.removePeer(peer);
        _expireEvent.remove(peer);
        
        // deal with races to make sure we drop the peers fully
        if ( (altByIdent != null) && (peer != altByIdent) ) locked_dropPeer(altByIdent, shouldBanlist, "recurse");
        if ( (altByHost != null) && (peer != altByHost) ) locked_dropPeer(altByHost, shouldBanlist, "recurse");
    }
    
    /**
     *  Rebuild the IPv4 external address if required
     */
    private void rebuildIfNecessary() {
        synchronized (_rebuildLock) {
            if (locked_needsRebuild())
                rebuildExternalAddress();
        }
    }

    private boolean locked_needsRebuild() {
        if (_needsRebuild) return true; // simple enough
        if (_context.router().isHidden()) return false;
        RouterAddress addr = getCurrentAddress(false);
        if (introducersRequired()) {
            UDPAddress ua = new UDPAddress(addr);
            long now = _context.clock().now();
            int valid = 0;
            for (int i = 0; i < ua.getIntroducerCount(); i++) {
                // warning: this is only valid as long as we use the ident hash as their key.
                byte[] key = ua.getIntroducerKey(i);
                if (key.length != Hash.HASH_LENGTH)
                    continue;
                long exp = ua.getIntroducerExpiration(i);
                if (exp > 0 && exp < now + INTRODUCER_EXPIRATION_MARGIN)
                    continue;
                PeerState peer = getPeerState(new Hash(key));
                if (peer != null)
                    valid++;
            }
            long sinceSelected = now - _introducersSelectedOn;
            if (valid >= PUBLIC_RELAY_COUNT) {
                // try to shift 'em around every 10 minutes or so
                if (sinceSelected > 17*60*1000) {
                    if (_log.shouldLog(Log.WARN))
                        _log.warn("Our introducers are valid, but haven't changed in " + DataHelper.formatDuration(sinceSelected) + ", so lets rechoose");
                    return true;
                } else {
                    if (_log.shouldLog(Log.INFO))
                        _log.info("Our introducers are valid and were selected " + DataHelper.formatDuration(sinceSelected) + " ago");
                    return false;
                }
            } else if (sinceSelected > 2*60*1000) {
                // Rate limit to prevent rapid churn after transition to firewalled or at startup
                if (_log.shouldLog(Log.INFO))
                    _log.info("Need more introducers (have " +valid + " need " + PUBLIC_RELAY_COUNT + ')');
                return true;
            } else {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Need more introducers (have " +valid + " need " + PUBLIC_RELAY_COUNT + ')' +
                              " but we just chose them " + DataHelper.formatDuration(sinceSelected) + " ago so wait");
                // TODO also check to see if we actually have more available
                return false;
            }
        } else {
            byte[] externalListenHost = addr != null ? addr.getIP() : null;
            int externalListenPort = addr != null ? addr.getPort() : -1;
            boolean rv = (externalListenHost == null) || (externalListenPort <= 0);
            if (!rv) {
                // shortcut to determine if introducers are present
                if (addr.getOption("ihost0") != null)
                    rv = true;  // status == ok and we don't actually need introducers, so rebuild
            }
            if (rv) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Need to initialize our direct SSU info (" + Addresses.toString(externalListenHost, externalListenPort) + ')');
            } else if (addr.getPort() <= 0 || addr.getHost() == null) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Our direct SSU info is initialized, but not used in our address yet");
                rv = true;
            } else {
                //_log.info("Our direct SSU info is initialized");
            }
            return rv;
        }
    }
    
    /**
     * Make sure we don't think this dropped peer is capable of doing anything anymore...
     *
     */
    private void dropPeerCapacities(PeerState peer) {
        /*
        RouterInfo info = _context.netDb().lookupRouterInfoLocally(peer.getRemotePeer());
        if (info != null) {
            String capacities = info.getOptions().getProperty(UDPAddress.PROP_CAPACITY);
            if (capacities != null) {
                for (int i = 0; i < capacities.length(); i++) {
                    char capacity = capacities.charAt(i);
                    int cap = capacity - 'A';
                    if ( (cap < 0) || (cap >= _peersByCapacity.length) ) 
                        continue;
                    List peers = _peersByCapacity[cap];
                    synchronized (peers) {
                        peers.remove(peer);
                    }
                }
            }
        }
         */
    }
    
    /**
     *  This sends it directly out, bypassing OutboundMessageFragments.
     *  The only queueing is for the bandwidth limiter.
     *  BLOCKING if OB queue is full.
     */
    void send(UDPPacket packet) { 
        if (_pusher != null) {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Sending packet " + packet);
            _pusher.send(packet); 
        } else {
            _log.error("No pusher", new Exception());
        }
    }
    
    /**
     *  Send a session destroy message, bypassing OMF and PacketPusher.
     *  BLOCKING if OB queue is full.
     *
     *  @since 0.8.9
     */
    void sendDestroy(PeerState peer) {
        // peer must be fully established
        if (peer.getCurrentCipherKey() == null)
            return;
        UDPPacket pkt = _destroyBuilder.buildSessionDestroyPacket(peer);
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Sending destroy to : " + peer);
        send(pkt);
    }

    /**
     *  Send a session destroy message to everybody.
     *  BLOCKING for at least 1 sec per 1K peers, more if BW is very low or if OB queue is full.
     *
     *  @since 0.8.9
     */
    private void destroyAll() {
        for (UDPEndpoint endpoint : _endpoints) {
            endpoint.clearOutbound();
        }
        int howMany = _peersByIdent.size();
        // use no more than 1/4 of configured bandwidth
        final int burst = 8;
        int pps = Math.max(48, (_context.bandwidthLimiter().getOutboundKBytesPerSecond() * 1000 / 4) /  48);
        int burstps = pps / burst;
        // max of 1000 pps
        int toSleep = Math.max(8, (1000 / burstps));
        int count = 0;
        if (_log.shouldInfo())
            _log.info("Sending destroy to : " + howMany + " peers");
        for (PeerState peer : _peersByIdent.values()) {
            sendDestroy(peer);
            // 1000 per second * 48 bytes = 400 KBps
            if ((++count) % burst == 0) { 
                try {
                    Thread.sleep(toSleep);
                } catch (InterruptedException ie) {}
            }
        }
        toSleep = Math.min(howMany / 3, 750);
        if (toSleep > 0) {
            try {
                Thread.sleep(toSleep);
            } catch (InterruptedException ie) {}
        }
    }

    public TransportBid bid(RouterInfo toAddress, long dataSize) {
        if (dataSize > OutboundMessageState.MAX_MSG_SIZE) {
            // NTCP max is lower, so msg will get dropped
            return null;
        }
        Hash to = toAddress.getIdentity().calculateHash();
        PeerState peer = getPeerState(to);
        if (peer != null) {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("bidding on a message to an established peer: " + peer);
            if (preferUDP())
                return _cachedBid[FAST_PREFERRED_BID];
            else
                return _cachedBid[FAST_BID];
        } else {
            // If we don't have a port, all is lost
            if ( _reachabilityStatus == Status.HOSED) {
                markUnreachable(to);
                return null;
            }

            // Validate his SSU address
            RouterAddress addr = getTargetAddress(toAddress);
            if (addr == null) {
                markUnreachable(to);
                return null;
            }

            // Check for supported sig type
            SigType type = toAddress.getIdentity().getSigType();
            if (type == null || !type.isAvailable()) {
                markUnreachable(to);
                return null;
            }

            // Can we connect to them if we are not DSA?
            RouterInfo us = _context.router().getRouterInfo();
            if (us != null) {
                RouterIdentity id = us.getIdentity();
                if (id.getSigType() != SigType.DSA_SHA1) {
                    String v = toAddress.getVersion();
                    if (VersionComparator.comp(v, MIN_SIGTYPE_VERSION) < 0) {
                        markUnreachable(to);
                        return null;
                    }
                }
            }

            if (!allowConnection())
                return _cachedBid[TRANSIENT_FAIL_BID];

            if (_log.shouldLog(Log.DEBUG))
                _log.debug("bidding on a message to an unestablished peer: " + to);

            // Try to maintain at least 5 peers (30 for v6) so we can determine our IP address and
            // we have a selection to run peer tests with.
            // If we are firewalled, and we don't have enough peers that volunteered to
            // also introduce us, also bid aggressively so we are preferred over NTCP.
            // (Otherwise we only talk UDP to those that are firewalled, and we will
            // never get any introducers)
            int count = _peersByIdent.size();
            if (alwaysPreferUDP()) {
                return _cachedBid[SLOW_PREFERRED_BID];
            } else if (count < _min_peers ||
                       (_haveIPv6Address && count < _min_v6_peers) ||
                       (introducersRequired() && _introManager.introducerCount() < MIN_INTRODUCER_POOL)) {
                 // Even if we haven't hit our minimums, give NTCP a chance some of the time.
                 // This may make things work a little faster at startup
                 // (especially when we have an IPv6 address and the increased minimums),
                 // and if UDP is completely blocked we'll still have some connectivity.
                 // TODO After some time, decide that UDP is blocked/broken and return TRANSIENT_FAIL_BID?
                if (_context.random().nextInt(4) == 0)
                    return _cachedBid[SLOWEST_BID];
                else
                    return _cachedBid[SLOW_PREFERRED_BID];
            } else if (preferUDP()) {
                return _cachedBid[SLOW_BID];
            } else if (haveCapacity()) {
                if (addr.getCost() > DEFAULT_COST)
                    return _cachedBid[SLOWEST_COST_BID];
                else
                    return _cachedBid[SLOWEST_BID];
            } else {
                if (addr.getCost() > DEFAULT_COST)
                    return _cachedBid[NEAR_CAPACITY_COST_BID];
                else
                    return _cachedBid[NEAR_CAPACITY_BID];
            }
        }
    }

    /**
     *  Get first available address we can use.
     *  @return address or null
     *  @since 0.9.6
     */
    RouterAddress getTargetAddress(RouterInfo target) {
        List<RouterAddress> addrs = getTargetAddresses(target);
        for (int i = 0; i < addrs.size(); i++) {
            RouterAddress addr = addrs.get(i);
            if (addr.getOption("ihost0") == null) {
                byte[] ip = addr.getIP();
                int port = addr.getPort();
                if (ip == null || !TransportUtil.isValidPort(port) ||
                    (!isValid(ip)) ||
                    (Arrays.equals(ip, getExternalIP()) && !allowLocal())) {
                    continue;
                }
            } else {
                // introducers
                if (getIPv6Config() == IPV6_ONLY)
                    continue;
            }
            return addr;
        }
        return null;
    }

    private boolean preferUDP() {
        String pref = _context.getProperty(PROP_PREFER_UDP, DEFAULT_PREFER_UDP);
        return (pref != null) && ! "false".equals(pref);
    }
    
    private boolean alwaysPreferUDP() {
        String pref = _context.getProperty(PROP_PREFER_UDP, DEFAULT_PREFER_UDP);
        return (pref != null) && "always".equals(pref);
    }
    
    // We used to have MAX_IDLE_TIME = 5m, but this causes us to drop peers
    // and lose the old introducer tags, causing introduction fails,
    // so we keep the max time long to give the introducer keepalive code
    // in the IntroductionManager a chance to work.
    public static final int EXPIRE_TIMEOUT = 20*60*1000;
    private static final int MAX_IDLE_TIME = EXPIRE_TIMEOUT;
    public static final int MIN_EXPIRE_TIMEOUT = 165*1000;
    
    public String getStyle() { return STYLE; }

    @Override
    public void send(OutNetMessage msg) { 
        if (msg == null) return;
        if (msg.getTarget() == null) return;
        if (msg.getTarget().getIdentity() == null) return;
        if (_establisher == null) {
            failed(msg, "UDP not up yet");
            return;    
        }

        msg.timestamp("sending on UDP transport");
        Hash to = msg.getTarget().getIdentity().calculateHash();
        PeerState peer = getPeerState(to);
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Sending to " + (to != null ? to.toString() : ""));
        if (peer != null) {
            long lastSend = peer.getLastSendFullyTime();
            long lastRecv = peer.getLastReceiveTime();
            long now = _context.clock().now();
            int inboundActive = peer.expireInboundMessages();
            if ( (lastSend > 0) && (lastRecv > 0) ) {
                if ( (now - lastSend > MAX_IDLE_TIME) && 
                     (now - lastRecv > MAX_IDLE_TIME) && 
                     (peer.getConsecutiveFailedSends() > 0) &&
                     (inboundActive <= 0)) {
                    // peer is waaaay idle, drop the con and queue it up as a new con
                    dropPeer(peer, false, "proactive reconnection");
                    msg.timestamp("peer is really idle, dropping con and reestablishing");
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug("Proactive reestablish to " + to);
                    _establisher.establish(msg);
                    _context.statManager().addRateData("udp.proactiveReestablish", now-lastSend, now-peer.getKeyEstablishedTime());
                    return;
                }
            }
            msg.timestamp("enqueueing for an already established peer");
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Add to fragments for " + to);

            // See comments in DummyThrottle.java
            if (USE_PRIORITY)
                _outboundMessages.add(msg);
            else  // skip the priority queue and go straight to the active pool
                _fragments.add(msg);
        } else {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Establish new connection to " + to);
            msg.timestamp("establishing a new connection");
            _establisher.establish(msg);
        }
    }

    /**
     *  Send only if established, otherwise fail immediately.
     *  Never queue with the establisher.
     *  @since 0.9.2
     */
    void sendIfEstablished(OutNetMessage msg) { 
        _fragments.add(msg);
    }

    /**
     *  "injected" message from the EstablishmentManager.
     *  If you have multiple messages, use the list variant,
     *  so the messages may be bundled efficiently.
     *
     *  @param peer all messages MUST be going to this peer
     */
    void send(I2NPMessage msg, PeerState peer) {
        try {
            OutboundMessageState state = new OutboundMessageState(_context, msg, peer);
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Injecting a data message to a new peer: " + peer);
            _fragments.add(state, peer);
        } catch (IllegalArgumentException iae) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Shouldnt happen", new Exception("I did it"));
        }
    }

    /**
     *  "injected" message from the EstablishmentManager,
     *  plus pending messages to send,
     *  so the messages may be bundled efficiently.
     *  Called at end of outbound establishment.
     *
     *  @param msg may be null if nothing to inject
     *  @param msgs non-null, may be empty
     *  @param peer all messages MUST be going to this peer
     *  @since 0.9.24
     */
    void send(I2NPMessage msg, List<OutNetMessage> msgs, PeerState peer) {
        try {
            int sz = msgs.size();
            List<OutboundMessageState> states = new ArrayList<OutboundMessageState>(sz + 1);
            if (msg != null) {
                OutboundMessageState state = new OutboundMessageState(_context, msg, peer);
                states.add(state);
            }
            for (int i = 0; i < sz; i++) {
                OutboundMessageState state = new OutboundMessageState(_context, msgs.get(i), peer);
                states.add(state);
            }
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Injecting " + states.size() + " data messages to a new peer: " + peer);
            _fragments.add(states, peer);
        } catch (IllegalArgumentException iae) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Shouldnt happen", new Exception("I did it"));
        }
    }

    /**
     *  "injected" messages from the EstablishmentManager.
     *  Called at end of inbound establishment.
     *
     *  @param peer all messages MUST be going to this peer
     *  @since 0.9.24
     */
    void send(List<I2NPMessage> msgs, PeerState peer) {
        try {
            int sz = msgs.size();
            List<OutboundMessageState> states = new ArrayList<OutboundMessageState>(sz);
            for (int i = 0; i < sz; i++) {
                OutboundMessageState state = new OutboundMessageState(_context, msgs.get(i), peer);
                states.add(state);
            }
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Injecting " + sz + " data messages to a new peer: " + peer);
            _fragments.add(states, peer);
        } catch (IllegalArgumentException iae) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Shouldnt happen", new Exception("I did it"));
        }
    }
    
    // we don't need the following, since we have our own queueing
    protected void outboundMessageReady() { throw new UnsupportedOperationException("Not used for UDP"); }
    
    public void startListening() {
        startup();
    }
    
    public void stopListening() {
        shutdown();
        replaceAddress(null);
    }
    
    private boolean explicitAddressSpecified() {
        String h = _context.getProperty(PROP_EXTERNAL_HOST);
        // Bug in config.jsp prior to 0.7.14, sets an empty host config
        return h != null && h.length() > 0;
    }
    
    /**
     * Rebuild to get updated cost and introducers.
     * Do not tell the router (he is the one calling this)
     * @since 0.7.12
     */
    @Override
    public List<RouterAddress> updateAddress() {
        rebuildExternalAddress(false);
        return getCurrentAddresses();
    }

    /**
     *  Update our IPv4 addresses AND tell the router to rebuild and republish the router info.
     *
     *  @return the new address if changed, else null
     */
    private RouterAddress rebuildExternalAddress() {
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("REA1");
        return rebuildExternalAddress(true);
    }

    /**
     *  Update our IPv4 address and optionally tell the router to rebuild and republish the router info.
     *
     *  @param allowRebuildRouterInfo whether to tell the router
     *  @return the new address if changed, else null
     */
    private RouterAddress rebuildExternalAddress(boolean allowRebuildRouterInfo) {
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("REA2 " + allowRebuildRouterInfo);
        // if the external port is specified, we want to use that to bind to even
        // if we don't know the external host.
        int port = _context.getProperty(PROP_EXTERNAL_PORT, -1);
        
        byte[] ip = null;
        String host = null;
        if (explicitAddressSpecified()) {
            host = _context.getProperty(PROP_EXTERNAL_HOST);
            if (host != null) {
                String[] hosts = DataHelper.split(host, "[,; \r\n\t]");
                RouterAddress rv = null;
                for (int i = 0; i < hosts.length; i++) {
                    String h = hosts[i];
                    if (h.length() <= 0)
                        continue;
                    rv = rebuildExternalAddress(h, port, allowRebuildRouterInfo);
                }
                return rv;
            }
        } else {
            if (!introducersRequired()) {
                RouterAddress cur = getCurrentExternalAddress(false);
                if (cur != null)
                    host = cur.getHost();
            }
        }
        return rebuildExternalAddress(host, port, allowRebuildRouterInfo);
    }
            
    /**
     *  Update our IPv4 or IPv6 address and optionally tell the router to rebuild and republish the router info.
     *
     *  @param ip new ip valid IPv4 or IPv6 or null
     *  @param port new valid port or -1
     *  @param allowRebuildRouterInfo whether to tell the router
     *  @return the new address if changed, else null
     *  @since IPv6
     */
    private RouterAddress rebuildExternalAddress(byte[] ip, int port, boolean allowRebuildRouterInfo) {
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("REA3 " + Addresses.toString(ip, port));
        if (ip == null)
            return rebuildExternalAddress((String) null, port, allowRebuildRouterInfo);
        if (isValid(ip))
            return rebuildExternalAddress(Addresses.toString(ip), port, allowRebuildRouterInfo);
        return null;
    }

    /**
     *  Update our IPv4 or IPv6 address and optionally tell the router to rebuild and republish the router info.
     *  FIXME no way to remove an IPv6 address
     *
     *  @param host new validated IPv4 or IPv6 or DNS hostname or null
     *  @param port new validated port or 0/-1
     *  @param allowRebuildRouterInfo whether to tell the router
     *  @return the new address if changed, else null
     *  @since IPv6
     */
    private RouterAddress rebuildExternalAddress(String host, int port, boolean allowRebuildRouterInfo) {
        synchronized (_rebuildLock) {
            return locked_rebuildExternalAddress(host, port, allowRebuildRouterInfo);
        }
    }

    private RouterAddress locked_rebuildExternalAddress(String host, int port, boolean allowRebuildRouterInfo) {
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("REA4 " + host + ':' + port);
        if (_context.router().isHidden())
            return null;
        
        OrderedProperties options = new OrderedProperties(); 
        boolean directIncluded;
        // DNS name assumed IPv4
        boolean isIPv6 = host != null && host.contains(":");
        boolean introducersRequired = (!isIPv6) && introducersRequired();
        if (!introducersRequired && allowDirectUDP() && port > 0 && host != null) {
            options.setProperty(UDPAddress.PROP_PORT, String.valueOf(port));
            options.setProperty(UDPAddress.PROP_HOST, host);
            directIncluded = true;
        } else {
            directIncluded = false;
        }

        boolean introducersIncluded = false;
        if (introducersRequired) {
            // intro manager now sorts introducers, so
            // deepEquals() below will not fail even with same introducers.
            // Was only a problem when we had very very few peers to pick from.
            RouterAddress current = getCurrentAddress(isIPv6);
            int found = _introManager.pickInbound(current, options, PUBLIC_RELAY_COUNT);
            if (found > 0) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Direct? " + directIncluded + " reqd? " + introducersRequired +
                              " picked introducers: " + found);
                _introducersSelectedOn = _context.clock().now();
                introducersIncluded = true;
            } else {
                if (_log.shouldLog(Log.WARN))
                    _log.warn("Direct? " + directIncluded + " reqd? " + introducersRequired +
                              " no introducers");
            }
        }
        
        // if we have explicit external addresses, they had better be reachable
        if (introducersRequired)
            options.setProperty(UDPAddress.PROP_CAPACITY, CAP_TESTING);
        else
            options.setProperty(UDPAddress.PROP_CAPACITY, CAP_TESTING_INTRO);

        // MTU since 0.9.2
        int mtu;
        if (host == null) {
            mtu = _mtu;
        } else {
            try {
                InetAddress ia = InetAddress.getByName(host);
                mtu = setMTU(ia);
            } catch (UnknownHostException uhe) {
                mtu = _mtu;
            }
        }
        if (mtu != PeerState.LARGE_MTU)
            options.setProperty(UDPAddress.PROP_MTU, Integer.toString(mtu));

        if (directIncluded || introducersIncluded) {
            // This is called via TransportManager.configTransports() before startup(), prevent NPE
            // Note that peers won't connect to us without this - see EstablishmentManager
            if (_introKey != null)
                options.setProperty(UDPAddress.PROP_INTRO_KEY, _introKey.toBase64());

            // SSU seems to regulate at about 85%, so make it a little higher.
            // If this is too low, both NTCP and SSU always have incremented cost and
            // the whole mechanism is not helpful.
            int cost = DEFAULT_COST;
            if (ADJUST_COST && !haveCapacity(91))
                cost += CONGESTION_COST_ADJUSTMENT;
            if (introducersIncluded)
                cost += 2;
            if (isIPv6) {
                TransportUtil.IPv6Config config = getIPv6Config();
                if (config == IPV6_PREFERRED)
                    cost--;
                else if (config == IPV6_NOT_PREFERRED)
                    cost++;
            }
            RouterAddress addr = new RouterAddress(STYLE, options, cost);

            RouterAddress current = getCurrentAddress(isIPv6);
            boolean wantsRebuild = !addr.deepEquals(current);

            // save the external address, even if we didn't publish it
            if (port > 0 && host != null) {
                RouterAddress local;
                if (directIncluded) {
                    local = addr;
                } else {
                    OrderedProperties localOpts = new OrderedProperties(); 
                    localOpts.setProperty(UDPAddress.PROP_PORT, String.valueOf(port));
                    localOpts.setProperty(UDPAddress.PROP_HOST, host);
                    local = new RouterAddress(STYLE, localOpts, cost);
                }
                replaceCurrentExternalAddress(local, isIPv6);
            }

            if (wantsRebuild) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Address rebuilt: " + addr, new Exception());
                replaceAddress(addr);
                // warning, this calls back into us with allowRebuildRouterInfo = false,
                // via CSFI.createAddresses->TM.getAddresses()->updateAddress()->REA
                if (allowRebuildRouterInfo)
                    _context.router().rebuildRouterInfo();
            } else {
                addr = null;
            }
        
            if (!isIPv6)
                _needsRebuild = false;
            return addr;
        } else {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Wanted to rebuild my SSU address, but couldn't specify either the direct or indirect info (needs introducers? " 
                           + introducersRequired + ")", new Exception("source"));
            _needsRebuild = true;
            // save the external address, even if we didn't publish it
            if (port > 0 && host != null) {
                OrderedProperties localOpts = new OrderedProperties(); 
                localOpts.setProperty(UDPAddress.PROP_PORT, String.valueOf(port));
                localOpts.setProperty(UDPAddress.PROP_HOST, host);
                RouterAddress local = new RouterAddress(STYLE, localOpts, DEFAULT_COST);
                replaceCurrentExternalAddress(local, isIPv6);
            }
            if (hasCurrentAddress()) {
                // We must remove current address, otherwise the user will see
                // "firewalled with inbound NTCP enabled" warning in console.
                // Remove the IPv4 address only
                removeAddress(false);
                // warning, this calls back into us with allowRebuildRouterInfo = false,
                // via CSFI.createAddresses->TM.getAddresses()->updateAddress()->REA
                if (allowRebuildRouterInfo)
                    _context.router().rebuildRouterInfo();
            }
            return null;
        }
    }

    /**
     *  Simple storage of IP and port, since
     *  we don't put them in the real, published RouterAddress anymore
     *  if we are firewalled.
     *
     *  Caller must sync on _rebuildLock
     *
     *  @since 0.9.18
     */
    private void replaceCurrentExternalAddress(RouterAddress ra, boolean isIPv6) {
        if (isIPv6)
            _currentOurV6Address = ra;
        else
            _currentOurV4Address = ra;
    }

    /**
     *  Simple fetch of stored IP and port, since
     *  we don't put them in the real, published RouterAddress anymore
     *  if we are firewalled.
     *
     *  @since 0.9.18
     */
    private RouterAddress getCurrentExternalAddress(boolean isIPv6) {
        // deadlock thru here ticket #1699
        synchronized (_rebuildLock) {
            return isIPv6 ? _currentOurV6Address : _currentOurV4Address;
        }
    }

    /**
     *  Replace then tell NTCP that we changed.
     *
     *  @param address the new address or null to remove all
     */
    @Override
    protected void replaceAddress(RouterAddress address) {
        super.replaceAddress(address);
        _context.commSystem().notifyReplaceAddress(address);
    }

    /**
     *  Remove then tell NTCP that we changed.
     *
     *  @since 0.9.20
     */
    @Override
    protected void removeAddress(RouterAddress address) {
        super.removeAddress(address);
        _context.commSystem().notifyRemoveAddress(address);
    }

    /**
     *  Remove then tell NTCP that we changed.
     *
     *  @since 0.9.20
     */
    @Override
    protected void removeAddress(boolean ipv6) {
        super.removeAddress(ipv6);
        if (ipv6)
            _lastInboundIPv6 = 0;
        _context.commSystem().notifyRemoveAddress(ipv6);
    }

    /**
     *  Calls replaceAddress(address), then shuts down the router if
     *  dynamic keys is enabled, which it never is, so all this is unused.
     *
     *  @param address the new address or null to remove all
     */
/****
    protected void replaceAddress(RouterAddress address, RouterAddress oldAddress) {
        replaceAddress(address);
        if (oldAddress != null) {
            UDPAddress old = new UDPAddress(oldAddress);
            InetAddress oldHost = old.getHostAddress();
            UDPAddress newAddr = new UDPAddress(address);
            InetAddress newHost = newAddr.getHostAddress();
            if ( (old.getPort() > 0) && (oldHost != null) && (isValid(oldHost.getAddress())) &&
                 (newAddr.getPort() > 0) && (newHost != null) && (isValid(newHost.getAddress())) ) {
                if ( (old.getPort() != newAddr.getPort()) || (!oldHost.equals(newHost)) ) {
                    // substantial data has changed, so if we are in 'dynamic keys' mode, restart the 
                    // router hard and regenerate a new identity
                    if (_context.getBooleanProperty(Router.PROP_DYNAMIC_KEYS)) {
                        if (_log.shouldLog(Log.ERROR))
                            _log.error("SSU address updated. new address: " 
                                       + newAddr.getHostAddress() + ":" + newAddr.getPort() + ", old address: " 
                                       + old.getHostAddress() + ":" + old.getPort());
                        // shutdown itself checks the DYNAMIC_KEYS flag, and if its set to true, deletes
                        // the keys
                        _context.router().shutdown(Router.EXIT_HARD_RESTART);              
                    }
                }
            }
        }
    }
****/
    
    /**
     *  Do we require introducers?
     */
    public boolean introducersRequired() {
        /******************
         *  Don't do this anymore, as we are removing the checkbox from the UI,
         *  and we rarely if ever see the problem of false negatives for firewall detection -
         *  it's usually false positives.
         ******************
        String forceIntroducers = _context.getProperty(PROP_FORCE_INTRODUCERS);
        if ( (forceIntroducers != null) && (Boolean.parseBoolean(forceIntroducers)) ) {
            if (_log.shouldLog(Log.INFO))
                _log.info("Force introducers specified");
            return true;
        }
        *******************/
        Status status = getReachabilityStatus();
        switch (status) {
            case REJECT_UNSOLICITED:
            case DIFFERENT:
            case IPV4_FIREWALLED_IPV6_OK:
            case IPV4_FIREWALLED_IPV6_UNKNOWN:
                if (_log.shouldLog(Log.DEBUG))
                    _log.debug("Require introducers, because our status is " + status);
                return true;

            default:
                if (!allowDirectUDP()) {
                    if (_log.shouldLog(Log.DEBUG))
                        _log.debug("Require introducers, because we do not allow direct UDP connections");
                    return true;
                }
                return false;
        }
    }
    
    /**
     *  MIGHT we require introducers?
     *  This is like introducersRequired, but if we aren't sure, this returns true.
     *  Used only by EstablishmentManager.
     *
     *  @since 0.9.24
     */
    boolean introducersMaybeRequired() {
        Status status = getReachabilityStatus();
        switch (status) {
            case REJECT_UNSOLICITED:
            case DIFFERENT:
            case IPV4_FIREWALLED_IPV6_OK:
            case IPV4_FIREWALLED_IPV6_UNKNOWN:
            case IPV4_UNKNOWN_IPV6_OK:
            case IPV4_UNKNOWN_IPV6_FIREWALLED:
            case UNKNOWN:
                return true;

            default:
                return !allowDirectUDP();
        }
    }
    
    /**
     *  For EstablishmentManager
     *  @since 0.9.3
     */
    boolean canIntroduce() {
        // we don't expect inbound connections when hidden, but it could happen
        // Don't offer if we are approaching max connections. While Relay Intros do not
        // count as connections, we have to keep the connection to this peer up longer if
        // we are offering introductions.
        return
            (!_context.router().isHidden()) &&
            (!introducersRequired()) &&
            haveCapacity() &&
            (!_context.netDb().floodfillEnabled()) &&
            _introManager.introducedCount() < IntroductionManager.MAX_OUTBOUND &&
            _introManager.introducedCount() < getMaxConnections() / 4;
    }

    /** default true */
    private boolean allowDirectUDP() {
        return _context.getBooleanPropertyDefaultTrue(PROP_ALLOW_DIRECT);
    }

    String getPacketHandlerStatus() {
        PacketHandler handler = _handler;
        if (handler != null)
            return handler.getHandlerStatus();
        else
            return "";
    }

    /** @since IPv6 */
    PacketHandler getPacketHandler() {
        return _handler;
    }

    public void failed(OutboundMessageState msg) { failed(msg, true); }

    void failed(OutboundMessageState msg, boolean allowPeerFailure) {
        if (msg == null) return;
        OutNetMessage m = msg.getMessage();
        if ( allowPeerFailure && (msg.getPeer() != null) && 
             ( (msg.getMaxSends() >= OutboundMessageFragments.MAX_VOLLEYS) ||
               (msg.isExpired())) ) {
            //long recvDelay = _context.clock().now() - msg.getPeer().getLastReceiveTime();
            //long sendDelay = _context.clock().now() - msg.getPeer().getLastSendFullyTime();
            //if (m != null)
            //    m.timestamp("message failure - volleys = " + msg.getMaxSends() 
            //                + " lastReceived: " + recvDelay
            //                + " lastSentFully: " + sendDelay
            //                + " expired? " + msg.isExpired());
            int consecutive = msg.getPeer().incrementConsecutiveFailedSends();
            if (_log.shouldLog(Log.INFO))
                _log.info("Consecutive failure #" + consecutive 
                          + " on " + msg.toString()
                          + " to " + msg.getPeer());
            if ( (_context.clock().now() - msg.getPeer().getLastSendFullyTime() <= 60*1000) || (consecutive < MAX_CONSECUTIVE_FAILED) ) {
                // ok, a few conseutive failures, but we /are/ getting through to them
            } else {
                _context.statManager().addRateData("udp.dropPeerConsecutiveFailures", consecutive, msg.getPeer().getInactivityTime());
                sendDestroy(msg.getPeer());
                dropPeer(msg.getPeer(), false, "too many failures");
            }
            //if ( (consecutive > MAX_CONSECUTIVE_FAILED) && (msg.getPeer().getInactivityTime() > DROP_INACTIVITY_TIME))
            //    dropPeer(msg.getPeer(), false);
            //else if (consecutive > 2 * MAX_CONSECUTIVE_FAILED) // they're sending us data, but we cant reply?
            //    dropPeer(msg.getPeer(), false);
        } else {
            if (_log.shouldLog(Log.DEBUG))
                _log.debug("Failed sending " + msg + " to " + msg.getPeer());
        }
        noteSend(msg, false);
        if (m != null)
            super.afterSend(m, false);
    }
    
    private void noteSend(OutboundMessageState msg, boolean successful) {
        // bail before we do all the work
        if (!_context.messageHistory().getDoLog())
            return;
        int pushCount = msg.getPushCount();
        int sends = msg.getMaxSends();
        boolean expired = msg.isExpired();
     
        OutNetMessage m = msg.getMessage();
        PeerState p = msg.getPeer();
        StringBuilder buf = new StringBuilder(64);
        buf.append(" lifetime: ").append(msg.getLifetime());
        buf.append(" sends: ").append(sends);
        buf.append(" pushes: ").append(pushCount);
        buf.append(" expired? ").append(expired);
        buf.append(" unacked: ").append(msg.getUnackedSize());
        if ( (p != null) && (!successful) ) {
            buf.append(" consec_failed: ").append(p.getConsecutiveFailedSends());
            long timeSinceSend = _context.clock().now() - p.getLastSendFullyTime();
            buf.append(" lastFullSend: ").append(timeSinceSend);
            long timeSinceRecv = _context.clock().now() - p.getLastReceiveTime();
            buf.append(" lastRecv: ").append(timeSinceRecv);
            buf.append(" xfer: ").append(p.getSendBps()).append("/").append(p.getReceiveBps());
            buf.append(" mtu: ").append(p.getMTU());
            buf.append(" rto: ").append(p.getRTO());
            buf.append(" sent: ").append(p.getMessagesSent()).append("/").append(p.getPacketsTransmitted());
            buf.append(" recv: ").append(p.getMessagesReceived()).append("/").append(p.getPacketsReceived());
            buf.append(" uptime: ").append(_context.clock().now()-p.getKeyEstablishedTime());
        }
        if ( (m != null) && (p != null) ) {
            _context.messageHistory().sendMessage(m.getMessageType(), msg.getMessageId(), m.getExpiration(), 
                                                  p.getRemotePeer(), successful, buf.toString());
        } else {
            _context.messageHistory().sendMessage("establish", msg.getMessageId(), -1, 
                                                  (p != null ? p.getRemotePeer() : null), successful, buf.toString());
        }
    }
    
    public void failed(OutNetMessage msg, String reason) {
        if (msg == null) return;
        if (_log.shouldLog(Log.INFO))
            _log.info("Send failed: " + reason + " msg: " + msg, new Exception("failed from"));
        
        if (_context.messageHistory().getDoLog())
            _context.messageHistory().sendMessage(msg.getMessageType(), msg.getMessageId(), msg.getExpiration(), 
                                              msg.getTarget().getIdentity().calculateHash(), false, reason);
        super.afterSend(msg, false);
    }

    public void succeeded(OutboundMessageState msg) {
        if (msg == null) return;
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("Sending message succeeded: " + msg);
        noteSend(msg, true);
        OutNetMessage m = msg.getMessage();
        if (m != null)
            super.afterSend(m, true);
    }

    public int countPeers() {
            return _peersByIdent.size();
    }

    public int countActivePeers() {
        long old = _context.clock().now() - 5*60*1000;
        int active = 0;
        for (PeerState peer : _peersByIdent.values()) {
            // PeerState initializes times at construction,
            // so check message count also
            if ((peer.getMessagesReceived() > 0 && peer.getLastReceiveTime() >= old) ||
                (peer.getMessagesSent() > 0 && peer.getLastSendTime() >= old)) {
                active++;
            }
        }
        return active;
    }
    
    public int countActiveSendPeers() {
        long old = _context.clock().now() - 60*1000;
        int active = 0;
        for (PeerState peer : _peersByIdent.values()) {
                if (peer.getLastSendFullyTime() >= old)
                    active++;
            }
        return active;
    }
    
    @Override
    public boolean isEstablished(Hash dest) {
        return getPeerState(dest) != null;
    }

    /**
     *  @since 0.9.3
     */
    @Override
    public boolean isBacklogged(Hash dest) {
        PeerState peer =  _peersByIdent.get(dest);
        return peer != null && peer.isBacklogged();
    }

    /**
     * Tell the transport that we may disconnect from this peer.
     * This is advisory only.
     *
     * @since 0.9.24
     */
    @Override
    public void mayDisconnect(final Hash peer) {
        final PeerState ps =  _peersByIdent.get(peer);
        if (ps != null &&
            ps.getWeRelayToThemAs() <= 0 &&
            (ps.getTheyRelayToUsAs() <= 0 || ps.getIntroducerTime() < _context.clock().now() - 2*60*60*1000) &&
            ps.getMessagesReceived() <= 2 && ps.getMessagesSent() <= 2) {
            ps.setMayDisconnect();
        }
    }

    public boolean allowConnection() {
            return _peersByIdent.size() < getMaxConnections();
    }

    /**
     * Return our peer clock skews on this transport.
     * Vector composed of Long, each element representing a peer skew in seconds.
     * A positive number means our clock is ahead of theirs.
     */
    @Override
    public Vector<Long> getClockSkews() {
        Vector<Long> skews = new Vector<Long>();

        // If our clock is way off, we may not have many (or any) successful connections,
        // so try hard in that case to return good data
        boolean includeEverybody = _context.router().getUptime() < 10*60*1000 || _peersByIdent.size() < 10;
        long now = _context.clock().now();
        for (PeerState peer : _peersByIdent.values()) {
            if ((!includeEverybody) && now - peer.getLastReceiveTime() > 5*60*1000)
                continue; // skip old peers
            if (peer.getRTT() > PeerState.INIT_RTT - 250)
                continue; // Big RTT makes for a poor calculation
            skews.addElement(Long.valueOf(peer.getClockSkew() / 1000));
        }
        if (_log.shouldLog(Log.DEBUG))
            _log.debug("UDP transport returning " + skews.size() + " peer clock skews.");
        return skews;
    }
    
    /**
     *  @return a new DHSessionKeyBuilder
     *  @since 0.9
     */
    DHSessionKeyBuilder getDHBuilder() {
        return _dhFactory.getBuilder();
    }
    
    /**
     *  @return the factory
     *  @since 0.9.2
     */
    DHSessionKeyBuilder.Factory getDHFactory() {
        return _dhFactory;
    }
    
    @Override
    public void renderStatusHTML(Writer out, String urlBase, int sortFlags) throws IOException {
        TreeSet<PeerState> peers = new TreeSet<PeerState>(getComparator(sortFlags));
        peers.addAll(_peersByIdent.values());
        long offsetTotal = 0;

        int bpsIn = 0;
        int bpsOut = 0;
        long uptimeMsTotal = 0;
        long cwinTotal = 0;
        long rttTotal = 0;
        long rtoTotal = 0;
        long sendTotal = 0;
        long recvTotal = 0;
        long resentTotal = 0;
        long dupRecvTotal = 0;
        int numPeers = 0;
        
        StringBuilder buf = new StringBuilder(512);
        buf.append("<h3 id=\"udpcon\">").append(_t("UDP connections")).append(": ").append(peers.size());
        buf.append(". ").append(_t("Limit")).append(": ").append(getMaxConnections());
        buf.append(". ").append(_t("Timeout")).append(": ").append(DataHelper.formatDuration2(_expireTimeout));
        if (_context.getBooleanProperty(PROP_ADVANCED)) {
            buf.append(". ").append(_t("Status")).append(": ").append(_t(_reachabilityStatus.toStatusString()));
        }
        buf.append(".</h3>\n");
        buf.append("<table>\n");
        buf.append("<tr><th class=\"smallhead\" nowrap><a href=\"#def.peer\">").append(_t("Peer")).append("</a><br>");
        if (sortFlags != FLAG_ALPHA)
            appendSortLinks(buf, urlBase, sortFlags, _t("Sort by peer hash"), FLAG_ALPHA);
        buf.append("</th><th class=\"smallhead\" nowrap><a href=\"#def.dir\" title=\"")
           .append(_t("Direction/Introduction")).append("\">").append(_t("Dir"))
           .append("</a></th><th class=\"smallhead\" nowrap><a href=\"#def.ipv6\">").append(_t("IPv6"))
           .append("</a></th><th class=\"smallhead\" nowrap><a href=\"#def.idle\">").append(_t("Idle")).append("</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by idle inbound"), FLAG_IDLE_IN);
        buf.append(" / ");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by idle outbound"), FLAG_IDLE_OUT);
        buf.append("</th>");
        buf.append("<th class=\"smallhead\" nowrap><a href=\"#def.rate\">").append(_t("In/Out")).append("</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by inbound rate"), FLAG_RATE_IN);
        buf.append(" / ");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by outbound rate"), FLAG_RATE_OUT);
        buf.append("</th>\n");
        buf.append("<th class=\"smallhead\" nowrap><a href=\"#def.up\">").append(_t("Up")).append("</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by connection uptime"), FLAG_UPTIME);
        buf.append("</th><th class=\"smallhead\" nowrap><a href=\"#def.skew\">").append(_t("Skew")).append("</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by clock skew"), FLAG_SKEW);
        buf.append("</th>\n");
        buf.append("<th class=\"smallhead\" nowrap><a href=\"#def.cwnd\">CWND</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by congestion window"), FLAG_CWND);
        buf.append("</th><th class=\"smallhead\" nowrap><a href=\"#def.ssthresh\">SST</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by slow start threshold"), FLAG_SSTHRESH);
        buf.append("</th>\n");
        buf.append("<th class=\"smallhead\" nowrap><a href=\"#def.rtt\">RTT</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by round trip time"), FLAG_RTT);
        //buf.append("</th><th class=\"smallhead\" nowrap><a href=\"#def.dev\">").append(_t("Dev")).append("</a><br>");
        //appendSortLinks(buf, urlBase, sortFlags, _t("Sort by round trip time deviation"), FLAG_DEV);
        buf.append("</th><th class=\"smallhead\" nowrap><a href=\"#def.rto\">RTO</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by retransmission timeout"), FLAG_RTO);
        buf.append("</th>\n");
        buf.append("<th class=\"smallhead\" nowrap><a href=\"#def.mtu\">MTU</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by outbound maximum transmit unit"), FLAG_MTU);
        buf.append("</th><th class=\"smallhead\" nowrap><a href=\"#def.send\">").append(_t("TX")).append("</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by packets sent"), FLAG_SEND);
        buf.append("</th><th class=\"smallhead\" nowrap><a href=\"#def.recv\">").append(_t("RX")).append("</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by packets received"), FLAG_RECV);
        buf.append("</th>\n");
        buf.append("<th class=\"smallhead\" nowrap><a href=\"#def.resent\">").append(_t("Dup TX")).append("</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by packets retransmitted"), FLAG_RESEND);
        buf.append("</th><th class=\"smallhead\" nowrap><a href=\"#def.dupRecv\">").append(_t("Dup RX")).append("</a><br>");
        appendSortLinks(buf, urlBase, sortFlags, _t("Sort by packets received more than once"), FLAG_DUP);
        buf.append("</th></tr>\n");
        out.write(buf.toString());
        buf.setLength(0);
        long now = _context.clock().now();
        for (PeerState peer : peers) {
            if (now-peer.getLastReceiveTime() > 60*60*1000)
                continue; // don't include old peers
            
            buf.append("<tr><td class=\"cells\" align=\"left\" nowrap>");
            buf.append(_context.commSystem().renderPeerHTML(peer.getRemotePeer()));
            //byte ip[] = peer.getRemoteIP();
            //if (ip != null)
            //    buf.append(' ').append(_context.blocklist().toStr(ip));
            buf.append("</td><td class=\"cells\" nowrap align=\"left\">");
            if (peer.isInbound())
                buf.append("<img src=\"/themes/console/images/inbound.png\" alt=\"Inbound\" title=\"").append(_t("Inbound")).append("\">");
            else
                buf.append("<img src=\"/themes/console/images/outbound.png\" alt=\"Outbound\" title=\"").append(_t("Outbound")).append("\">");
            if (peer.getWeRelayToThemAs() > 0)
                buf.append("&nbsp;&nbsp;<img src=\"/themes/console/images/outbound.png\" height=\"8\" width=\"12\" alt=\"^\" title=\"").append(_t("We offered to introduce them")).append("\">");
            if (peer.getTheyRelayToUsAs() > 0)
                buf.append("&nbsp;&nbsp;<img src=\"/themes/console/images/inbound.png\" height=\"8\" width=\"12\" alt=\"V\" title=\"").append(_t("They offered to introduce us")).append("\">");
            
            boolean appended = false;
            if (_activeThrottle.isChoked(peer.getRemotePeer())) {
                buf.append("<br><i>").append(_t("Choked")).append("</i>");
                appended = true;
            }
            int cfs = peer.getConsecutiveFailedSends();
            if (cfs > 0) {
                if (!appended) buf.append("<br>");
                buf.append(" <i>");
                if (cfs == 1)
                    buf.append(_t("1 fail"));
                else
                    buf.append(_t("{0} fails", cfs));
                buf.append("</i>");
                appended = true;
            }
            if (_context.banlist().isBanlisted(peer.getRemotePeer(), STYLE)) {
                if (!appended) buf.append("<br>");
                buf.append(" <i>").append(_t("Banned")).append("</i>");
                appended = true;
            }
            //byte[] ip = getIP(peer.getRemotePeer());
            //if (ip != null)
            //    buf.append(' ').append(_context.blocklist().toStr(ip));
            buf.append("</td>");

            buf.append("<td class=\"cells\" align=\"center\">");
            if (peer.isIPv6())
                buf.append("&#x2713;");
            else
                buf.append("&nbsp;");
            buf.append("</td>");
            
            long idleIn = Math.max(now-peer.getLastReceiveTime(), 0);
            long idleOut = Math.max(now-peer.getLastSendTime(), 0);
            
            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(DataHelper.formatDuration2(idleIn));
            buf.append(THINSP);
            buf.append(DataHelper.formatDuration2(idleOut));
            buf.append("</td>");
 
            int recvBps = (idleIn > 15*1000 ? 0 : peer.getReceiveBps());
            int sendBps = (idleOut > 15*1000 ? 0 : peer.getSendBps());
            
            buf.append("<td class=\"cells\" align=\"right\" nowrap>");
            buf.append(formatKBps(recvBps));
            buf.append(THINSP);
            buf.append(formatKBps(sendBps));
            //buf.append(" K/s");
            //buf.append(formatKBps(peer.getReceiveACKBps()));
            //buf.append("K/s/");
            //buf.append(formatKBps(peer.getSendACKBps()));
            //buf.append("K/s ");
            buf.append("</td>");

            long uptime = now - peer.getKeyEstablishedTime();
            
            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(DataHelper.formatDuration2(uptime));
            buf.append("</td>");
            
            buf.append("<td class=\"cells\" align=\"right\">");
            long skew = peer.getClockSkew();
            buf.append(DataHelper.formatDuration2(skew));
            buf.append("</td>");
            offsetTotal = offsetTotal + skew;

            long sendWindow = peer.getSendWindowBytes();
            
            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(sendWindow/1024);
            buf.append("K");
            buf.append(THINSP).append(peer.getConcurrentSends());
            buf.append(THINSP).append(peer.getConcurrentSendWindow());
            buf.append(THINSP).append(peer.getConsecutiveSendRejections());
            if (peer.isBacklogged())
                buf.append(' ').append(_t("backlogged"));
            buf.append("</td>");

            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(peer.getSlowStartThreshold()/1024);
            buf.append("K</td>");

            int rtt = peer.getRTT();
            int rto = peer.getRTO();
            
            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(DataHelper.formatDuration2(rtt));
            buf.append("</td>");
            
            //buf.append("<td class=\"cells\" align=\"right\">");
            //buf.append(DataHelper.formatDuration2(peer.getRTTDeviation()));
            //buf.append("</td>");

            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(DataHelper.formatDuration2(rto));
            buf.append("</td>");
            
            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(peer.getMTU()).append(THINSP).append(peer.getReceiveMTU());
            
            //.append('/');
            //buf.append(peer.getMTUIncreases()).append('/');
            //buf.append(peer.getMTUDecreases());
            buf.append("</td>");
        
            long sent = peer.getMessagesSent();
            long recv = peer.getMessagesReceived();
            
            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(sent);
            buf.append("</td>");
            
            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(recv);
            buf.append("</td>");
            
            //double sent = (double)peer.getPacketsPeriodTransmitted();
            //double sendLostPct = 0;
            //if (sent > 0)
            //    sendLostPct = (double)peer.getPacketsRetransmitted()/(sent);
            
            long resent = peer.getPacketsRetransmitted();
            long dupRecv = peer.getPacketsReceivedDuplicate();
            
            buf.append("<td class=\"cells\" align=\"right\">");
            //buf.append(formatPct(sendLostPct));
            buf.append(resent); // + "/" + peer.getPacketsPeriodRetransmitted() + "/" + sent);
            //buf.append(peer.getPacketRetransmissionRate());
            buf.append("</td>");
            
            buf.append("<td class=\"cells\" align=\"right\">");
            buf.append(dupRecv); //formatPct(recvDupPct));
            buf.append("</td>");

            buf.append("</tr>\n");
            out.write(buf.toString());
            buf.setLength(0);
            
            bpsIn += recvBps;
            bpsOut += sendBps;
        
            uptimeMsTotal += uptime;
            cwinTotal += sendWindow;
            rttTotal += rtt;
            rtoTotal += rto;
        
            sendTotal += sent;
            recvTotal += recv;
            resentTotal += resent;
            dupRecvTotal += dupRecv;
            
            numPeers++;
        }
        
      if (numPeers > 0) {
//        buf.append("<tr><td colspan=\"16\"><hr></td></tr>\n");
        buf.append("<tr class=\"tablefooter\"><td colspan=\"4\" align=\"left\"><b>")
           .append(ngettext("{0} peer", "{0} peers", peers.size()))
           .append("</b></td>" +
                   "<td align=\"center\" nowrap><b>");
        buf.append(formatKBps(bpsIn)).append(THINSP).append(formatKBps(bpsOut));
        long x = uptimeMsTotal/numPeers;
        buf.append("</b></td>" +
                   "<td align=\"center\"><b>").append(DataHelper.formatDuration2(x));
        x = offsetTotal/numPeers;
        buf.append("</b></td><td align=\"center\"><b>").append(DataHelper.formatDuration2(x)).append("</b></td>\n" +
                   "<td align=\"center\"><b>");
        buf.append(cwinTotal/(numPeers*1024) + "K");
        buf.append("</b></td><td>&nbsp;</td>\n" +
                   "<td align=\"center\"><b>");
        buf.append(DataHelper.formatDuration2(rttTotal/numPeers));
        //buf.append("</b></td><td>&nbsp;</td><td align=\"center\"><b>");
        buf.append("</b></td><td align=\"center\"><b>");
        buf.append(DataHelper.formatDuration2(rtoTotal/numPeers));
        buf.append("</b></td><td align=\"center\"><b>").append(_mtu).append("</b></td><td align=\"center\"><b>");
        buf.append(sendTotal).append("</b></td><td align=\"center\"><b>").append(recvTotal).append("</b></td>\n" +
                   "<td align=\"center\"><b>").append(resentTotal);
        buf.append("</b></td><td align=\"center\"><b>").append(dupRecvTotal).append("</b></td></tr>\n");
        if (sortFlags == FLAG_DEBUG) {
            buf.append("<tr><td colspan=\"16\">");
            buf.append("peersByIdent: ").append(_peersByIdent.size());
            buf.append(" peersByRemoteHost: ").append(_peersByRemoteHost.size());
            int dir = 0;
            int indir = 0;
            for (RemoteHostId rhi : _peersByRemoteHost.keySet()) {
                 if (rhi.getIP() != null)
                     dir++;
                 else
                     indir++;
            }
            buf.append(" pBRH direct: ").append(dir).append(" indirect: ").append(indir);
            buf.append("</td></tr>");
        }
     }  // numPeers > 0
        buf.append("</table>\n");

      /*****
        long bytesTransmitted = _context.bandwidthLimiter().getTotalAllocatedOutboundBytes();
        // NPE here early
        double averagePacketSize = _context.statManager().getRate("udp.sendPacketSize").getLifetimeAverageValue();
        // lifetime value, not just the retransmitted packets of current connections
        resentTotal = (long)_context.statManager().getRate("udp.packetsRetransmitted").getLifetimeEventCount();
        double nondupSent = ((double)bytesTransmitted - ((double)resentTotal)*averagePacketSize);
        double bwResent = (nondupSent <= 0 ? 0d : ((((double)resentTotal)*averagePacketSize) / nondupSent));
        buf.append("<h3>Percentage of bytes retransmitted (lifetime): ").append(formatPct(bwResent));
        buf.append("</h3><i>(Includes retransmission required by packet loss)</i>\n");
      *****/

        out.write(buf.toString());
        buf.setLength(0);
    }

    private static final DecimalFormat _fmt = new DecimalFormat("#,##0.00");
    private static final String formatKBps(int bps) {
        synchronized (_fmt) {
            return _fmt.format((float)bps/1024);
        }
    }
    private static final DecimalFormat _pctFmt = new DecimalFormat("#0.0%");

    /*
     * Cache the bid to reduce object churn
     */
    private class SharedBid extends TransportBid {
        public SharedBid(int ms) { super(); setLatencyMs(ms); }
        @Override
        public Transport getTransport() { return UDPTransport.this; }
        @Override
        public String toString() { return "UDP bid @ " + getLatencyMs(); }
    }
    
    private class ExpirePeerEvent extends SimpleTimer2.TimedEvent {
        // TODO why have separate Set, just use _peersByIdent.values()
        private final Set<PeerState> _expirePeers;
        private final List<PeerState> _expireBuffer;
        private volatile boolean _alive;
        private int _runCount;
        private boolean _lastLoopShort;
        // we've seen firewalls change ports after 40 seconds
        private static final long PING_FIREWALL_TIME = 30*1000;
        private static final long PING_FIREWALL_CUTOFF = PING_FIREWALL_TIME / 2;
        // ping 1/4 of the peers every loop
        private static final int SLICES = 4;
        private static final long SHORT_LOOP_TIME = PING_FIREWALL_CUTOFF / (SLICES + 1);
        private static final long LONG_LOOP_TIME = 25*1000;
        private static final long EXPIRE_INCREMENT = 15*1000;
        private static final long EXPIRE_DECREMENT = 45*1000;
        private static final long MAY_DISCON_TIMEOUT = 10*1000;

        public ExpirePeerEvent() {
            super(_context.simpleTimer2());
            _expirePeers = new ConcurrentHashSet<PeerState>(128);
            _expireBuffer = new ArrayList<PeerState>();
        }

        public void timeReached() {
            // Increase allowed idle time if we are well under allowed connections, otherwise decrease
            boolean haveCap = haveCapacity(33);
            if (haveCap) {
                long inc;
                // don't adjust too quickly if we are looping fast
                if (_lastLoopShort)
                    inc = EXPIRE_INCREMENT * SHORT_LOOP_TIME / LONG_LOOP_TIME;
                else
                    inc = EXPIRE_INCREMENT;
                _expireTimeout = Math.min(_expireTimeout + inc, EXPIRE_TIMEOUT);
            } else {
                long dec;
                if (_lastLoopShort)
                    dec = EXPIRE_DECREMENT * SHORT_LOOP_TIME / LONG_LOOP_TIME;
                else
                    dec = EXPIRE_DECREMENT;
                _expireTimeout = Math.max(_expireTimeout - dec, MIN_EXPIRE_TIMEOUT);
            }
            long now = _context.clock().now();
            long shortInactivityCutoff = now - _expireTimeout;
            long longInactivityCutoff = now - EXPIRE_TIMEOUT;
            final long mayDisconCutoff = now - MAY_DISCON_TIMEOUT;
            long pingCutoff = now - (2 * 60*60*1000);
            long pingFirewallCutoff = now - PING_FIREWALL_CUTOFF;
            boolean shouldPingFirewall = _reachabilityStatus != Status.OK;
            int currentListenPort = getListenPort(false);
            boolean pingOneOnly = shouldPingFirewall && getExternalPort(false) == currentListenPort;
            boolean shortLoop = shouldPingFirewall || !haveCap || _context.netDb().floodfillEnabled();
            _lastLoopShort = shortLoop;
            _expireBuffer.clear();
            _runCount++;

                for (Iterator<PeerState> iter = _expirePeers.iterator(); iter.hasNext(); ) {
                    PeerState peer = iter.next();
                    long inactivityCutoff;
                    // if we offered to introduce them, or we used them as introducer in last 2 hours
                    if (peer.getWeRelayToThemAs() > 0 || peer.getIntroducerTime() > pingCutoff) {
                        inactivityCutoff = longInactivityCutoff;
                    } else if ((!haveCap || !peer.isInbound()) &&
                               peer.getMayDisconnect() &&
                               peer.getMessagesReceived() <= 2 && peer.getMessagesSent() <= 2) {
                        if (_log.shouldInfo())
                            _log.info("Possible early disconnect for: " + peer);
                        inactivityCutoff = mayDisconCutoff;
                    } else {
                        inactivityCutoff = shortInactivityCutoff;
                    }
                    if ( (peer.getLastReceiveTime() < inactivityCutoff) && (peer.getLastSendTime() < inactivityCutoff) ) {
                        _expireBuffer.add(peer);
                        iter.remove();
                    } else if (shouldPingFirewall &&
                               ((_runCount ^ peer.hashCode()) & (SLICES - 1)) == 0 &&
                               peer.getLastSendOrPingTime() < pingFirewallCutoff &&
                               peer.getLastReceiveTime() < pingFirewallCutoff) {
                        // ping if firewall is mapping the port to keep port the same...
                        // if the port changes we are screwed
                        if (_log.shouldLog(Log.DEBUG))
                            _log.debug("Pinging for firewall: " + peer);
                        // don't update or idle time won't be right and peer won't get dropped
                        // TODO if both sides are firewalled should only one ping
                        // or else session will stay open forever?
                        //peer.setLastSendTime(now);
                        send(_destroyBuilder.buildPing(peer));
                        peer.setLastPingTime(now);
                        // If external port is different, it may be changing the port for every
                        // session, so ping all of them. Otherwise only one.
                        if (pingOneOnly)
                            shouldPingFirewall = false;
		    }
                }

            if (!_expireBuffer.isEmpty()) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Expiring " + _expireBuffer.size() + " peers");
                for (PeerState peer : _expireBuffer) {
                    sendDestroy(peer);
                    dropPeer(peer, false, "idle too long");
                    // TODO sleep to limit burst like in destroyAll() ??
                    // but we are on the timer thread...
                    // hopefully this isn't too many at once
                    // ... or only send a max of x, then requeue
                }
                _expireBuffer.clear();
            }

            if (_alive)
                schedule(shortLoop ? SHORT_LOOP_TIME : LONG_LOOP_TIME);
        }

        public void add(PeerState peer) {
                _expirePeers.add(peer);
        }

        public void remove(PeerState peer) {
                _expirePeers.remove(peer);
        }

        public void setIsAlive(boolean isAlive) {
            _alive = isAlive;
            if (isAlive) {
                reschedule(LONG_LOOP_TIME);
            } else {
                cancel();
                _expirePeers.clear();
            }
        }
    }
    
    /**
     *  IPv4 only
     */
    private void setReachabilityStatus(Status status) { 
        setReachabilityStatus(status, false);
    }
    
    /**
     *  @since 0.9.27
     *  @param isIPv6 Is the change an IPv6 change?
     */
    void setReachabilityStatus(Status status, boolean isIPv6) { 
        synchronized (_rebuildLock) {
            locked_setReachabilityStatus(status, isIPv6);
        }
    }

    /**
     *  @param isIPv6 Is the change an IPv6 change?
     */
    private void locked_setReachabilityStatus(Status newStatus, boolean isIPv6) { 
        Status old = _reachabilityStatus;
        // merge new status into old
        Status status = Status.merge(old, newStatus);
        _testEvent.setLastTested(isIPv6);
        // now modify if we are IPv6 only
        TransportUtil.IPv6Config config = getIPv6Config();
        if (config == IPV6_ONLY) {
            if (status == Status.IPV4_UNKNOWN_IPV6_OK)
                status = Status.IPV4_DISABLED_IPV6_OK;
            else if (status == Status.IPV4_UNKNOWN_IPV6_FIREWALLED)
                status = Status.IPV4_DISABLED_IPV6_FIREWALLED;
            else if (status == Status.UNKNOWN)
                status = Status.IPV4_DISABLED_IPV6_UNKNOWN;
        }
        if (status != Status.UNKNOWN) {
            // now modify if we have no IPv6 address
            if (_currentOurV6Address == null && !_haveIPv6Address) {
                if (status == Status.IPV4_OK_IPV6_UNKNOWN)
                    status = Status.OK;
                else if (status == Status.IPV4_FIREWALLED_IPV6_UNKNOWN)
                    status = Status.REJECT_UNSOLICITED;
                else if (status == Status.IPV4_SNAT_IPV6_UNKNOWN)
                    status = Status.DIFFERENT;
                // prevent firewalled -> OK -> firewalled+OK
                else if (status == Status.IPV4_FIREWALLED_IPV6_OK)
                    status = Status.REJECT_UNSOLICITED;
                else if (status == Status.IPV4_SNAT_IPV6_OK)
                    status = Status.DIFFERENT;
            }

            if (status != old) {
                // for the following transitions ONLY, require two in a row
                // to prevent thrashing
                if ((old == Status.OK && (status == Status.DIFFERENT ||
                                          status == Status.REJECT_UNSOLICITED ||
                                          status == Status.IPV4_FIREWALLED_IPV6_OK ||
                                          status == Status.IPV4_SNAT_IPV6_OK ||
                                          status == Status.IPV4_OK_IPV6_FIREWALLED)) ||
                    (status == Status.OK && (old == Status.DIFFERENT ||
                                             old == Status.REJECT_UNSOLICITED ||
                                             old == Status.IPV4_FIREWALLED_IPV6_OK ||
                                             old == Status.IPV4_SNAT_IPV6_OK ||
                                             old == Status.IPV4_OK_IPV6_FIREWALLED))) {
                    if (status != _reachabilityStatusPending) {
                        if (_log.shouldLog(Log.WARN))
                            _log.warn("Old status: " + old + " status pending confirmation: " + status +
                                      " Caused by update: " + newStatus);
                        _reachabilityStatusPending = status;
                        _testEvent.forceRunSoon(isIPv6);
                        return;
                    }
                }
                _reachabilityStatusUnchanged = 0;
                long now = _context.clock().now();
                _reachabilityStatusLastUpdated = now;
                _reachabilityStatus = status;
            } else {
                _reachabilityStatusUnchanged++;
            }
            _reachabilityStatusPending = status;
        }
        if (status != old) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Old status: " + old + " New status: " + status +
                          " Caused by update: " + newStatus +
                          " from: ", new Exception("traceback"));
            if (old != Status.UNKNOWN)
                _context.router().eventLog().addEvent(EventLog.REACHABILITY,
                   "from " + _t(old.toStatusString()) + " to " +  _t(status.toStatusString()));
            // Always rebuild when the status changes, even if our address hasn't changed,
            // as rebuildExternalAddress() calls replaceAddress() which calls CSFI.notifyReplaceAddress()
            // which will start up NTCP inbound when we transition to OK.
            // if (needsRebuild())
                rebuildExternalAddress();
        } else {
            if (_log.shouldLog(Log.INFO))
                _log.info("Status unchanged: " + _reachabilityStatus +
                          " after update: " + newStatus +
                          " (unchanged " + _reachabilityStatusUnchanged + " consecutive times), last updated " +
                          DataHelper.formatDuration(_context.clock().now() - _reachabilityStatusLastUpdated) + " ago");
        }
    }

    private static final String PROP_REACHABILITY_STATUS_OVERRIDE = "i2np.udp.status";

    /**
     * Previously returned short, now enum as of 0.9.20
     */
    public Status getReachabilityStatus() { 
        String override = _context.getProperty(PROP_REACHABILITY_STATUS_OVERRIDE);
        if (override != null) {
            if ("ok".equals(override))
                return Status.OK;
            else if ("err-reject".equals(override))
                return Status.REJECT_UNSOLICITED;
            else if ("err-different".equals(override))
                return Status.DIFFERENT;
        }
        return _reachabilityStatus; 
    }

    /**
     * @deprecated unused
     */
    @Override
    @Deprecated
    public void recheckReachability() {
        // FIXME locking if we do this again
        //_testEvent.runTest();
    }
    
    /**
     *  Pick a Bob (if we are Alice) or a Charlie (if we are Bob).
     *
     *  For Bob (as called from PeerTestEvent below), returns an established IPv4/v6 peer.
     *  While the protocol allows Alice to select an unestablished Bob, we don't support that.
     *
     *  For Charlie (as called from PeerTestManager), returns an established IPv4 or IPv6 peer.
     *  (doesn't matter how Bob and Charlie communicate)
     *
     *  Any returned peer must advertise an IPv4 address to prove it is IPv4-capable.
     *  Ditto for v6.
     *
     *  @param peerRole The role of the peer we are looking for, BOB or CHARLIE only (NOT our role)
     *  @param isIPv6 true to get a v6-capable peer back
     *  @param dontInclude may be null
     *  @return IPv4 peer or null
     */
    PeerState pickTestPeer(PeerTestState.Role peerRole, boolean isIPv6, RemoteHostId dontInclude) {
        if (peerRole == ALICE)
            throw new IllegalArgumentException();
        List<PeerState> peers = new ArrayList<PeerState>(_peersByIdent.values());
        for (Iterator<PeerState> iter = new RandomIterator<PeerState>(peers); iter.hasNext(); ) {
            PeerState peer = iter.next();
            if ( (dontInclude != null) && (dontInclude.equals(peer.getRemoteHostId())) )
                continue;
            // enforce IPv4/v6 connection if we are ALICE looking for a BOB
            byte[] ip = peer.getRemoteIP();
            if (peerRole == BOB) {
                if (isIPv6) {
                    if (ip.length != 16)
                        continue;
                } else {
                    if (ip.length != 4)
                        continue;
                }
            }
            // enforce IPv4/v6 advertised for all
            RouterInfo peerInfo = _context.netDb().lookupRouterInfoLocally(peer.getRemotePeer());
            if (peerInfo == null)
                continue;
            if (isIPv6) {
                String v = peerInfo.getVersion();
                if (VersionComparator.comp(v, MIN_V6_PEER_TEST_VERSION) < 0)
                    continue;
            }
            ip = null;
            List<RouterAddress> addrs = getTargetAddresses(peerInfo);
            for (RouterAddress addr : addrs) {
                byte[] rip = addr.getIP();
                if (rip != null) {
                    if (isIPv6) {
                        if (rip.length != 16)
                            continue;
                    } else {
                        if (rip.length != 4)
                            continue;
                    }
                    // as of 0.9.27, we trust the 'B' cap for IPv6
                    String caps = addr.getOption(UDPAddress.PROP_CAPACITY);
                    if (caps != null && caps.contains(CAP_TESTING)) {
                        ip = rip;
                        break;
                    }
                }
            }
            if (ip == null)
                continue;
            if (isTooClose(ip))
                continue;
            return peer;
        }
        return null;
    }
    
    /**
     *  Periodically ping the introducers, split out since we need to
     *  do it faster than we rebuild our address.
     *  @since 0.8.11
     */
    private class PingIntroducers implements SimpleTimer.TimedEvent {
        public void timeReached() {
            if (introducersRequired())
                _introManager.pingIntroducers();
        }
    }

/*******
    private static final String BADIPS[] = new String[] { "192.168.0.1", "127.0.0.1", "10.3.4.5", "172.16.3.4", "224.5.6.7" };
    private static final String GOODIPS[] = new String[] { "192.167.0.1", "126.0.0.1", "11.3.4.5", "172.15.3.4", "223.5.6.7" };
    public static void main(String args[]) {
        for (int i = 0; i < BADIPS.length; i++) {
            try { 
                InetAddress addr = InetAddress.getByName(BADIPS[i]);
                boolean routable = isPubliclyRoutable(addr.getAddress());
                System.out.println("Routable: " + routable + " (" + BADIPS[i] + ")");
            } catch (Exception e) { e.printStackTrace(); }
        }
        for (int i = 0; i < GOODIPS.length; i++) {
            try { 
                InetAddress addr = InetAddress.getByName(GOODIPS[i]);
                boolean routable = isPubliclyRoutable(addr.getAddress());
                System.out.println("Routable: " + routable + " (" + GOODIPS[i] + ")");
            } catch (Exception e) { e.printStackTrace(); }
        }
    }
*******/
}
