From 5995b0b7a75326d83998a657141ff1ddcbba5dd7 Mon Sep 17 00:00:00 2001
From: zzz <zzz@i2pmail.org>
Date: Sat, 18 Dec 2021 06:14:09 -0500
Subject: [PATCH] Tunnels: Restore support for IP restriction in client tunnels
 (MR !45)

Removed in May 2011 when we added fast tier slices
Also add support in exploratory tunnels
Create MaskedIPSet in peer selectors, pass to ProfileOrganizer.selectXXX() for each call.
Not required for one-hop tunnels.
Disable for test networks (i2np.allowLocal)
Reported by 'vulnerability_reports' http://zzz.i2p/topics/3215
---
 .../router/peermanager/ProfileOrganizer.java  | 126 ++++++++++++------
 .../tunnel/pool/ClientPeerSelector.java       |  32 +++--
 .../tunnel/pool/ExploratoryPeerSelector.java  |  38 +++---
 .../tunnel/pool/TunnelPeerSelector.java       |  73 +++++-----
 4 files changed, 161 insertions(+), 108 deletions(-)

diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
index e84709d935..a480d58ae0 100644
--- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
+++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java
@@ -407,7 +407,7 @@ public class ProfileOrganizer {
      *
      */
     public void selectFastPeers(int howMany, Set<Hash> exclude, Set<Hash> matches) {
-        selectFastPeers(howMany, exclude, matches, 0);
+        selectFastPeers(howMany, exclude, matches, 0, null);
     }
 
     /**
@@ -421,17 +421,19 @@ public class ProfileOrganizer {
      * @param matches set to store the return value in
      * @param mask 0-4 Number of bytes to match to determine if peers in the same IP range should
      *             not be in the same tunnel. 0 = disable check; 1 = /8; 2 = /16; 3 = /24; 4 = exact IP match
+     * @param ipSet in/out param, use for multiple calls, may be null only if mask is 0
+     * @since 0.9.53 added ipSet param
      *
      */
-    public void selectFastPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask) {
+    public void selectFastPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask, MaskedIPSet ipSet) {
         getReadLock();
         try {
-            locked_selectPeers(_fastPeers, howMany, exclude, matches, mask);
+            locked_selectPeers(_fastPeers, howMany, exclude, matches, mask, ipSet);
         } finally { releaseReadLock(); }
         if (matches.size() < howMany) {
             if (_log.shouldLog(Log.INFO))
                 _log.info("selectFastPeers("+howMany+"), not enough fast (" + matches.size() + ") going on to highCap");
-            selectHighCapacityPeers(howMany, exclude, matches, mask);
+            selectHighCapacityPeers(howMany, exclude, matches, mask, ipSet);
         } else {
             if (_log.shouldDebug())
                 _log.debug("selectFastPeers("+howMany+"), found enough fast (" + matches.size() + ")");
@@ -482,8 +484,12 @@ public class ProfileOrganizer {
      *    6: return only from group 2
      *    7: return only from group 3
      *</pre>
+     * @param mask 0-4
+     * @param ipSet in/out param, use for multiple calls, may be null only if mask is 0
+     * @since 0.9.53 added mask and ipSet params
      */
-    public void selectFastPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, SessionKey randomKey, Slice subTierMode) {
+    public void selectFastPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, SessionKey randomKey,
+                                Slice subTierMode, int mask, MaskedIPSet ipSet) {
         getReadLock();
         try {
             if (subTierMode != Slice.SLICE_ALL) {
@@ -492,14 +498,14 @@ public class ProfileOrganizer {
                     subTierMode = Slice.SLICE_ALL;
             }
             if (subTierMode != Slice.SLICE_ALL)
-                locked_selectPeers(_fastPeers, howMany, exclude, matches, randomKey, subTierMode);
+                locked_selectPeers(_fastPeers, howMany, exclude, matches, randomKey, subTierMode, mask, ipSet);
             else
-                locked_selectPeers(_fastPeers, howMany, exclude, matches, 2);
+                locked_selectPeers(_fastPeers, howMany, exclude, matches, mask, ipSet);
         } finally { releaseReadLock(); }
         if (matches.size() < howMany) {
             if (_log.shouldLog(Log.INFO))
                 _log.info("selectFastPeers("+howMany+"), not enough fast (" + matches.size() + ") going on to highCap");
-            selectHighCapacityPeers(howMany, exclude, matches, 2);
+            selectHighCapacityPeers(howMany, exclude, matches, mask, ipSet);
         } else {
             if (_log.shouldDebug())
                 _log.debug("selectFastPeers("+howMany+"), found enough fast (" + matches.size() + ")");
@@ -512,14 +518,16 @@ public class ProfileOrganizer {
      *
      */
     public void selectHighCapacityPeers(int howMany, Set<Hash> exclude, Set<Hash> matches) {
-        selectHighCapacityPeers(howMany, exclude, matches, 0);
+        selectHighCapacityPeers(howMany, exclude, matches, 0, null);
     }
 
     /**
      * @param mask 0-4 Number of bytes to match to determine if peers in the same IP range should
      *             not be in the same tunnel. 0 = disable check; 1 = /8; 2 = /16; 3 = /24; 4 = exact IP match
+     * @param ipSet in/out param, use for multiple calls, may be null only if mask is 0
+     * @since 0.9.53 added ipSet param
      */
-    public void selectHighCapacityPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask) {
+    public void selectHighCapacityPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask, MaskedIPSet ipSet) {
         getReadLock();
         try {
             // we only use selectHighCapacityPeers when we are selecting for PURPOSE_TEST
@@ -531,12 +539,12 @@ public class ProfileOrganizer {
             else
                 exclude.addAll(_fastPeers.keySet());
              */
-            locked_selectPeers(_highCapacityPeers, howMany, exclude, matches, mask);
+            locked_selectPeers(_highCapacityPeers, howMany, exclude, matches, mask, ipSet);
         } finally { releaseReadLock(); }
         if (matches.size() < howMany) {
             if (_log.shouldLog(Log.INFO))
                 _log.info("selectHighCap("+howMany+"), not enough highcap (" + matches.size() + ") going on to ANFP2");
-            selectActiveNotFailingPeers2(howMany, exclude, matches, mask);
+            selectActiveNotFailingPeers2(howMany, exclude, matches, mask, ipSet);
         } else {
             if (_log.shouldDebug())
                 _log.debug("selectHighCap("+howMany+"), found enough highCap (" + matches.size() + ")");
@@ -551,7 +559,7 @@ public class ProfileOrganizer {
      */
     @Deprecated
     public void selectWellIntegratedPeers(int howMany, Set<Hash> exclude, Set<Hash> matches) {
-        selectWellIntegratedPeers(howMany, exclude, matches, 0);
+        selectWellIntegratedPeers(howMany, exclude, matches, 0, null);
     }
 
     /**
@@ -559,18 +567,19 @@ public class ProfileOrganizer {
      *
      * @param mask 0-4 Number of bytes to match to determine if peers in the same IP range should
      *             not be in the same tunnel. 0 = disable check; 1 = /8; 2 = /16; 3 = /24; 4 = exact IP match
+     * @since 0.9.53 added ipSet param
      * @deprecated unused
      */
     @Deprecated
-    public void selectWellIntegratedPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask) {
+    public void selectWellIntegratedPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask, MaskedIPSet ipSet) {
         getReadLock();
         try {
-            locked_selectPeers(_wellIntegratedPeers, howMany, exclude, matches, mask);
+            locked_selectPeers(_wellIntegratedPeers, howMany, exclude, matches, mask, ipSet);
         } finally { releaseReadLock(); }
         if (matches.size() < howMany) {
             if (_log.shouldLog(Log.INFO))
                 _log.info("selectWellIntegrated("+howMany+"), not enough integrated (" + matches.size() + ") going on to notFailing");
-            selectNotFailingPeers(howMany, exclude, matches, mask);
+            selectNotFailingPeers(howMany, exclude, matches, mask, ipSet);
         } else {            
             if (_log.shouldDebug())
                 _log.debug("selectWellIntegrated("+howMany+"), found enough well integrated (" + matches.size() + ")");
@@ -585,18 +594,20 @@ public class ProfileOrganizer {
      *
      */
     public void selectNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches) {
-        selectNotFailingPeers(howMany, exclude, matches, false, 0);
+        selectNotFailingPeers(howMany, exclude, matches, false, 0, null);
     }
 
     /**
      * @param mask ignored, should call locked_selectPeers, to be fixed
+     * @param ipSet ignored, should call locked_selectPeers, to be fixed
+     * @since 0.9.53 added ipSet param
      */
-    public void selectNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask) {
-        selectNotFailingPeers(howMany, exclude, matches, false, mask);
+    public void selectNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask, MaskedIPSet ipSet) {
+        selectNotFailingPeers(howMany, exclude, matches, false, mask, ipSet);
     }
 
     public void selectNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, boolean onlyNotFailing) {
-        selectNotFailingPeers(howMany, exclude, matches, onlyNotFailing, 0);
+        selectNotFailingPeers(howMany, exclude, matches, onlyNotFailing, 0, null);
     }
 
     /**
@@ -608,8 +619,11 @@ public class ProfileOrganizer {
      * @param matches set to store the matches in
      * @param onlyNotFailing if true, don't include any high capacity peers
      * @param mask ignored, should call locked_selectPeers, to be fixed
+     * @param ipSet ignored, should call locked_selectPeers, to be fixed
+     * @since 0.9.53 added ipSet param
      */
-    public void selectNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, boolean onlyNotFailing, int mask) {
+    public void selectNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, boolean onlyNotFailing,
+                                      int mask, MaskedIPSet ipSet) {
         if (matches.size() < howMany)
             selectAllNotFailingPeers(howMany, exclude, matches, onlyNotFailing, mask);
         return;
@@ -627,9 +641,30 @@ public class ProfileOrganizer {
      * be used when there is a good number of connected peers.
      *
      * @param exclude non-null, WARNING - side effect, all not-connected peers are added
-     * No mask parameter, to be fixed
      */
     public void selectActiveNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches) {
+        selectActiveNotFailingPeers(howMany, exclude, matches, 0, null);
+    }
+
+    /**
+     * Return a set of Hashes for peers that are both not failing and we're actively
+     * talking with.
+     *
+     * We use commSystem().isEstablished(), not profile.getIsActive(), as the
+     * NTCP idle time is now shorter than the 5 minute getIsActive() threshold,
+     * and we're using this to try and limit connections.
+     *
+     * Caution, this does NOT cascade further to non-connected peers, so it should only
+     * be used when there is a good number of connected peers.
+     *
+     * @param exclude non-null, WARNING - side effect, all not-connected peers are added
+     * @param mask 0-4 Number of bytes to match to determine if peers in the same IP range should
+     *             not be in the same tunnel. 0 = disable check; 1 = /8; 2 = /16; 3 = /24; 4 = exact IP match
+     * @param ipSet ignored, should call locked_selectPeers, to be fixed
+     * @param ipSet may be null only if mask is 0
+     * @since 0.9.53
+     */
+    public void selectActiveNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask, MaskedIPSet ipSet) {
         if (matches.size() < howMany) {
             Set<Hash> connected = _context.commSystem().getEstablished();
             getReadLock();
@@ -638,7 +673,7 @@ public class ProfileOrganizer {
                     if (!connected.contains(peer))
                         exclude.add(peer);
                 }
-                locked_selectPeers(_notFailingPeers, howMany, exclude, matches, 0);
+                locked_selectPeers(_notFailingPeers, howMany, exclude, matches, mask, ipSet);
             } finally { releaseReadLock(); }
         }
     }
@@ -655,8 +690,10 @@ public class ProfileOrganizer {
      *
      * @param mask 0-4 Number of bytes to match to determine if peers in the same IP range should
      *             not be in the same tunnel. 0 = disable check; 1 = /8; 2 = /16; 3 = /24; 4 = exact IP match
+     * @param ipSet in/out param, use for multiple calls, may be null only if mask is 0
+     * @since 0.9.53 added ipSet param
      */
-    private void selectActiveNotFailingPeers2(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask) {
+    private void selectActiveNotFailingPeers2(int howMany, Set<Hash> exclude, Set<Hash> matches, int mask, MaskedIPSet ipSet) {
         if (matches.size() < howMany) {
             Set<Hash> connected = _context.commSystem().getEstablished();
             Map<Hash, PeerProfile> activePeers = new HashMap<Hash, PeerProfile>(connected.size());
@@ -667,13 +704,13 @@ public class ProfileOrganizer {
                     if (prof != null)
                         activePeers.put(peer, prof);
                 }
-                locked_selectPeers(activePeers, howMany, exclude, matches, mask);
+                locked_selectPeers(activePeers, howMany, exclude, matches, mask, ipSet);
             } finally { releaseReadLock(); }
         }
         if (matches.size() < howMany) {
             if (_log.shouldLog(Log.INFO))
                 _log.info("selectANFP2("+howMany+"), not enough ANFP (" + matches.size() + ") going on to notFailing");
-            selectNotFailingPeers(howMany, exclude, matches, mask);
+            selectNotFailingPeers(howMany, exclude, matches, mask, ipSet);
         } else {
             if (_log.shouldDebug())
                 _log.debug("selectANFP2("+howMany+"), found enough ANFP (" + matches.size() + ")");
@@ -690,7 +727,6 @@ public class ProfileOrganizer {
 
     /**
      * @param mask ignored, should call locked_selectPeers, to be fixed
-     *
      */
     private void selectAllNotFailingPeers(int howMany, Set<Hash> exclude, Set<Hash> matches, boolean onlyNotFailing, int mask) {
         if (matches.size() < howMany) {
@@ -1287,19 +1323,21 @@ public class ProfileOrganizer {
      *
      */
     private void locked_selectPeers(Map<Hash, PeerProfile> peers, int howMany, Set<Hash> toExclude, Set<Hash> matches) {
-        locked_selectPeers(peers, howMany, toExclude, matches, 0);
+        locked_selectPeers(peers, howMany, toExclude, matches, 0, null);
     }
 
     /**
-      *
-      * As of 0.9.24, checks for a netdb family match as well, unless mask == 0.
-      *
+     *
+     * As of 0.9.24, checks for a netdb family match as well, unless mask == 0.
+     *
      * @param mask 0-4 Number of bytes to match to determine if peers in the same IP range should
      *             not be in the same tunnel. 0 = disable check; 1 = /8; 2 = /16; 3 = /24; 4 = exact IP match
+     * @param ipSet may be null only if mask is 0
+     * @since 0.9.53 added ipSet param
      */
-    private void locked_selectPeers(Map<Hash, PeerProfile> peers, int howMany, Set<Hash> toExclude, Set<Hash> matches, int mask) {
+    private void locked_selectPeers(Map<Hash, PeerProfile> peers, int howMany, Set<Hash> toExclude, Set<Hash> matches,
+                                    int mask, MaskedIPSet ipSet) {
         List<Hash> all = new ArrayList<Hash>(peers.keySet());
-        MaskedIPSet IPSet = new MaskedIPSet(16);
         // use RandomIterator to avoid shuffling the whole thing
         for (Iterator<Hash> iter = new RandomIterator<Hash>(all); (matches.size() < howMany) && iter.hasNext(); ) {
             Hash peer = iter.next();
@@ -1311,8 +1349,8 @@ public class ProfileOrganizer {
                 continue;
             boolean ok = isSelectable(peer);
             if (ok) {
-                ok = mask <= 0 || notRestricted(peer, IPSet, mask);
-                if ((!ok) && _log.shouldLog(Log.WARN))
+                ok = mask <= 0 || notRestricted(peer, ipSet, mask);
+                if ((!ok) && _log.shouldWarn())
                     _log.warn("IP restriction prevents " + peer + " from joining " + matches);
             }
             if (ok)
@@ -1330,12 +1368,13 @@ public class ProfileOrganizer {
      *
      * @param mask is 1-4 (number of bytes to match)
      * @param IPMatches all IPs so far, modified by this routine
+     * @return true if ok, false if not
      */
-    private boolean notRestricted(Hash peer, MaskedIPSet IPSet, int mask) {
+    private boolean notRestricted(Hash peer, MaskedIPSet ipSet, int mask) {
         Set<String> peerIPs = new MaskedIPSet(_context, peer, mask);
-        if (IPSet.containsAny(peerIPs))
+        if (!ipSet.isEmpty() && ipSet.containsAny(peerIPs))
             return false;
-        IPSet.addAll(peerIPs);
+        ipSet.addAll(peerIPs);
         return true;
     }
 
@@ -1350,9 +1389,13 @@ public class ProfileOrganizer {
      *    6: return only from group 2
      *    7: return only from group 3
      *</pre>
+     * @param mask is 1-4 (number of bytes to match)
+     * @param IPMatches all IPs so far, modified by this routine
+     * @since 0.9.53 added mask/ipSet params
      */
     private void locked_selectPeers(Map<Hash, PeerProfile> peers, int howMany, Set<Hash> toExclude,
-                                    Set<Hash> matches, SessionKey randomKey, Slice subTierMode) {
+                                    Set<Hash> matches, SessionKey randomKey, Slice subTierMode,
+                                    int mask, MaskedIPSet ipSet) {
         List<Hash> all = new ArrayList<Hash>(peers.keySet());
         byte[] rk = randomKey.getData();
         // we use the first half of the random key here,
@@ -1373,6 +1416,11 @@ public class ProfileOrganizer {
             if ((subTier & subTierMode.mask) != subTierMode.val)
                 continue;
             boolean ok = isSelectable(peer);
+            if (ok) {
+                ok = mask <= 0 || notRestricted(peer, ipSet, mask);
+                if ((!ok) && _log.shouldWarn())
+                    _log.warn("IP restriction prevents " + peer + " from joining " + matches);
+            }
             if (ok)
                 matches.add(peer);
             else
diff --git a/router/java/src/net/i2p/router/tunnel/pool/ClientPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/ClientPeerSelector.java
index cb8ffdef85..7a537c5e45 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/ClientPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/ClientPeerSelector.java
@@ -13,6 +13,7 @@ import net.i2p.router.TunnelInfo;
 import net.i2p.router.TunnelManagerFacade;
 import net.i2p.router.TunnelPoolSettings;
 import static net.i2p.router.peermanager.ProfileOrganizer.Slice.*;
+import net.i2p.router.util.MaskedIPSet;
 
 /**
  * Pick peers randomly out of the fast pool, and put them into tunnels
@@ -29,7 +30,7 @@ class ClientPeerSelector extends TunnelPeerSelector {
      * Returns ENDPOINT FIRST, GATEWAY LAST!!!!
      * In: us .. closest .. middle .. IBGW
      * Out: OBGW .. middle .. closest .. us
-     * 
+     *
      * @return ordered list of Hash objects (one per peer) specifying what order
      *         they should appear in a tunnel (ENDPOINT FIRST).  This includes
      *         the local router in the list.  If there are no tunnels or peers
@@ -45,7 +46,7 @@ class ClientPeerSelector extends TunnelPeerSelector {
 
         List<Hash> rv;
         boolean isInbound = settings.isInbound();
-    
+
         if (length > 0) {
             // special cases
             boolean v6Only = isIPv6Only();
@@ -57,10 +58,12 @@ class ClientPeerSelector extends TunnelPeerSelector {
                              !ctx.commSystem().haveInboundCapacity(95);
             boolean hiddenInbound = hidden && isInbound;
             boolean hiddenOutbound = hidden && !isInbound;
+            int ipRestriction =  (ctx.getBooleanProperty("i2np.allowLocal") || length <= 1) ? 0 : settings.getIPRestriction();
+            MaskedIPSet ipSet = ipRestriction > 0 ? new MaskedIPSet(16) : null;
 
             if (shouldSelectExplicit(settings))
                 return selectExplicit(settings, length);
-        
+
             Set<Hash> exclude = getExclude(isInbound, false);
             Set<Hash> matches = new HashSet<Hash>(length);
             if (length == 1) {
@@ -70,6 +73,7 @@ class ClientPeerSelector extends TunnelPeerSelector {
                     if (moreExclude != null)
                         exclude.addAll(moreExclude);
                 }
+                // 1-hop, IP restrictions not required here
                 if (hiddenInbound) {
                     // SANFP adds all not-connected to exclude, so make a copy
                     Set<Hash> SANFPExclude = new HashSet<Hash>(exclude);
@@ -77,7 +81,7 @@ class ClientPeerSelector extends TunnelPeerSelector {
                 }
                 if (matches.isEmpty()) {
                     // ANFP does not fall back to non-connected
-                    ctx.profileOrganizer().selectFastPeers(length, exclude, matches, 0);
+                    ctx.profileOrganizer().selectFastPeers(length, exclude, matches);
                 }
                 matches.remove(ctx.routerHash());
                 rv = new ArrayList<Hash>(matches);
@@ -108,17 +112,17 @@ class ClientPeerSelector extends TunnelPeerSelector {
                     lastHopExclude = exclude;
                 }
                 if (hiddenInbound) {
-                    // IB closest hop 
+                    // IB closest hop
                     if (log.shouldInfo())
                         log.info("CPS SANFP closest IB exclude " + lastHopExclude.size());
                     // SANFP adds all not-connected to exclude, so make a copy
                     Set<Hash> SANFPExclude = new HashSet<Hash>(lastHopExclude);
-                    ctx.profileOrganizer().selectActiveNotFailingPeers(1, SANFPExclude, matches);
+                    ctx.profileOrganizer().selectActiveNotFailingPeers(1, SANFPExclude, matches, ipRestriction, ipSet);
                     if (matches.isEmpty()) {
                         if (log.shouldInfo())
                             log.info("CPS SFP closest IB exclude " + lastHopExclude.size());
                         // ANFP does not fall back to non-connected
-                        ctx.profileOrganizer().selectFastPeers(1, lastHopExclude, matches, randomKey, length == 2 ? SLICE_0_1 : SLICE_0);
+                        ctx.profileOrganizer().selectFastPeers(1, lastHopExclude, matches, randomKey, length == 2 ? SLICE_0_1 : SLICE_0, ipRestriction, ipSet);
                     }
                 } else if (hiddenOutbound) {
                     // OBEP
@@ -177,19 +181,19 @@ class ClientPeerSelector extends TunnelPeerSelector {
                             log.info("CPS SANFP OBEP exclude " + lastHopExclude.size());
                         // SANFP adds all not-connected to exclude, so make a copy
                         Set<Hash> SANFPExclude = new HashSet<Hash>(lastHopExclude);
-                        ctx.profileOrganizer().selectActiveNotFailingPeers(1, SANFPExclude, matches);
+                        ctx.profileOrganizer().selectActiveNotFailingPeers(1, SANFPExclude, matches, ipRestriction, ipSet);
                         if (matches.isEmpty()) {
                             // ANFP does not fall back to non-connected
                             if (log.shouldInfo())
                                 log.info("CPS SFP OBEP exclude " + lastHopExclude.size());
-                            ctx.profileOrganizer().selectFastPeers(1, lastHopExclude, matches, randomKey, length == 2 ? SLICE_0_1 : SLICE_0);
+                            ctx.profileOrganizer().selectFastPeers(1, lastHopExclude, matches, randomKey, length == 2 ? SLICE_0_1 : SLICE_0, ipRestriction, ipSet);
                         }
                     } else {
-                        ctx.profileOrganizer().selectFastPeers(1, lastHopExclude, matches, randomKey, length == 2 ? SLICE_0_1 : SLICE_0);
+                        ctx.profileOrganizer().selectFastPeers(1, lastHopExclude, matches, randomKey, length == 2 ? SLICE_0_1 : SLICE_0, ipRestriction, ipSet);
                     }
                 } else {
                     // TODO exclude IPv6-only at OBEP? Caught in checkTunnel() below
-                    ctx.profileOrganizer().selectFastPeers(1, lastHopExclude, matches, randomKey, length == 2 ? SLICE_0_1 : SLICE_0);
+                    ctx.profileOrganizer().selectFastPeers(1, lastHopExclude, matches, randomKey, length == 2 ? SLICE_0_1 : SLICE_0, ipRestriction, ipSet);
                 }
 
                 matches.remove(ctx.routerHash());
@@ -199,7 +203,7 @@ class ClientPeerSelector extends TunnelPeerSelector {
                 if (length > 2) {
                     // middle hop(s)
                     // group 2 or 3
-                    ctx.profileOrganizer().selectFastPeers(length - 2, exclude, matches, randomKey, SLICE_2_3);
+                    ctx.profileOrganizer().selectFastPeers(length - 2, exclude, matches, randomKey, SLICE_2_3, ipRestriction, ipSet);
                     matches.remove(ctx.routerHash());
                     if (matches.size() > 1) {
                         // order the middle peers for tunnels >= 4 hops
@@ -225,14 +229,14 @@ class ClientPeerSelector extends TunnelPeerSelector {
                     }
                 }
                 // TODO exclude IPv6-only at IBGW? Caught in checkTunnel() below
-                ctx.profileOrganizer().selectFastPeers(1, exclude, matches, randomKey, length == 2 ? SLICE_2_3 : SLICE_1);
+                ctx.profileOrganizer().selectFastPeers(1, exclude, matches, randomKey, length == 2 ? SLICE_2_3 : SLICE_1, ipRestriction, ipSet);
                 matches.remove(ctx.routerHash());
                 rv.addAll(matches);
             }
         } else {
             rv = new ArrayList<Hash>(1);
         }
-        
+
         //if (length != rv.size() && log.shouldWarn())
         //    log.warn("CPS requested " + length + " got " + rv.size() + ": " + DataHelper.toString(rv));
         //else if (log.shouldDebug())
diff --git a/router/java/src/net/i2p/router/tunnel/pool/ExploratoryPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/ExploratoryPeerSelector.java
index 866a1708bf..fbff109229 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/ExploratoryPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/ExploratoryPeerSelector.java
@@ -11,6 +11,7 @@ import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelInfo;
 import net.i2p.router.TunnelManagerFacade;
 import net.i2p.router.TunnelPoolSettings;
+import net.i2p.router.util.MaskedIPSet;
 import net.i2p.stat.Rate;
 import net.i2p.stat.RateStat;
 import net.i2p.util.Log;
@@ -31,7 +32,7 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
      * Returns ENDPOINT FIRST, GATEWAY LAST!!!!
      * In: us .. closest .. middle .. IBGW
      * Out: OBGW .. middle .. closest .. us
-     * 
+     *
      * @return ordered list of Hash objects (one per peer) specifying what order
      *         they should appear in a tunnel (ENDPOINT FIRST).  This includes
      *         the local router in the list.  If there are no tunnels or peers
@@ -40,19 +41,19 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
      */
     public List<Hash> selectPeers(TunnelPoolSettings settings) {
         int length = getLength(settings);
-        if (length < 0) { 
+        if (length < 0) {
             if (log.shouldLog(Log.DEBUG))
                 log.debug("Length requested is zero: " + settings);
             return null;
         }
-        
+
         //if (false && shouldSelectExplicit(settings)) {
         //    List<Hash> rv = selectExplicit(settings, length);
         //    if (l.shouldLog(Log.DEBUG))
         //        l.debug("Explicit peers selected: " + rv);
         //    return rv;
         //}
-        
+
         boolean isInbound = settings.isInbound();
         Set<Hash> exclude = getExclude(isInbound, true);
         exclude.add(ctx.routerHash());
@@ -70,6 +71,8 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
         boolean hiddenInbound = hidden && isInbound;
         boolean hiddenOutbound = hidden && !isInbound;
         boolean lowOutbound = nonzero && !isInbound && !ctx.commSystem().haveHighOutboundCapacity();
+        int ipRestriction =  (ctx.getBooleanProperty("i2np.allowLocal") || length <= 1) ? 0 : settings.getIPRestriction();
+        MaskedIPSet ipSet = ipRestriction > 0 ? new MaskedIPSet(16) : null;
 
 
         // closest-hop restrictions
@@ -99,21 +102,21 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
                     log.info("EPS SANFP closest " + (isInbound ? "IB" : "OB") + " exclude " + closestExclude.size());
                 // SANFP adds all not-connected to exclude, so make a copy
                 Set<Hash> SANFPExclude = new HashSet<Hash>(closestExclude);
-                ctx.profileOrganizer().selectActiveNotFailingPeers(1, SANFPExclude, closest);
+                ctx.profileOrganizer().selectActiveNotFailingPeers(1, SANFPExclude, closest, ipRestriction, ipSet);
                 if (closest.isEmpty()) {
                     // ANFP does not fall back to non-connected
                     if (log.shouldLog(Log.INFO))
                         log.info("EPS SFP closest " + (isInbound ? "IB" : "OB") + " exclude " + closestExclude.size());
-                    ctx.profileOrganizer().selectFastPeers(1, closestExclude, closest);
+                    ctx.profileOrganizer().selectFastPeers(1, closestExclude, closest, ipRestriction, ipSet);
                 }
             } else if (exploreHighCap) {
                 if (log.shouldLog(Log.INFO))
                     log.info("EPS SHCP closest " + (isInbound ? "IB" : "OB") + " exclude " + closestExclude.size());
-                ctx.profileOrganizer().selectHighCapacityPeers(1, closestExclude, closest);
+                ctx.profileOrganizer().selectHighCapacityPeers(1, closestExclude, closest, ipRestriction, ipSet);
             } else {
                 if (log.shouldLog(Log.INFO))
                     log.info("EPS SNFP closest " + (isInbound ? "IB" : "OB") + " exclude " + closestExclude.size());
-                ctx.profileOrganizer().selectNotFailingPeers(1, closestExclude, closest, false);
+                ctx.profileOrganizer().selectNotFailingPeers(1, closestExclude, closest, false, ipRestriction, ipSet);
             }
             if (!closest.isEmpty()) {
                 closestHop = closest.iterator().next();
@@ -155,12 +158,12 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
                     log.info("EPS SANFP furthest OB exclude " + exclude.size());
                 // ANFP adds all not-connected to exclude, so make a copy
                 Set<Hash> SANFPExclude = new HashSet<Hash>(exclude);
-                ctx.profileOrganizer().selectActiveNotFailingPeers(1, SANFPExclude, furthest);
+                ctx.profileOrganizer().selectActiveNotFailingPeers(1, SANFPExclude, furthest, ipRestriction, ipSet);
                 if (furthest.isEmpty()) {
                     // ANFP does not fall back to non-connected
                     if (log.shouldLog(Log.INFO))
                         log.info("EPS SFP furthest OB exclude " + exclude.size());
-                    ctx.profileOrganizer().selectFastPeers(1, exclude, furthest);
+                    ctx.profileOrganizer().selectFastPeers(1, exclude, furthest, ipRestriction, ipSet);
                 }
                 if (!furthest.isEmpty()) {
                     furthestHop = furthest.iterator().next();
@@ -179,13 +182,10 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
         HashSet<Hash> matches = new HashSet<Hash>(length);
 
         if (length > 0) {
-            //
-            // We don't honor IP Restriction here, to be fixed
-            //
             if (exploreHighCap) {
                 if (log.shouldLog(Log.INFO))
                     log.info("EPS SHCP " + length + (isInbound ? " IB" : " OB") + " exclude " + exclude.size());
-                ctx.profileOrganizer().selectHighCapacityPeers(length, exclude, matches);
+                ctx.profileOrganizer().selectHighCapacityPeers(length, exclude, matches, ipRestriction, ipSet);
             } else {
                 // As of 0.9.23, we include a max of 2 not failing peers,
                 // to improve build success on 3-hop tunnels.
@@ -194,7 +194,7 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
                     ctx.profileOrganizer().selectHighCapacityPeers(length - 2, exclude, matches);
                 if (log.shouldLog(Log.INFO))
                     log.info("EPS SNFP " + length + (isInbound ? " IB" : " OB") + " exclude " + exclude.size());
-                ctx.profileOrganizer().selectNotFailingPeers(length, exclude, matches, false);
+                ctx.profileOrganizer().selectNotFailingPeers(length, exclude, matches, false, ipRestriction, ipSet);
             }
             matches.remove(ctx.routerHash());
         }
@@ -231,7 +231,7 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
         }
         return rv;
     }
-    
+
     private static final int MIN_NONFAILING_PCT = 15;
     private static final int MIN_ACTIVE_PEERS_STARTUP = 6;
     private static final int MIN_ACTIVE_PEERS = 12;
@@ -290,7 +290,7 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
         }
         return (failPct >= ctx.random().nextInt(100));
     }
-    
+
     /**
      * We should really use the difference between the exploratory fail rate
      * and the high capacity fail rate - but we don't have a stat for high cap,
@@ -326,11 +326,11 @@ class ExploratoryPeerSelector extends TunnelPeerSelector {
         double pct = (double)(reject + timeout) / (accept + reject + timeout);
         return (int)(100 * pct);
     }
-    
+
     /** Use current + last to get more recent and smoother data */
     private int getEvents(String stat, long period) {
         RateStat rs = ctx.statManager().getRate(stat);
-        if (rs == null) 
+        if (rs == null)
             return 0;
         Rate r = rs.getRate(period);
         if (r == null)
diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
index 76519d8f04..969acde503 100644
--- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
+++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
@@ -32,7 +32,7 @@ import net.i2p.util.SystemVersion;
 import net.i2p.util.VersionComparator;
 
 /**
- * Coordinate the selection of peers to go into a tunnel for one particular 
+ * Coordinate the selection of peers to go into a tunnel for one particular
  * pool.
  *
  */
@@ -45,8 +45,8 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
     }
 
     /**
-     * Which peers should go into the next tunnel for the given settings?  
-     * 
+     * Which peers should go into the next tunnel for the given settings?
+     *
      * @return ordered list of Hash objects (one per peer) specifying what order
      *         they should appear in a tunnel (ENDPOINT FIRST).  This includes
      *         the local router in the list.  If there are no tunnels or peers
@@ -54,7 +54,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
      *         return null.
      */
     public abstract List<Hash> selectPeers(TunnelPoolSettings settings);
-    
+
     /**
      *  @return randomized number of hops 0-7, not including ourselves
      */
@@ -81,7 +81,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
         else if (length > 7) // as documented in tunnel.html
             length = 7;
         /*
-        if ( (ctx.tunnelManager().getOutboundTunnelCount() <= 0) || 
+        if ( (ctx.tunnelManager().getOutboundTunnelCount() <= 0) ||
              (ctx.tunnelManager().getFreeTunnelCount() <= 0) ) {
             Log log = ctx.logManager().getLog(TunnelPeerSelector.class);
             // no tunnels to build tunnels with
@@ -98,7 +98,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
         */
         return length;
     }
-    
+
     /**
      *  For debugging, also possibly for restricted routes?
      *  Needs analysis and testing
@@ -118,7 +118,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
             return true;
         return false;
     }
-    
+
     /**
      *  For debugging, also possibly for restricted routes?
      *  Needs analysis and testing
@@ -128,10 +128,10 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
         String peers = null;
         Properties opts = settings.getUnknownOptions();
         peers = opts.getProperty("explicitPeers");
-        
+
         if (peers == null)
             peers = ctx.getProperty("explicitPeers");
-        
+
         List<Hash> rv = new ArrayList<Hash>();
         StringTokenizer tok = new StringTokenizer(peers, ",");
         while (tok.hasMoreTokens()) {
@@ -139,7 +139,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
             Hash peer = new Hash();
             try {
                 peer.fromBase64(peerStr);
-                
+
                 if (ctx.profileOrganizer().isSelectable(peer)) {
                     rv.add(peer);
                 } else {
@@ -150,14 +150,14 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
                     log.error("Explicit peer is improperly formatted (" + peerStr + ")", dfe);
             }
         }
-        
+
         int sz = rv.size();
         if (sz == 0) {
             log.logAlways(Log.WARN, "No valid explicit peers found, building zero hop");
         } else if (sz > 1) {
             Collections.shuffle(rv, ctx.random());
         }
-        
+
         while (rv.size() > length) {
             rv.remove(0);
         }
@@ -166,11 +166,12 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
             Set<Hash> exclude = getExclude(settings.isInbound(), settings.isExploratory());
             exclude.addAll(rv);
             Set<Hash> matches = new HashSet<Hash>(more);
-            ctx.profileOrganizer().selectFastPeers(more, exclude, matches, 0);
+            // don't bother with IP restrictions here
+            ctx.profileOrganizer().selectFastPeers(more, exclude, matches);
             rv.addAll(matches);
             Collections.shuffle(rv, ctx.random());
         }
-        
+
         if (log.shouldLog(Log.INFO)) {
             StringBuilder buf = new StringBuilder();
             if (settings.getDestinationNickname() != null)
@@ -187,16 +188,16 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
             buf.append(", out of ").append(sz).append(" (not including self)");
             log.info(buf.toString());
         }
-        
+
         if (settings.isInbound())
             rv.add(0, ctx.routerHash());
         else
             rv.add(ctx.routerHash());
-        
+
         return rv;
     }
-    
-    /** 
+
+    /**
      * Pick peers that we want to avoid
      */
     public Set<Hash> getExclude(boolean isInbound, boolean isExploratory) {
@@ -267,7 +268,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
                             peers.add(peer.getIdentity().calculateHash());
                         // otherwise, it contains flags we aren't trying to focus on,
                         // so don't exclude it based on published capacity
-                        
+
                         if (filterUptime(ctx, isInbound, isExploratory)) {
                             Properties opts = peer.getOptions();
                             if (opts != null) {
@@ -298,7 +299,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
                                     peers.add(peer.getIdentity().calculateHash());
                                     continue;
                                 }
-                                
+
                                 long infoAge = ctx.clock().now() - peer.getPublished();
                                 if (infoAge < 0) {
                                     infoAge = 0;
@@ -387,8 +388,8 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
             return false;
         return canConnect(ANY_V4, ri);
     }
-    
-    /** 
+
+    /**
      *  Pick peers that we want to avoid for the first OB hop or last IB hop.
      *  There's several cases of importance:
      *  <ol><li>Inbound and we are hidden -
@@ -452,19 +453,19 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
         }
         return rv;
     }
-    
+
     /** warning, this is also called by ProfileOrganizer.isSelectable() */
     public static boolean shouldExclude(RouterContext ctx, RouterInfo peer) {
         return shouldExclude(peer, getExcludeCaps(ctx));
     }
-    
+
     /**
      *  @return non-null, possibly empty
      */
     private static String getExcludeCaps(RouterContext ctx) {
         return ctx.getProperty("router.excludePeerCaps", DEFAULT_EXCLUDE_CAPS);
     }
-    
+
     /** NTCP2 */
     private static final String MIN_VERSION = "0.9.36";
 
@@ -555,18 +556,18 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
       ******/
         return false;
     }
-    
+
     private static final String PROP_OUTBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = "router.outboundExploratoryExcludeUnreachable";
     private static final String PROP_OUTBOUND_CLIENT_EXCLUDE_UNREACHABLE = "router.outboundClientExcludeUnreachable";
     private static final String PROP_INBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = "router.inboundExploratoryExcludeUnreachable";
     private static final String PROP_INBOUND_CLIENT_EXCLUDE_UNREACHABLE = "router.inboundClientExcludeUnreachable";
-    
+
     private static final boolean DEFAULT_OUTBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = false;
     private static final boolean DEFAULT_OUTBOUND_CLIENT_EXCLUDE_UNREACHABLE = false;
     // see comments at getExclude() above
     private static final boolean DEFAULT_INBOUND_EXPLORATORY_EXCLUDE_UNREACHABLE = false;
     private static final boolean DEFAULT_INBOUND_CLIENT_EXCLUDE_UNREACHABLE = false;
-    
+
     /**
      * do we want to skip unreachable peers?
      * @return true if yes
@@ -587,18 +588,18 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
                 if (ctx.router().isHidden())
                     return true;
                 return ctx.getProperty(PROP_INBOUND_CLIENT_EXCLUDE_UNREACHABLE, DEFAULT_INBOUND_CLIENT_EXCLUDE_UNREACHABLE);
-            } else { 
+            } else {
                 return ctx.getProperty(PROP_OUTBOUND_CLIENT_EXCLUDE_UNREACHABLE, DEFAULT_OUTBOUND_CLIENT_EXCLUDE_UNREACHABLE);
             }
         }
     }
 
-    
+
     private static final String PROP_OUTBOUND_EXPLORATORY_EXCLUDE_SLOW = "router.outboundExploratoryExcludeSlow";
     private static final String PROP_OUTBOUND_CLIENT_EXCLUDE_SLOW = "router.outboundClientExcludeSlow";
     private static final String PROP_INBOUND_EXPLORATORY_EXCLUDE_SLOW = "router.inboundExploratoryExcludeSlow";
     private static final String PROP_INBOUND_CLIENT_EXCLUDE_SLOW = "router.inboundClientExcludeSlow";
-    
+
     /**
      * do we want to skip peers that are slow?
      * @return true unless configured otherwise
@@ -612,18 +613,18 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
         } else {
             if (isInbound)
                 return ctx.getProperty(PROP_INBOUND_CLIENT_EXCLUDE_SLOW, true);
-            else 
+            else
                 return ctx.getProperty(PROP_OUTBOUND_CLIENT_EXCLUDE_SLOW, true);
-        }        
+        }
     }
-    
+
 /****
     private static final String PROP_OUTBOUND_EXPLORATORY_EXCLUDE_UPTIME = "router.outboundExploratoryExcludeUptime";
     private static final String PROP_OUTBOUND_CLIENT_EXCLUDE_UPTIME = "router.outboundClientExcludeUptime";
     private static final String PROP_INBOUND_EXPLORATORY_EXCLUDE_UPTIME = "router.inboundExploratoryExcludeUptime";
     private static final String PROP_INBOUND_CLIENT_EXCLUDE_UPTIME = "router.inboundClientExcludeUptime";
 ****/
-    
+
     /**
      * do we want to skip peers who haven't been up for long?
      * @return true unless configured otherwise
@@ -638,7 +639,7 @@ public abstract class TunnelPeerSelector extends ConnectChecker {
         } else {
             if (isInbound)
                 return ctx.getProperty(PROP_INBOUND_CLIENT_EXCLUDE_UPTIME, true);
-            else 
+            else
                 return ctx.getProperty(PROP_OUTBOUND_CLIENT_EXCLUDE_UPTIME, true);
         }
     }
-- 
GitLab