From 25d16b13f5d3417f06cbd1e7b7442b647248125c Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Sat, 6 Jan 2018 20:19:55 +0000
Subject: [PATCH] i2ptunnel: Advanced config in/out tunnels separately

---
 .../net/i2p/i2ptunnel/ui/GeneralHelper.java   | 24 +++++
 .../net/i2p/i2ptunnel/ui/TunnelConfig.java    | 69 +++++++++++--
 .../src/net/i2p/i2ptunnel/web/EditBean.java   | 37 ++++++-
 .../src/net/i2p/i2ptunnel/web/IndexBean.java  | 65 +++++++++++-
 apps/i2ptunnel/jsp/editClient.jsp             |  2 +-
 apps/i2ptunnel/jsp/editServer.jsp             | 98 ++++++++++++++++++-
 .../themes/console/classic/i2ptunnel.css      |  9 +-
 .../themes/console/dark/i2ptunnel.css         |  8 +-
 .../themes/console/light/i2ptunnel.css        |  5 +-
 .../themes/console/midnight/i2ptunnel.css     |  8 +-
 10 files changed, 300 insertions(+), 25 deletions(-)

diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java
index 2a684c7e40..aee75e0215 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java
@@ -429,22 +429,46 @@ public class GeneralHelper {
         return getProperty(tunnel, "i2p.streaming.maxWindowSize", 128) == 16;
     }
 
+    /** Inbound or both in/out */
     public int getTunnelDepth(int tunnel, int defaultLength) {
         return getProperty(tunnel, "inbound.length", defaultLength);
     }
 
+    /** Inbound or both in/out */
     public int getTunnelQuantity(int tunnel, int defaultQuantity) {
         return getProperty(tunnel, "inbound.quantity", defaultQuantity);
     }
 
+    /** Inbound or both in/out */
     public int getTunnelBackupQuantity(int tunnel, int defaultBackupQuantity) {
         return getProperty(tunnel, "inbound.backupQuantity", defaultBackupQuantity);
     }
 
+    /** Inbound or both in/out */
     public int getTunnelVariance(int tunnel, int defaultVariance) {
         return getProperty(tunnel, "inbound.lengthVariance", defaultVariance);
     }
 
+    /** @since 0.9.33 */
+    public int getTunnelDepthOut(int tunnel, int defaultLength) {
+        return getProperty(tunnel, "outbound.length", defaultLength);
+    }
+
+    /** @since 0.9.33 */
+    public int getTunnelQuantityOut(int tunnel, int defaultQuantity) {
+        return getProperty(tunnel, "outbound.quantity", defaultQuantity);
+    }
+
+    /** @since 0.9.33 */
+    public int getTunnelBackupQuantityOut(int tunnel, int defaultBackupQuantity) {
+        return getProperty(tunnel, "outbound.backupQuantity", defaultBackupQuantity);
+    }
+
+    /** @since 0.9.33 */
+    public int getTunnelVarianceOut(int tunnel, int defaultVariance) {
+        return getProperty(tunnel, "outbound.lengthVariance", defaultVariance);
+    }
+
     public boolean getReduceOnIdle(int tunnel, boolean def) {
         return getBooleanProperty(tunnel, "i2cp.reduceOnIdle", def);
     }
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java
index d381939a11..e53a28ce56 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/TunnelConfig.java
@@ -50,6 +50,10 @@ public class TunnelConfig {
     // -2 or higher is valid
     private int _tunnelVariance = -3;
     private int _tunnelBackupQuantity = -1;
+    private int _tunnelDepthOut = -1;
+    private int _tunnelQuantityOut = -1;
+    private int _tunnelVarianceOut = -3;
+    private int _tunnelBackupQuantityOut = -1;
     private boolean _connectDelay;
     private String _customOptions;
     private String _proxyList;
@@ -104,22 +108,63 @@ public class TunnelConfig {
     public void setClientPort(String port) {
         _i2cpPort = (port != null ? port.trim() : null);
     }
-    /** how many hops to use for inbound tunnels */
+
+    /** how many hops to use for inbound tunnels
+     *  In or both in/out
+     */
     public void setTunnelDepth(int tunnelDepth) { 
         _tunnelDepth = tunnelDepth;
     }
-    /** how many parallel inbound tunnels to use */
+
+    /** how many parallel inbound tunnels to use
+     *  In or both in/out
+     */
     public void setTunnelQuantity(int tunnelQuantity) { 
         _tunnelQuantity = tunnelQuantity;
     }
-    /** how much randomisation to apply to the depth of tunnels */
+
+    /** how much randomisation to apply to the depth of tunnels
+     *  In or both in/out
+     */
     public void setTunnelVariance(int tunnelVariance) { 
         _tunnelVariance = tunnelVariance;
     }
-    /** how many tunnels to hold in reserve to guard against failures */
+
+    /** how many tunnels to hold in reserve to guard against failures
+     *  In or both in/out
+     */
     public void setTunnelBackupQuantity(int tunnelBackupQuantity) { 
         _tunnelBackupQuantity = tunnelBackupQuantity;
     }
+
+    /** how many hops to use for outbound tunnels
+     *  @since 0.9.33
+     */
+    public void setTunnelDepthOut(int tunnelDepth) { 
+        _tunnelDepthOut = tunnelDepth;
+    }
+
+    /** how many parallel outbound tunnels to use
+     *  @since 0.9.33
+     */
+    public void setTunnelQuantityOut(int tunnelQuantity) { 
+        _tunnelQuantityOut = tunnelQuantity;
+    }
+
+    /** how much randomisation to apply to the depth of tunnels
+     *  @since 0.9.33
+     */
+    public void setTunnelVarianceOut(int tunnelVariance) { 
+        _tunnelVarianceOut = tunnelVariance;
+    }
+
+    /** how many tunnels to hold in reserve to guard against failures
+     *  @since 0.9.33
+     */
+    public void setTunnelBackupQuantityOut(int tunnelBackupQuantity) { 
+        _tunnelBackupQuantityOut = tunnelBackupQuantity;
+    }
+
     /** what I2P session overrides should be used */
     public void setCustomOptions(String customOptions) { 
         _customOptions = (customOptions != null ? customOptions.trim() : null);
@@ -840,19 +885,27 @@ public class TunnelConfig {
     public void updateTunnelQuantities(Properties config) {
         if (_tunnelQuantity >= 0) {
             config.setProperty("option.inbound.quantity", Integer.toString(_tunnelQuantity));
-            config.setProperty("option.outbound.quantity", Integer.toString(_tunnelQuantity));
+            if (_tunnelQuantityOut < 0)
+                _tunnelQuantityOut = _tunnelQuantity;
+            config.setProperty("option.outbound.quantity", Integer.toString(_tunnelQuantityOut));
         }
         if (_tunnelDepth >= 0) {
             config.setProperty("option.inbound.length", Integer.toString(_tunnelDepth));
-            config.setProperty("option.outbound.length", Integer.toString(_tunnelDepth));
+            if (_tunnelDepthOut < 0)
+                _tunnelDepthOut = _tunnelDepth;
+            config.setProperty("option.outbound.length", Integer.toString(_tunnelDepthOut));
         }
         if (_tunnelVariance >= -2) {
             config.setProperty("option.inbound.lengthVariance", Integer.toString(_tunnelVariance));
-            config.setProperty("option.outbound.lengthVariance", Integer.toString(_tunnelVariance));
+            if (_tunnelVarianceOut < -2)
+                _tunnelVarianceOut = _tunnelVariance;
+            config.setProperty("option.outbound.lengthVariance", Integer.toString(_tunnelVarianceOut));
         }
         if (_tunnelBackupQuantity >= 0) {
             config.setProperty("option.inbound.backupQuantity", Integer.toString(_tunnelBackupQuantity));
-            config.setProperty("option.outbound.backupQuantity", Integer.toString(_tunnelBackupQuantity));
+            if (_tunnelBackupQuantityOut < 0)
+                _tunnelBackupQuantityOut = _tunnelBackupQuantity;
+            config.setProperty("option.outbound.backupQuantity", Integer.toString(_tunnelBackupQuantityOut));
         }
     }
 }
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
index 29daf558a9..6e63732260 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
@@ -142,22 +142,46 @@ public class EditBean extends IndexBean {
         return _helper.isInteractive(tunnel);
     }
     
+    /** in or both in/out */
     public int getTunnelDepth(int tunnel, int defaultLength) {
         return _helper.getTunnelDepth(tunnel, defaultLength);
     }
     
+    /** in or both in/out */
     public int getTunnelQuantity(int tunnel, int defaultQuantity) {
         return _helper.getTunnelQuantity(tunnel, defaultQuantity);
     }
    
+    /** in or both in/out */
     public int getTunnelBackupQuantity(int tunnel, int defaultBackupQuantity) {
         return _helper.getTunnelBackupQuantity(tunnel, defaultBackupQuantity);
     }
   
+    /** in or both in/out */
     public int getTunnelVariance(int tunnel, int defaultVariance) {
         return _helper.getTunnelVariance(tunnel, defaultVariance);
     }
     
+    /** @since 0.9.33 */
+    public int getTunnelDepthOut(int tunnel, int defaultLength) {
+        return _helper.getTunnelDepthOut(tunnel, defaultLength);
+    }
+    
+    /** @since 0.9.33 */
+    public int getTunnelQuantityOut(int tunnel, int defaultQuantity) {
+        return _helper.getTunnelQuantityOut(tunnel, defaultQuantity);
+    }
+   
+    /** @since 0.9.33 */
+    public int getTunnelBackupQuantityOut(int tunnel, int defaultBackupQuantity) {
+        return _helper.getTunnelBackupQuantityOut(tunnel, defaultBackupQuantity);
+    }
+  
+    /** @since 0.9.33 */
+    public int getTunnelVarianceOut(int tunnel, int defaultVariance) {
+        return _helper.getTunnelVarianceOut(tunnel, defaultVariance);
+    }
+    
     public boolean getReduce(int tunnel) {
         return _helper.getReduceOnIdle(tunnel, false);
     }
@@ -426,10 +450,12 @@ public class EditBean extends IndexBean {
     private static final int MAX_ADVANCED_QUANTITY = 16;
 
     /**
+     *  @param mode 0=both, 1=in, 2=out
      *  @since 0.9.7
      */
-    public String getQuantityOptions(int tunnel) {
-        int tunnelQuantity = getTunnelQuantity(tunnel, DFLT_QUANTITY);
+    public String getQuantityOptions(int tunnel, int mode) {
+        int tunnelQuantity = mode == 2 ? getTunnelQuantityOut(tunnel, DFLT_QUANTITY)
+                                       : getTunnelQuantity(tunnel, DFLT_QUANTITY);
         boolean advanced = _context.getBooleanProperty(PROP_ADVANCED);
         int maxQuantity = advanced ? MAX_ADVANCED_QUANTITY :
                                      (isClient(tunnel) ? MAX_CLIENT_QUANTITY : MAX_SERVER_QUANTITY);
@@ -441,7 +467,12 @@ public class EditBean extends IndexBean {
              if (i == tunnelQuantity)
                  buf.append(" selected=\"selected\"");
              buf.append('>');
-             buf.append(ngettext("{0} inbound, {0} outbound tunnel", "{0} inbound, {0} outbound tunnels", i));
+             if (mode == 1)
+                 buf.append(ngettext("{0} inbound tunnel", "{0} inbound tunnels", i));
+             else if (mode == 2)
+                 buf.append(ngettext("{0} outbound tunnel", "{0} outbound tunnels", i));
+             else
+                 buf.append(ngettext("{0} inbound, {0} outbound tunnel", "{0} inbound, {0} outbound tunnels", i));
              if (i <= 3) {
                  buf.append(" (");
                  if (i == 1)
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
index 30507cb1ac..02ad183e7f 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
@@ -621,7 +621,10 @@ public class IndexBean {
     public void setClientport(String port) {
         _config.setClientPort(port);
     }
-    /** how many hops to use for inbound tunnels */
+
+    /** how many hops to use for inbound tunnels
+     *  In or both in/out
+     */
     public void setTunnelDepth(String tunnelDepth) {
         if (tunnelDepth != null) {
             try {
@@ -629,7 +632,10 @@ public class IndexBean {
             } catch (NumberFormatException nfe) {}
         }
     }
-    /** how many parallel inbound tunnels to use */
+
+    /** how many parallel inbound tunnels to use
+     *  In or both in/out
+     */
     public void setTunnelQuantity(String tunnelQuantity) {
         if (tunnelQuantity != null) {
             try {
@@ -637,7 +643,10 @@ public class IndexBean {
             } catch (NumberFormatException nfe) {}
         }
     }
-    /** how much randomisation to apply to the depth of tunnels */
+
+    /** how much randomisation to apply to the depth of tunnels
+     *  In or both in/out
+     */
     public void setTunnelVariance(String tunnelVariance) {
         if (tunnelVariance != null) {
             try {
@@ -645,7 +654,10 @@ public class IndexBean {
             } catch (NumberFormatException nfe) {}
         }
     }
-    /** how many tunnels to hold in reserve to guard against failures */
+
+    /** how many tunnels to hold in reserve to guard against failures
+     *  In or both in/out
+     */
     public void setTunnelBackupQuantity(String tunnelBackupQuantity) {
         if (tunnelBackupQuantity != null) {
             try {
@@ -653,6 +665,51 @@ public class IndexBean {
             } catch (NumberFormatException nfe) {}
         }
     }
+
+    /** how many hops to use for inbound tunnels
+     *  @since 0.9.33
+     */
+    public void setTunnelDepthOut(String tunnelDepth) {
+        if (tunnelDepth != null) {
+            try {
+                _config.setTunnelDepthOut(Integer.parseInt(tunnelDepth.trim()));
+            } catch (NumberFormatException nfe) {}
+        }
+    }
+
+    /** how many parallel inbound tunnels to use
+     *  @since 0.9.33
+     */
+    public void setTunnelQuantityOut(String tunnelQuantity) {
+        if (tunnelQuantity != null) {
+            try {
+                _config.setTunnelQuantityOut(Integer.parseInt(tunnelQuantity.trim()));
+            } catch (NumberFormatException nfe) {}
+        }
+    }
+
+    /** how much randomisation to apply to the depth of tunnels
+     *  @since 0.9.33
+     */
+    public void setTunnelVarianceOut(String tunnelVariance) {
+        if (tunnelVariance != null) {
+            try {
+                _config.setTunnelVarianceOut(Integer.parseInt(tunnelVariance.trim()));
+            } catch (NumberFormatException nfe) {}
+        }
+    }
+
+    /** how many tunnels to hold in reserve to guard against failures
+     *  @since 0.9.33
+     */
+    public void setTunnelBackupQuantityOut(String tunnelBackupQuantity) {
+        if (tunnelBackupQuantity != null) {
+            try {
+                _config.setTunnelBackupQuantityOut(Integer.parseInt(tunnelBackupQuantity.trim()));
+            } catch (NumberFormatException nfe) {}
+        }
+    }
+
     /** what I2P session overrides should be used */
     public void setNofilter_customOptions(String customOptions) { 
         _config.setCustomOptions(customOptions);
diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp
index d176b1e72a..a0f0d40d11 100644
--- a/apps/i2ptunnel/jsp/editClient.jsp
+++ b/apps/i2ptunnel/jsp/editClient.jsp
@@ -369,7 +369,7 @@ input.default { width: 1px; height: 1px; visibility: hidden; }
         <tr>
             <td>
                 <select id="tunnelQuantity" name="tunnelQuantity" title="<%=intl._t("Number of Tunnels in Group")%>" class="selectbox">
-                    <%=editBean.getQuantityOptions(curTunnel)%>
+                    <%=editBean.getQuantityOptions(curTunnel, 0)%>
                 </select>
             </td>
 
diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp
index 7d55560617..7a39d7555c 100644
--- a/apps/i2ptunnel/jsp/editServer.jsp
+++ b/apps/i2ptunnel/jsp/editServer.jsp
@@ -310,6 +310,15 @@ input.default { width: 1px; height: 1px; visibility: hidden; }
             </th>
         </tr>
 
+    <% if (editBean.isAdvanced()) {
+      %><tr>
+            <th colspan="2">
+                <%=intl._t("Inbound")%>
+            </th>
+        </tr><%
+      }  // isAdvanced()
+     %>
+
         <tr>
             <td>
                 <b><%=intl._t("Length")%></b>
@@ -367,24 +376,109 @@ input.default { width: 1px; height: 1px; visibility: hidden; }
         <tr>
             <td>
                 <select id="tunnelQuantity" name="tunnelQuantity" title="<%=intl._t("Number of Tunnels in Group")%>" class="selectbox">
-                    <%=editBean.getQuantityOptions(curTunnel)%>
+                    <%=editBean.getQuantityOptions(curTunnel, editBean.isAdvanced() ? 1 : 0)%>
                 </select>
             </td>
 
             <td>
                 <select id="tunnelBackupQuantity" name="tunnelBackupQuantity" title="<%=intl._t("Number of Reserve Tunnels")%>" class="selectbox">
                     <% int tunnelBackupQuantity = editBean.getTunnelBackupQuantity(curTunnel, 0);
+                   if (editBean.isAdvanced()) {
+                       // TODO ngettext
+                  %><option value="0"<%=(tunnelBackupQuantity == 0 ? " selected=\"selected\"" : "") %>>0 <%=intl._t("backup tunnels")%></option>
+                    <option value="1"<%=(tunnelBackupQuantity == 1 ? " selected=\"selected\"" : "") %>>1 <%=intl._t("backup tunnels")%></option>
+                    <option value="2"<%=(tunnelBackupQuantity == 2 ? " selected=\"selected\"" : "") %>>2 <%=intl._t("backup tunnels")%></option>
+                    <option value="3"<%=(tunnelBackupQuantity == 3 ? " selected=\"selected\"" : "") %>>3 <%=intl._t("backup tunnels")%></option>
+                 <%
+                   } else {
                   %><option value="0"<%=(tunnelBackupQuantity == 0 ? " selected=\"selected\"" : "") %>><%=intl._t("0 backup tunnels (0 redundancy, no added resource usage)")%></option>
                     <option value="1"<%=(tunnelBackupQuantity == 1 ? " selected=\"selected\"" : "") %>><%=intl._t("1 backup tunnel each direction (low redundancy, low resource usage)")%></option>
                     <option value="2"<%=(tunnelBackupQuantity == 2 ? " selected=\"selected\"" : "") %>><%=intl._t("2 backup tunnels each direction (medium redundancy, medium resource usage)")%></option>
                     <option value="3"<%=(tunnelBackupQuantity == 3 ? " selected=\"selected\"" : "") %>><%=intl._t("3 backup tunnels each direction (high redundancy, high resource usage)")%></option>
-                <% if (tunnelBackupQuantity > 3) {
+                <% } // isAdvanced()
+                   if (tunnelBackupQuantity > 3) {
                 %>    <option value="<%=tunnelBackupQuantity%>" selected="selected"><%=tunnelBackupQuantity%> <%=intl._t("backup tunnels")%></option>
                 <% }
               %></select>
             </td>
         </tr>
 
+    <% if (editBean.isAdvanced()) {
+       // repeat four options above for outbound
+      %><tr>
+            <th colspan="2">
+                <%=intl._t("Outbound")%>
+            </th>
+        </tr>
+        <tr>
+            <td>
+                <b><%=intl._t("Length")%></b>
+            </td>
+            <td>
+                <b><%=intl._t("Variance")%></b>
+            </td>
+        </tr>
+        <tr>
+            <td>
+                <select id="tunnelDepthOut" name="tunnelDepthOut" title="<%=intl._t("Length of each Tunnel")%>" class="selectbox">
+                    <% int tunnelDepthOut = editBean.getTunnelDepthOut(curTunnel, 3);
+                  %><option value="0"<%=(tunnelDepthOut == 0 ? " selected=\"selected\"" : "") %>><%=intl._t("0 hop tunnel (no anonymity)")%></option>
+                    <option value="1"<%=(tunnelDepthOut == 1 ? " selected=\"selected\"" : "") %>><%=intl._t("1 hop tunnel (low anonymity)")%></option>
+                    <option value="2"<%=(tunnelDepthOut == 2 ? " selected=\"selected\"" : "") %>><%=intl._t("2 hop tunnel (medium anonymity)")%></option>
+                    <option value="3"<%=(tunnelDepthOut == 3 ? " selected=\"selected\"" : "") %>><%=intl._t("3 hop tunnel (high anonymity)")%></option>
+                    <option value="4"<%=(tunnelDepthOut == 4 ? " selected=\"selected\"" : "") %>>4 hop tunnel</option>
+                    <option value="5"<%=(tunnelDepthOut == 5 ? " selected=\"selected\"" : "") %>>5 hop tunnel</option>
+                    <option value="6"<%=(tunnelDepthOut == 6 ? " selected=\"selected\"" : "") %>>6 hop tunnel</option>
+                    <option value="7"<%=(tunnelDepthOut == 7 ? " selected=\"selected\"" : "") %>>7 hop tunnel</option>
+                </select>
+            </td>
+            <td>
+                <select id="tunnelVarianceOut" name="tunnelVarianceOut" title="<%=intl._t("Level of Randomization for Tunnel Depth")%>" class="selectbox">
+                    <% int tunnelVarianceOut = editBean.getTunnelVarianceOut(curTunnel, 0);
+                  %><option value="0"<%=(tunnelVarianceOut  ==  0 ? " selected=\"selected\"" : "") %>><%=intl._t("0 hop variance (no randomization, consistent performance)")%></option>
+                    <option value="1"<%=(tunnelVarianceOut  ==  1 ? " selected=\"selected\"" : "") %>><%=intl._t("+ 0-1 hop variance (medium additive randomization, subtractive performance)")%></option>
+                    <option value="2"<%=(tunnelVarianceOut  ==  2 ? " selected=\"selected\"" : "") %>><%=intl._t("+ 0-2 hop variance (high additive randomization, subtractive performance)")%></option>
+                    <option value="-1"<%=(tunnelVarianceOut == -1 ? " selected=\"selected\"" : "") %>><%=intl._t("+/- 0-1 hop variance (standard randomization, standard performance)")%></option>
+                    <option value="-2"<%=(tunnelVarianceOut == -2 ? " selected=\"selected\"" : "") %>><%=intl._t("+/- 0-2 hop variance (not recommended)")%></option>
+                <% if (tunnelVarianceOut > 2 || tunnelVarianceOut < -2) {
+                %>    <option value="<%=tunnelVarianceOut%>" selected="selected"><%= (tunnelVarianceOut > 2 ? "+ " : "+/- ") %>0-<%=tunnelVarianceOut%> <%=intl._t("hop variance")%></option>
+                <% }
+              %></select>
+            </td>
+        </tr>
+        <tr>
+            <td>
+                <b><%=intl._t("Count")%></b>
+            </td>
+
+            <td>
+                <b><%=intl._t("Backup Count")%></b>
+            </td>
+        </tr>
+        <tr>
+            <td>
+                <select id="tunnelQuantityOut" name="tunnelQuantityOut" title="<%=intl._t("Number of Tunnels in Group")%>" class="selectbox">
+                    <%=editBean.getQuantityOptions(curTunnel, 2)%>
+                </select>
+            </td>
+            <td>
+                <select id="tunnelBackupQuantityOut" name="tunnelBackupQuantityOut" title="<%=intl._t("Number of Reserve Tunnels")%>" class="selectbox">
+                    <% int tunnelBackupQuantityOut = editBean.getTunnelBackupQuantityOut(curTunnel, 0);
+                       // TODO ngettext
+                  %><option value="0"<%=(tunnelBackupQuantityOut == 0 ? " selected=\"selected\"" : "") %>>0 <%=intl._t("backup tunnels")%></option>
+                    <option value="1"<%=(tunnelBackupQuantityOut == 1 ? " selected=\"selected\"" : "") %>>1 <%=intl._t("backup tunnels")%></option>
+                    <option value="2"<%=(tunnelBackupQuantityOut == 2 ? " selected=\"selected\"" : "") %>>2 <%=intl._t("backup tunnels")%></option>
+                    <option value="3"<%=(tunnelBackupQuantityOut == 3 ? " selected=\"selected\"" : "") %>>3 <%=intl._t("backup tunnels")%></option>
+                <% if (tunnelBackupQuantityOut > 3) {
+                %>    <option value="<%=tunnelBackupQuantityOut%>" selected="selected"><%=tunnelBackupQuantityOut%> <%=intl._t("backup tunnels")%></option>
+                <% }
+              %></select>
+            </td>
+        </tr>
+    <%
+      }  // isAdvanced() End outbound config section
+     %>
+
          <% if (!"streamrserver".equals(tunnelType)) { %>
 
         <tr>
diff --git a/installer/resources/themes/console/classic/i2ptunnel.css b/installer/resources/themes/console/classic/i2ptunnel.css
index 72ab0b8255..ec7bc20179 100644
--- a/installer/resources/themes/console/classic/i2ptunnel.css
+++ b/installer/resources/themes/console/classic/i2ptunnel.css
@@ -576,6 +576,7 @@ input {
 }
 
 #tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
 #localDestination, #customOptions, #leasesetKey, #name, #description, textarea[name="accessList"] {
      width: 100% !important;
      margin: 0 !important;
@@ -865,7 +866,9 @@ input[type="file"] {
      margin: 5px !important;
 }
 
-#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity, #leasesetKey {
+#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
+#leasesetKey {
      margin: 5px !important;
      width: calc(100% - 10px) !important;
 }
@@ -907,7 +910,9 @@ td.tunnelDestination, td.tunnelDescription {
 /* responsive layout */
 
 @media screen and (max-width: 700px) {
-#leasesetKey, #tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity, #leasesetKey {
+#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
+#leasesetKey {
     min-width: 270px;
 }
 }
diff --git a/installer/resources/themes/console/dark/i2ptunnel.css b/installer/resources/themes/console/dark/i2ptunnel.css
index 256ffd3553..f1bd51b091 100644
--- a/installer/resources/themes/console/dark/i2ptunnel.css
+++ b/installer/resources/themes/console/dark/i2ptunnel.css
@@ -702,7 +702,8 @@ input {
      text-align: left !important;
 }
 
-#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity {
+#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
@@ -710,6 +711,7 @@ input {
 }
 
 #tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
 #localDestination, #customOptions, #leasesetKey, #name, #description, textarea[name="accessList"] {
      width: 100% !important;
      margin: 0 !important;
@@ -1033,7 +1035,9 @@ input[type="checkbox"], input[type="radio"] {
      margin: 5px 10px 5px 0;
 }
 
-#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity, #leasesetKey {
+#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
+#leasesetKey {
      margin: 5px !important;
      width: calc(100% - 10px) !important;
 }
diff --git a/installer/resources/themes/console/light/i2ptunnel.css b/installer/resources/themes/console/light/i2ptunnel.css
index 1c3815419d..fb48dc9eaf 100644
--- a/installer/resources/themes/console/light/i2ptunnel.css
+++ b/installer/resources/themes/console/light/i2ptunnel.css
@@ -601,6 +601,7 @@ input {
 }
 
 #tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
 #localDestination, #customOptions, #leasesetKey, #name, #description, textarea[name="accessList"] {
      width: 100% !important;
      margin: 0 !important;
@@ -897,7 +898,9 @@ input.tunnelName, input.tunnelDescriptionText, #userAgents, .freetext.tunnelDesc
      margin: 5px !important;
 }
 
-#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity, #leasesetKey {
+#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
+#leasesetKey {
      margin: 5px !important;
      width: calc(100% - 10px) !important;
 }
diff --git a/installer/resources/themes/console/midnight/i2ptunnel.css b/installer/resources/themes/console/midnight/i2ptunnel.css
index d4ab28f52b..416b012db1 100644
--- a/installer/resources/themes/console/midnight/i2ptunnel.css
+++ b/installer/resources/themes/console/midnight/i2ptunnel.css
@@ -637,7 +637,8 @@ input {
      text-align: left !important;
 }
 
-#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity {
+#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut {
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
@@ -645,6 +646,7 @@ input {
 }
 
 #tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
 #localDestination, #customOptions, #leasesetKey, #name, #description, textarea[name="accessList"] {
      width: 100% !important;
      margin: 0 !important;
@@ -955,7 +957,9 @@ input[type="checkbox"], input[type="radio"] {
      margin: 5px !important;
 }
 
-#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity, #leasesetKey {
+#tunnelDepth, #tunnelVariance, #tunnelQuantity, #tunnelBackupQuantity,
+#tunnelDepthOut, #tunnelVarianceOut, #tunnelQuantityOut, #tunnelBackupQuantityOut,
+#leasesetKey {
      margin: 5px !important;
      width: calc(100% - 10px) !important;
 }
-- 
GitLab