From 1868d2b50f9f033aaf4cd035dcfcb466bbf097da Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Thu, 30 Apr 2015 20:33:46 +0000
Subject: [PATCH] Transport: Ticket #1458 continued... Implement methods to
 remove only a single IPv4 or IPv6 address, so that IPv6 addresses will remain
 when SSU detects that IPv4 is firewalled Summary bar status fixes Fix
 getIsPortFixed() for more enum cases log tweaks, cleanups

---
 .../src/net/i2p/router/web/SummaryHelper.java |  5 +-
 .../src/net/i2p/router/CommSystemFacade.java  | 14 ++-
 .../transport/CommSystemFacadeImpl.java       | 29 +++++-
 .../net/i2p/router/transport/Transport.java   | 18 ++++
 .../i2p/router/transport/TransportImpl.java   | 77 +++++++++++++++
 .../router/transport/TransportManager.java    | 18 +++-
 .../router/transport/ntcp/NTCPTransport.java  | 66 ++++++++-----
 .../router/transport/udp/UDPTransport.java    | 94 ++++++++++++++-----
 8 files changed, 269 insertions(+), 52 deletions(-)

diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
index 1581b7b4df..42dcd3352b 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java
@@ -160,7 +160,6 @@ public class SummaryHelper extends HelperBase {
             case IPV4_OK_IPV6_UNKNOWN:
             case IPV4_OK_IPV6_FIREWALLED:
             case IPV4_UNKNOWN_IPV6_OK:
-            case IPV4_FIREWALLED_IPV6_OK:
             case IPV4_DISABLED_IPV6_OK:
             case IPV4_SNAT_IPV6_OK:
                 RouterAddress ra = routerInfo.getTargetAddress("NTCP");
@@ -179,10 +178,12 @@ public class SummaryHelper extends HelperBase {
                 return _("ERR-SymmetricNAT");
 
             case REJECT_UNSOLICITED:
-            case IPV4_FIREWALLED_IPV6_UNKNOWN:
             case IPV4_DISABLED_IPV6_FIREWALLED:
                 if (routerInfo.getTargetAddress("NTCP") != null)
                     return _("WARN-Firewalled with Inbound TCP Enabled");
+                // fall through...
+            case IPV4_FIREWALLED_IPV6_OK:
+            case IPV4_FIREWALLED_IPV6_UNKNOWN:
                 if (((FloodfillNetworkDatabaseFacade)_context.netDb()).floodfillEnabled())
                     return _("WARN-Firewalled and Floodfill");
                 //if (_context.router().getRouterInfo().getCapabilities().indexOf('O') >= 0)
diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java
index cdd4624f34..e9c050762f 100644
--- a/router/java/src/net/i2p/router/CommSystemFacade.java
+++ b/router/java/src/net/i2p/router/CommSystemFacade.java
@@ -113,7 +113,19 @@ public abstract class CommSystemFacade implements Service {
     /** 
      * Tell other transports our address changed
      */
-    public void notifyReplaceAddress(RouterAddress UDPAddr) {}
+    public void notifyReplaceAddress(RouterAddress address) {}
+
+    /** 
+     * Tell other transports our address changed
+     * @since 0.9.20
+     */
+    public void notifyRemoveAddress(RouterAddress address) {}
+
+    /** 
+     * Tell other transports our address changed
+     * @since 0.9.20
+     */
+    public void notifyRemoveAddress(boolean ipv6) {}
 
     /**
      *  Pluggable transport
diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
index d2e29f21c8..a9b8020607 100644
--- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
+++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
@@ -238,7 +238,34 @@ public class CommSystemFacadeImpl extends CommSystemFacade {
             if (udp != null)
                 port = udp.getRequestedPort();
         }
-        _manager.externalAddressReceived(Transport.AddressSource.SOURCE_SSU, ip, port);
+        if (ip != null || port > 0)
+            _manager.externalAddressReceived(Transport.AddressSource.SOURCE_SSU, ip, port);
+        else
+            notifyRemoveAddress(false);
+    }
+
+    /** 
+     *  Tell other transports our address changed
+     *
+     *  @param address non-null; but address's host/IP may be null
+     *  @since 0.9.20
+     */
+    @Override
+    public void notifyRemoveAddress(RouterAddress address) {
+        // just keep this simple for now, multiple v4 or v6 addresses not yet supported
+        notifyRemoveAddress(address != null &&
+                            address.getIP() != null &&
+                            address.getIP().length == 16);
+    }
+
+    /** 
+     *  Tell other transports our address changed
+     *
+     *  @since 0.9.20
+     */
+    @Override
+    public void notifyRemoveAddress(boolean ipv6) {
+        _manager.externalAddressRemoved(Transport.AddressSource.SOURCE_SSU, ipv6);
     }
 
     /**
diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java
index 0789d7e75f..a9c03b2ee8 100644
--- a/router/java/src/net/i2p/router/transport/Transport.java
+++ b/router/java/src/net/i2p/router/transport/Transport.java
@@ -98,6 +98,24 @@ public interface Transport {
      */
     public void externalAddressReceived(AddressSource source, byte[] ip, int port);
 
+    /**
+     *  Notify a transport of an external address change.
+     *  This may be from a local interface, UPnP, a config change, etc.
+     *  This should not be called if the ip didn't change
+     *  (from that source's point of view), or is a local address.
+     *  May be called multiple times for IPv4 or IPv6.
+     *  The transport should also do its own checking on whether to accept
+     *  notifications from this source.
+     *
+     *  This can be called after the transport is running.
+     *
+     *  TODO externalAddressRemoved(source, ip, port)
+     *
+     *  @param source defined in Transport.java
+     *  @since 0.9.20
+     */
+    public void externalAddressRemoved(AddressSource source, boolean ipv6);
+
     /**
      *  Notify a transport of the results of trying to forward a port.
      *
diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java
index 6056089dd0..b5f0870aad 100644
--- a/router/java/src/net/i2p/router/transport/TransportImpl.java
+++ b/router/java/src/net/i2p/router/transport/TransportImpl.java
@@ -554,6 +554,7 @@ public abstract class TransportImpl implements Transport {
      *  with the same IP length (4 or 16) with the given one.
      *  TODO: Allow multiple addresses of the same length.
      *  Calls listener.transportAddressChanged()
+     *  To remove all IPv4 or IPv6 addresses, use removeAddress(boolean).
      *
      *  @param address null to remove all
      */
@@ -566,6 +567,7 @@ public abstract class TransportImpl implements Transport {
             boolean isIPv6 = TransportUtil.isIPv6(address);
             for (RouterAddress ra : _currentAddresses) {
                 if (isIPv6 == TransportUtil.isIPv6(ra))
+                    // COWAL
                     _currentAddresses.remove(ra);
             }
             _currentAddresses.add(address);
@@ -576,6 +578,61 @@ public abstract class TransportImpl implements Transport {
             _listener.transportAddressChanged();
     }
 
+    /**
+     *  Remove only this address.
+     *  Calls listener.transportAddressChanged().
+     *  To remove all IPv4 or IPv6 addresses, use removeAddress(boolean).
+     *  To remove all IPv4 and IPv6 addresses, use replaceAddress(null).
+     *
+     *  @param ipv6 true to remove all IPv6 addresses, false to remove all IPv4 addresses
+     *  @since 0.9.20
+     */
+    protected void removeAddress(RouterAddress address) {
+        if (_log.shouldWarn())
+             _log.warn("Removing address " + address, new Exception());
+        boolean changed = _currentAddresses.remove(address);
+            changed = true;
+        if (changed) {
+            if (_log.shouldWarn())
+                 _log.warn(getStyle() + " now has " + _currentAddresses.size() + " addresses");
+            if (_listener != null)
+                _listener.transportAddressChanged();
+        } else {
+            if (_log.shouldWarn())
+                 _log.warn(getStyle() + " no addresses removed");
+        }
+    }
+
+    /**
+     *  Remove all existing addresses with the specified IP length (4 or 16).
+     *  Calls listener.transportAddressChanged().
+     *  To remove all IPv4 and IPv6 addresses, use replaceAddress(null).
+     *
+     *  @param ipv6 true to remove all IPv6 addresses, false to remove all IPv4 addresses
+     *  @since 0.9.20
+     */
+    protected void removeAddress(boolean ipv6) {
+        if (_log.shouldWarn())
+             _log.warn("Removing addresses, ipv6? " + ipv6, new Exception());
+        boolean changed = false;
+        for (RouterAddress ra : _currentAddresses) {
+            if (ipv6 == TransportUtil.isIPv6(ra)) {
+                // COWAL
+                if (_currentAddresses.remove(ra))
+                    changed = true;
+            }
+        }
+        if (changed) {
+            if (_log.shouldWarn())
+                 _log.warn(getStyle() + " now has " + _currentAddresses.size() + " addresses");
+            if (_listener != null)
+                _listener.transportAddressChanged();
+        } else {
+            if (_log.shouldWarn())
+                 _log.warn(getStyle() + " no addresses removed");
+        }
+    }
+
     /**
      *  Save a local address we were notified about before we started.
      *
@@ -699,6 +756,26 @@ public abstract class TransportImpl implements Transport {
      */
     public void externalAddressReceived(AddressSource source, byte[] ip, int port) {}
 
+    /**
+     *  Notify a transport of an external address change.
+     *  This may be from a local interface, UPnP, a config change, etc.
+     *  This should not be called if the ip didn't change
+     *  (from that source's point of view), or is a local address.
+     *  May be called multiple times for IPv4 or IPv6.
+     *  The transport should also do its own checking on whether to accept
+     *  notifications from this source.
+     *
+     *  This can be called after the transport is running.
+     *
+     *  TODO externalAddressRemoved(source, ip, port)
+     *
+     *  This implementation does nothing. Transports should override if they want notification.
+     *
+     *  @param source defined in Transport.java
+     *  @since 0.9.20
+     */
+    public void externalAddressRemoved(AddressSource source, boolean ipv6) {}
+
     /**
      *  Notify a transport of the results of trying to forward a port.
      *
diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
index 217b1acbeb..eb44660133 100644
--- a/router/java/src/net/i2p/router/transport/TransportManager.java
+++ b/router/java/src/net/i2p/router/transport/TransportManager.java
@@ -193,7 +193,8 @@ public class TransportManager implements TransportEventListener {
 
     /**
      * Initialize from interfaces, and callback from UPnP or SSU.
-     * Tell all transports... but don't loop
+     * See CSFI.notifyReplaceAddress().
+     * Tell all transports... but don't loop.
      *
      */
     public void externalAddressReceived(Transport.AddressSource source, byte[] ip, int port) {
@@ -204,6 +205,21 @@ public class TransportManager implements TransportEventListener {
         }
     }
 
+    /**
+     *  Remove all ipv4 or ipv6 addresses.
+     *  See CSFI.notifyRemoveAddress().
+     *  Tell all transports... but don't loop.
+     *
+     *  @since 0.9.20
+     */
+    public void externalAddressRemoved(Transport.AddressSource source, boolean ipv6) {
+        for (Transport t : _transports.values()) {
+            // don't loop
+            if (!(source == SOURCE_SSU && t.getStyle().equals(UDPTransport.STYLE)))
+                t.externalAddressRemoved(source, ipv6);
+        }
+    }
+
     /**
      * callback from UPnP
      *
diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
index 3b681afdb7..ca6259e456 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
@@ -602,7 +602,7 @@ public class NTCPTransport extends TransportImpl {
         // try once again to prevent two pumpers which is fatal
         if (_pumper.isAlive())
             return;
-        if (_log.shouldLog(Log.WARN)) _log.warn("Starting ntcp transport listening");
+        if (_log.shouldLog(Log.WARN)) _log.warn("Starting NTCP transport listening");
 
         startIt();
         RouterAddress addr = configureLocalAddress();
@@ -636,16 +636,19 @@ public class NTCPTransport extends TransportImpl {
 
     /**
      *  Only called by externalAddressReceived().
+     *  Calls replaceAddress() or removeAddress().
+     *  To remove all addresses, call replaceAddress(null) directly.
      *
      *  Doesn't actually restart unless addr is non-null and
      *  the port is different from the current listen port.
-     *  If addr is null, removes IPv4 addresses only.
+     *  If addr is null, removes the addresses specified (v4 or v6)
      *
      *  If we had interface addresses before, we lost them.
      *
-     *  @param addr may be null to indicate remove the IPv4 address only
+     *  @param addr may be null to indicate remove the address
+     *  @param ipv6 ignored if addr is non-null
      */
-    private synchronized void restartListening(RouterAddress addr) {
+    private synchronized void restartListening(RouterAddress addr, boolean ipv6) {
         if (addr != null) {
             RouterAddress myAddress = bindAddress(addr.getPort());
             if (myAddress != null)
@@ -654,16 +657,11 @@ public class NTCPTransport extends TransportImpl {
                 replaceAddress(addr);
             // UDPTransport.rebuildExternalAddress() calls router.rebuildRouterInfo()
         } else {
-            // can't do this, want to remove IPv4 only
-            //replaceAddress(null);
-            for (RouterAddress ra : _currentAddresses) {
-                byte[] ip = ra.getIP();
-                if (ip != null && ip.length == 4) {
-                    // COWAL
-                    _currentAddresses.remove(ra);
-                }
-            }
-            _lastInboundIPv4 = 0;
+            removeAddress(ipv6);
+            if (ipv6)
+                _lastInboundIPv6 = 0;
+            else
+                _lastInboundIPv4 = 0;
         }
     }
 
@@ -952,7 +950,7 @@ public class NTCPTransport extends TransportImpl {
     @Override
     public void externalAddressReceived(AddressSource source, byte[] ip, int port) {
         if (_log.shouldLog(Log.WARN))
-            _log.warn("Received address: " + Addresses.toString(ip, port) + " from: " + source);
+            _log.warn("Received address: " + Addresses.toString(ip, port) + " from: " + source, new Exception());
         if ((source == SOURCE_INTERFACE || source == SOURCE_SSU)
              && ip != null && ip.length == 16) {
             // must be set before isValid() call
@@ -978,8 +976,35 @@ public class NTCPTransport extends TransportImpl {
         // ignore UPnP for now, get everything from SSU
         if (source != SOURCE_SSU)
             return;
-        externalAddressReceived(ip, port);
+        boolean isIPv6 = ip != null && ip.length == 16;
+        externalAddressReceived(ip, isIPv6, port);
     }
+
+    /**
+     *  Notify a transport of an external address change.
+     *  This may be from a local interface, UPnP, a config change, etc.
+     *  This should not be called if the ip didn't change
+     *  (from that source's point of view), or is a local address.
+     *  May be called multiple times for IPv4 or IPv6.
+     *  The transport should also do its own checking on whether to accept
+     *  notifications from this source.
+     *
+     *  This can be called after the transport is running.
+     *
+     *  TODO externalAddressRemoved(source, ip, port)
+     *
+     *  @param source defined in Transport.java
+     *  @since 0.9.20
+     */
+    @Override
+    public void externalAddressRemoved(AddressSource source, boolean ipv6) {
+        if (_log.shouldWarn())
+            _log.warn("Removing address, ipv6? " + ipv6 + " from: " + source, new Exception());
+        // ignore UPnP for now, get everything from SSU
+        if (source != SOURCE_SSU)
+            return;
+        externalAddressReceived(null, ipv6, 0);
+    }    
     
     /**
      *  UDP changed addresses, tell NTCP and restart.
@@ -988,10 +1013,9 @@ public class NTCPTransport extends TransportImpl {
      *  @param ip previously validated; may be null to indicate IPv4 failure or port info only
      *  @since IPv6 moved from CSFI.notifyReplaceAddress()
      */
-    private synchronized void externalAddressReceived(byte[] ip, int port) {
+    private synchronized void externalAddressReceived(byte[] ip, boolean isIPv6, int port) {
         // FIXME just take first address for now
         // FIXME if SSU set to hostname, NTCP will be set to IP
-        boolean isIPv6 = ip != null && ip.length == 16;
         RouterAddress oldAddr = getCurrentAddress(isIPv6);
         if (_log.shouldLog(Log.INFO))
             _log.info("Changing NTCP Address? was " + oldAddr);
@@ -1136,13 +1160,11 @@ public class NTCPTransport extends TransportImpl {
         //while (isAlive()) {
         //    try { Thread.sleep(5*1000); } catch (InterruptedException ie) {}
         //}
-        restartListening(newAddr);
+        restartListening(newAddr, isIPv6);
         if (_log.shouldLog(Log.WARN))
-            _log.warn("Updating NTCP Address with " + newAddr);
+            _log.warn("Updating NTCP Address (ipv6? " + isIPv6 + ") with " + newAddr);
         return;     	
     }
-    
-
 
     /**
      *  If we didn't used to be forwarded, and we have an address,
diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
index 9e7dbf6180..41d2cb8137 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -92,6 +92,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      *  TODO periodically update via CSFI.NetMonitor?
      */
     private boolean _haveIPv6Address;
+    private long _lastInboundIPv6;
     
     /** do we need to rebuild our external router address asap? */
     private boolean _needsRebuild;
@@ -464,6 +465,20 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         // 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();
+                    setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK);
+                } else {
+                    setReachabilityStatus(Status.IPV4_OK_IPV6_UNKNOWN);
+                }
                 rebuildExternalAddress(ia.getHostAddress(), newPort, false);
             }
         }
@@ -497,6 +512,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         _introManager.reset();
         UDPPacket.clearCache();
         UDPAddress.clearCache();
+        _lastInboundIPv6 = 0;
     }
 
     /**
@@ -664,6 +680,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     
     void inboundConnectionReceived(boolean isIPv6) {
         if (isIPv6) {
+            _lastInboundIPv6 = _context.clock().now();
             if (_currentOurV6Address != null)
                 setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK);
         } else {
@@ -775,8 +792,6 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      * @param ourPort >= 1024
      */
     void externalAddressReceived(Hash from, byte ourIP[], int ourPort) {
-        if (ourIP.length != 4)
-            return;
         boolean isValid = isValid(ourIP) &&
                           TransportUtil.isValidPort(ourPort);
         boolean explicitSpecified = explicitAddressSpecified();
@@ -785,6 +800,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             _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;
@@ -993,7 +1011,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         if (prop != null)
             return Boolean.parseBoolean(prop);
         Status status = getReachabilityStatus();
-        return status != Status.REJECT_UNSOLICITED;
+        return status != Status.REJECT_UNSOLICITED &&
+               status != Status.IPV4_FIREWALLED_IPV6_OK &&
+               status != Status.IPV4_FIREWALLED_IPV6_UNKNOWN;
     }
 
     /** 
@@ -1035,6 +1055,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      *  @since 0.9.3
      */
     void changePeerPort(PeerState peer, int newPort) {
+        // this happens a lot
         int oldPort;
         synchronized (_addDropLock) {
             oldPort = peer.getRemotePort();
@@ -1044,8 +1065,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                 _peersByRemoteHost.put(peer.getRemoteHostId(), peer);
             }
         }
-        if (_log.shouldLog(Log.WARN) && oldPort != newPort)
-            _log.warn("Changed port from " + oldPort + " to " + newPort + " for " + peer);
+        if (_log.shouldInfo() && oldPort != newPort)
+            _log.info("Changed port from " + oldPort + " to " + newPort + " for " + peer);
     }
 
     /**
@@ -1131,8 +1152,9 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         if (remotePeer != null) {
             oldPeer = _peersByIdent.put(remotePeer, peer);
             if ( (oldPeer != null) && (oldPeer != peer) ) {
-                if (_log.shouldLog(Log.WARN))
-                    _log.warn("Peer already connected (PBID): old=" + oldPeer + " new=" + 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();
@@ -1147,8 +1169,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             RemoteHostId oldID = oldPeer.getRemoteHostId();
             if (!remoteId.equals(oldID)) {
                 // leak fix, remove old address
-                if (_log.shouldLog(Log.WARN))
-                    _log.warn(remotePeer + " changed address FROM " + oldID + " TO " + remoteId);
+                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) {
@@ -1532,8 +1554,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         // max of 1000 pps
         int toSleep = Math.max(8, (1000 / burstps));
         int count = 0;
-        if (_log.shouldLog(Log.WARN))
-            _log.warn("Sending destroy to : " + howMany + " peers");
+        if (_log.shouldInfo())
+            _log.info("Sending destroy to : " + howMany + " peers");
         for (PeerState peer : _peersByIdent.values()) {
             sendDestroy(peer);
             // 1000 per second * 48 bytes = 400 KBps
@@ -1858,6 +1880,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
 
     /**
      *  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
@@ -1972,7 +1995,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
 
             if (wantsRebuild) {
                 if (_log.shouldLog(Log.INFO))
-                    _log.info("Address rebuilt: " + addr);
+                    _log.info("Address rebuilt: " + addr, new Exception());
                 replaceAddress(addr);
                 if (allowRebuildRouterInfo)
                     _context.router().rebuildRouterInfo();
@@ -1999,9 +2022,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             if (hasCurrentAddress()) {
                 // We must remove current address, otherwise the user will see
                 // "firewalled with inbound NTCP enabled" warning in console.
-                // Unfortunately this will remove any IPv6 also,
-                // but we don't have a method to remove just the IPv4 address. FIXME
-                replaceAddress(null);
+                // Remove the IPv4 address only
+                removeAddress(false);
                 if (allowRebuildRouterInfo)
                     _context.router().rebuildRouterInfo();
             }
@@ -2049,6 +2071,30 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         _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.
@@ -2858,16 +2904,14 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      */
     public Status getReachabilityStatus() { 
         String override = _context.getProperty(PROP_REACHABILITY_STATUS_OVERRIDE);
-        if (override == null)
-            return _reachabilityStatus;
-            
-        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;
-        
+        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; 
     }
 
-- 
GitLab