diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java
index d243de198c68e3d5e9c98e0e4d93a33baa48fb40..37fc5fe49eb028196b8fea612b9e04ea0dc5341f 100644
--- a/router/java/src/net/i2p/router/transport/TransportImpl.java
+++ b/router/java/src/net/i2p/router/transport/TransportImpl.java
@@ -68,6 +68,9 @@ public abstract class TransportImpl implements Transport {
     private static final long UNREACHABLE_PERIOD = 5*60*1000;
     private static final long WAS_UNREACHABLE_PERIOD = 30*60*1000;
 
+    /** @since 0.9.44 */
+    protected static final String PROP_IPV6_FIREWALLED = "i2np.lastIPv6Firewalled";
+
     static {
         long maxMemory = SystemVersion.getMaxMemory();
         long min = 512;
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 b85b2d7042e379aade50e760d01e33e44e87a40d..a4e9497adefe2890d851f2c99b9eb3790d941be5 100644
--- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
+++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
@@ -872,9 +872,12 @@ public class NTCPTransport extends TransportImpl {
                     props.setProperty(RouterAddress.PROP_HOST, ia.getHostAddress());
                     props.setProperty(RouterAddress.PROP_PORT, Integer.toString(port));
                     addNTCP2Options(props);
-                    int cost = getDefaultCost(ia instanceof Inet6Address);
-                    myAddress = new RouterAddress(getPublishStyle(), props, cost);
-                    replaceAddress(myAddress);
+                    boolean ipv6 = ia instanceof Inet6Address;
+                    if (!ipv6 || !_context.getBooleanProperty(PROP_IPV6_FIREWALLED)) {
+                        int cost = getDefaultCost(ipv6);
+                        myAddress = new RouterAddress(getPublishStyle(), props, cost);
+                        replaceAddress(myAddress);
+                    }
                 }
             } else if (_enableNTCP2) {
                 setOutboundNTCP2Address();
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 6b18cdbfd8854b009a9c1421ed9ab048bd8d260a..d9bc105e386d122fa8123fc5bc9a47e85328236a 100644
--- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
+++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
@@ -9,6 +9,7 @@ import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -169,7 +170,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     public static final String PROP_IP_CHANGE = "i2np.lastIPChange";
     public static final String PROP_LAPTOP_MODE = "i2np.laptopMode";
     /** @since 0.9.43 */
-    public static final String PROP_IPV6= "i2np.lastIPv6";
+    public static final String PROP_IPV6 = "i2np.lastIPv6";
 
     /** do we require introducers, regardless of our status? */
     public static final String PROP_FORCE_INTRODUCERS = "i2np.udp.forceIntroducers";
@@ -235,6 +236,42 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
      */
     private static final String MIN_V6_PEER_TEST_VERSION = "0.9.27";
 
+    // various state bitmaps
+
+    private static final Set<Status> STATUS_IPV4_FW =    EnumSet.of(Status.DIFFERENT,
+                                                                    Status.REJECT_UNSOLICITED,
+                                                                    Status.IPV4_FIREWALLED_IPV6_OK,
+                                                                    Status.IPV4_SNAT_IPV6_OK,
+                                                                    Status.IPV4_OK_IPV6_FIREWALLED);
+
+    private static final Set<Status> STATUS_IPV6_FW =    EnumSet.of(Status.IPV4_OK_IPV6_FIREWALLED,
+                                                                    Status.IPV4_UNKNOWN_IPV6_FIREWALLED,
+                                                                    Status.IPV4_DISABLED_IPV6_FIREWALLED);
+
+    private static final Set<Status> STATUS_IPV6_FW_2 =  EnumSet.of(Status.IPV4_OK_IPV6_FIREWALLED,
+                                                                    Status.IPV4_UNKNOWN_IPV6_FIREWALLED,
+                                                                    Status.IPV4_DISABLED_IPV6_FIREWALLED,
+                                                                    Status.DIFFERENT,
+                                                                    Status.REJECT_UNSOLICITED);
+
+    private static final Set<Status> STATUS_IPV6_OK =    EnumSet.of(Status.OK,
+                                                                    Status.IPV4_UNKNOWN_IPV6_OK,
+                                                                    Status.IPV4_FIREWALLED_IPV6_OK,
+                                                                    Status.IPV4_DISABLED_IPV6_OK,
+                                                                    Status.IPV4_SNAT_IPV6_OK);
+
+    private static final Set<Status> STATUS_NO_RETEST =  EnumSet.of(Status.OK,
+                                                                    Status.IPV4_OK_IPV6_UNKNOWN,
+                                                                    Status.IPV4_OK_IPV6_FIREWALLED,
+                                                                    Status.IPV4_DISABLED_IPV6_OK,
+                                                                    Status.IPV4_DISABLED_IPV6_UNKNOWN,
+                                                                    Status.IPV4_DISABLED_IPV6_FIREWALLED,
+                                                                    Status.DISCONNECTED);
+
+    private static final Set<Status> STATUS_NEED_INTRO = EnumSet.of(Status.REJECT_UNSOLICITED,
+                                                                    Status.IPV4_FIREWALLED_IPV6_OK,
+                                                                    Status.IPV4_FIREWALLED_IPV6_UNKNOWN);
+
 
     public UDPTransport(RouterContext ctx, DHSessionKeyBuilder.Factory dh) {
         super(ctx);
@@ -555,17 +592,18 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                     if (hasv6)
                         continue;
                     hasv6 = true;
-                    // 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())
+                    if (isIPv6Firewalled() || _context.getBooleanProperty(PROP_IPV6_FIREWALLED)) {
+                        setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_FIREWALLED, true);
+                    } else {
+                        _lastInboundIPv6 = _context.clock().now();
                         setReachabilityStatus(Status.IPV4_UNKNOWN_IPV6_OK, true);
+                        rebuildExternalAddress(ia.getHostAddress(), newPort, false);
+                    }
                 } else {
                     if (!isIPv4Firewalled())
                         setReachabilityStatus(Status.IPV4_OK_IPV6_UNKNOWN);
+                    rebuildExternalAddress(ia.getHostAddress(), newPort, false);
                 }
-                rebuildExternalAddress(ia.getHostAddress(), newPort, false);
             }
         } else if (newPort > 0 && !bindToAddrs.isEmpty()) {
             for (InetAddress ia : bindToAddrs) {
@@ -595,6 +633,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
     }
     
     public synchronized void shutdown() {
+        if (_haveIPv6Address) {
+            boolean fwOld = _context.getBooleanProperty(PROP_IPV6_FIREWALLED);
+            boolean fwNew = STATUS_IPV6_FW.contains(_reachabilityStatus);
+            if (fwOld != fwNew)
+                _context.router().saveConfig(PROP_IPV6_FIREWALLED, Boolean.toString(fwNew));
+        }
         destroyAll();
         for (UDPEndpoint endpoint : _endpoints) {
             endpoint.shutdown();
@@ -1051,11 +1095,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                                 _log.info("New IPv6 address received but not one of our local addresses: " + ipstr, new Exception());
                             return false;
                         }
-                        if (_reachabilityStatus == Status.IPV4_OK_IPV6_FIREWALLED ||
-                            _reachabilityStatus == Status.IPV4_UNKNOWN_IPV6_FIREWALLED ||
-                            _reachabilityStatus == Status.IPV4_DISABLED_IPV6_FIREWALLED ||
-                            _reachabilityStatus == Status.DIFFERENT ||
-                            _reachabilityStatus == Status.REJECT_UNSOLICITED) {
+                        if (STATUS_IPV6_FW_2.contains(_reachabilityStatus)) {
                             // If we were firewalled before, let's assume we're still firewalled.
                             // Save the new IP and fire a test
                             String oldIP = _context.getProperty(PROP_IPV6);
@@ -1217,9 +1257,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         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;
+        return !STATUS_NEED_INTRO.contains(status);
     }
 
     /** 
@@ -1450,13 +1488,7 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
         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 &&
+            if (!STATUS_NO_RETEST.contains(status) &&
                 _reachabilityStatusUnchanged < 7) {
                 _testEvent.forceRunSoon(peer.isIPv6());
             }
@@ -3063,16 +3095,8 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             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 ((old == Status.OK && STATUS_IPV4_FW.contains(status)) ||
+                    (status == Status.OK && STATUS_IPV4_FW.contains(old))) {
                     if (status != _reachabilityStatusPending) {
                         if (_log.shouldLog(Log.WARN))
                             _log.warn("Old status: " + old + " status pending confirmation: " + status +
@@ -3103,18 +3127,10 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
             // as rebuildExternalAddress() calls replaceAddress() which calls CSFI.notifyReplaceAddress()
             // which will start up NTCP inbound when we transition to OK.
             if (isIPv6) {
-                if (status == Status.IPV4_OK_IPV6_FIREWALLED ||
-                    status == Status.IPV4_UNKNOWN_IPV6_FIREWALLED ||
-                    status == Status.IPV4_DISABLED_IPV6_FIREWALLED) {
+                if (STATUS_IPV6_FW.contains(status)) {
                     removeExternalAddress(true, true);
-                } else if ((old == Status.IPV4_OK_IPV6_FIREWALLED ||
-                            old == Status.IPV4_UNKNOWN_IPV6_FIREWALLED ||
-                            old == Status.IPV4_DISABLED_IPV6_FIREWALLED) &&
-                           (status == Status.OK ||
-                            status == Status.IPV4_UNKNOWN_IPV6_OK ||
-                            status == Status.IPV4_FIREWALLED_IPV6_OK ||
-                            status == Status.IPV4_DISABLED_IPV6_OK ||
-                            status == Status.IPV4_SNAT_IPV6_OK) &&
+                } else if (STATUS_IPV6_FW.contains(old) &&
+                           STATUS_IPV6_OK.contains(status) &&
                            _lastOurIPv6 != null &&
                            !explicitAddressSpecified()){
                      String addr = Addresses.toString(_lastOurIPv6);