diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java
new file mode 100644
index 0000000000000000000000000000000000000000..246cc277fded1d518972a65b8cc1bc1a14ab5010
--- /dev/null
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/ui/GeneralHelper.java
@@ -0,0 +1,185 @@
+package net.i2p.i2ptunnel.ui;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import net.i2p.I2PAppContext;
+import net.i2p.i2ptunnel.I2PTunnelClientBase;
+import net.i2p.i2ptunnel.SSLClientUtil;
+import net.i2p.i2ptunnel.TunnelController;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+import net.i2p.i2ptunnel.web.Messages;
+import net.i2p.util.FileUtil;
+import net.i2p.util.Log;
+import net.i2p.util.SecureFile;
+
+/**
+ * General helper functions used by all UIs.
+ *
+ * @since 0.9.19
+ */
+public class GeneralHelper {
+    private static final String OPT = TunnelController.PFX_OPTION;
+
+    public static TunnelController getController(TunnelControllerGroup tcg, int tunnel) {
+        if (tunnel < 0) return null;
+        if (tcg == null) return null;
+        List<TunnelController> controllers = tcg.getControllers();
+        if (controllers.size() > tunnel)
+            return controllers.get(tunnel); 
+        else
+            return null;
+    }
+
+    public static List<String> saveTunnel(
+            I2PAppContext context, TunnelControllerGroup tcg, int tunnel, TunnelConfig config) {
+        List<String> msgs = updateTunnelConfig(tcg, tunnel, config);
+        msgs.addAll(saveConfig(context, tcg));
+        return msgs;
+    }
+
+    protected static List<String> updateTunnelConfig(TunnelControllerGroup tcg, int tunnel, TunnelConfig config) {
+        // Get current tunnel controller
+        TunnelController cur = getController(tcg, tunnel);
+
+        Properties props = config.getConfig();
+
+        List<String> msgs = new ArrayList<String>();
+        String type = props.getProperty(TunnelController.PROP_TYPE);
+        if (TunnelController.TYPE_STD_CLIENT.equals(type) || TunnelController.TYPE_IRC_CLIENT.equals(type)) {
+            //
+            // If we switch to SSL, create the keystore here, so we can store the new properties.
+            // Down in I2PTunnelClientBase it's very hard to save the config.
+            //
+            if (Boolean.parseBoolean(props.getProperty(OPT + I2PTunnelClientBase.PROP_USE_SSL))) {
+                try {
+                    boolean created = SSLClientUtil.verifyKeyStore(props, OPT);
+                    if (created) {
+                        // config now contains new keystore props
+                        msgs.add("Created new self-signed certificate for tunnel " + getTunnelName(tcg, tunnel));
+                    }        
+                } catch (IOException ioe) {       
+                    msgs.add("Failed to create new self-signed certificate for tunnel " +
+                            getTunnelName(tcg, tunnel) + ", check logs: " + ioe);
+                }        
+            }        
+        }        
+        if (cur == null) {
+            // creating new
+            cur = new TunnelController(props, "", true);
+            tcg.addController(cur);
+            if (cur.getStartOnLoad())
+                cur.startTunnelBackground();
+        } else {
+            cur.setConfig(props, "");
+        }
+        // Only modify other shared tunnels
+        // if the current tunnel is shared, and of supported type
+        if (Boolean.parseBoolean(cur.getSharedClient()) && TunnelController.isClient(cur.getType())) {
+            // all clients use the same I2CP session, and as such, use the same I2CP options
+            List<TunnelController> controllers = tcg.getControllers();
+
+            for (int i = 0; i < controllers.size(); i++) {
+                TunnelController c = controllers.get(i);
+
+                // Current tunnel modified by user, skip
+                if (c == cur) continue;
+
+                // Only modify this non-current tunnel
+                // if it belongs to a shared destination, and is of supported type
+                if (Boolean.parseBoolean(c.getSharedClient()) && TunnelController.isClient(c.getType())) {
+                    Properties cOpt = c.getConfig("");
+                    config.updateTunnelQuantities(cOpt);
+                    cOpt.setProperty("option.inbound.nickname", TunnelConfig.SHARED_CLIENT_NICKNAME);
+                    cOpt.setProperty("option.outbound.nickname", TunnelConfig.SHARED_CLIENT_NICKNAME);
+
+                    c.setConfig(cOpt, "");
+                }
+            }
+        }
+
+        return msgs;
+    }
+
+    protected static List<String> saveConfig(I2PAppContext context, TunnelControllerGroup tcg) { 
+        List<String> rv = tcg.clearAllMessages();
+        try {
+            tcg.saveConfig();
+            rv.add(0, _("Configuration changes saved", context));
+        } catch (IOException ioe) {
+            Log log = context.logManager().getLog(GeneralHelper.class);
+            log.error("Failed to save config file", ioe);
+            rv.add(0, _("Failed to save configuration", context) + ": " + ioe.toString());
+        }
+        return rv;
+    }
+
+    /**
+     *  Stop the tunnel, delete from config,
+     *  rename the private key file if in the default directory
+     */
+    public static List<String> deleteTunnel(
+            I2PAppContext context, TunnelControllerGroup tcg,int tunnel, TunnelConfig config) {
+        List<String> msgs;
+        TunnelController cur = getController(tcg, tunnel);
+        if (cur == null) {
+            msgs = new ArrayList<>();
+            msgs.add("Invalid tunnel number");
+            return msgs;
+        }
+
+        msgs = tcg.removeController(cur);
+        msgs.addAll(saveConfig(context, tcg));
+
+        // Rename private key file if it was a default name in
+        // the default directory, so it doesn't get reused when a new
+        // tunnel is created.
+        // Use configured file name if available, not the one from the form.
+        String pk = cur.getPrivKeyFile();
+        if (pk == null)
+            pk = config.getPrivKeyFile();
+        if (pk != null && pk.startsWith("i2ptunnel") && pk.endsWith("-privKeys.dat") &&
+            ((!TunnelController.isClient(cur.getType())) || cur.getPersistentClientKey())) {
+            File pkf = new File(context.getConfigDir(), pk);
+            if (pkf.exists()) {
+                String name = cur.getName();
+                if (name == null) {
+                    name = cur.getDescription();
+                    if (name == null) {
+                        name = cur.getType();
+                        if (name == null)
+                            name = Long.toString(context.clock().now());
+                    }
+                }
+                name = name.replace(' ', '_').replace(':', '_').replace("..", "_").replace('/', '_').replace('\\', '_');
+                name = "i2ptunnel-deleted-" + name + '-' + context.clock().now() + "-privkeys.dat";
+                File backupDir = new SecureFile(context.getConfigDir(), TunnelController.KEY_BACKUP_DIR);
+                File to;
+                if (backupDir.isDirectory() || backupDir.mkdir())
+                    to = new File(backupDir, name);
+                else
+                    to = new File(context.getConfigDir(), name);
+                boolean success = FileUtil.rename(pkf, to);
+                if (success)
+                    msgs.add("Private key file " + pkf.getAbsolutePath() +
+                             " renamed to " + to.getAbsolutePath());
+            }
+        }
+        return msgs;
+    }
+
+    public static String getTunnelName(TunnelControllerGroup tcg, int tunnel) {
+        TunnelController tun = getController(tcg, tunnel);
+        if (tun != null)
+            return tun.getName();
+        else
+            return null;
+    }
+
+    protected static String _(String key, I2PAppContext context) {
+        return Messages._(key, context);
+    }
+}
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 67d84fdab86428176eeb2354247efcb332eae7f6..956caa0d1a5e63b959835f4e7ecd1cdfd8d4a6e3 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
@@ -9,7 +9,6 @@ package net.i2p.i2ptunnel.web;
  */
 
 import java.io.File;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Locale;
@@ -23,19 +22,16 @@ import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.data.PrivateKeyFile;
 import net.i2p.data.SessionKey;
-import net.i2p.i2ptunnel.I2PTunnelClientBase;
 import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
 import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase;
 import net.i2p.i2ptunnel.I2PTunnelHTTPServer;
 import net.i2p.i2ptunnel.I2PTunnelServer;
-import net.i2p.i2ptunnel.SSLClientUtil;
 import net.i2p.i2ptunnel.TunnelController;
 import net.i2p.i2ptunnel.TunnelControllerGroup;
+import net.i2p.i2ptunnel.ui.GeneralHelper;
 import net.i2p.i2ptunnel.ui.TunnelConfig;
 import net.i2p.util.Addresses;
-import net.i2p.util.FileUtil;
 import net.i2p.util.Log;
-import net.i2p.util.SecureFile;
 
 /**
  * Simple accessor for exposing tunnel info, but also an ugly form handler
@@ -79,7 +75,6 @@ public class IndexBean {
     public static final String PROP_CSS_DISABLED = "routerconsole.css.disabled";
     public static final String PROP_JS_DISABLED = "routerconsole.javascript.disabled";
     private static final String PROP_PW_ENABLE = "routerconsole.auth.enable";
-    private static final String OPT = TunnelController.PFX_OPTION;
     
     public IndexBean() {
         _context = I2PAppContext.getGlobalContext();
@@ -240,84 +235,10 @@ public class IndexBean {
     }
     
     private String saveChanges() {
-        // Get current tunnel controller
-        TunnelController cur = getController(_tunnel);
-        
-        Properties config = getConfig();
-
-        String ksMsg = null;
-        String type = config.getProperty(TunnelController.PROP_TYPE);
-        if (TunnelController.TYPE_STD_CLIENT.equals(type) || TunnelController.TYPE_IRC_CLIENT.equals(type)) {
-            //
-            // If we switch to SSL, create the keystore here, so we can store the new properties.
-            // Down in I2PTunnelClientBase it's very hard to save the config.
-            //
-            if (Boolean.parseBoolean(config.getProperty(OPT + I2PTunnelClientBase.PROP_USE_SSL))) {
-                try {
-                    boolean created = SSLClientUtil.verifyKeyStore(config, OPT);
-                    if (created) {
-                        // config now contains new keystore props
-                        ksMsg = "Created new self-signed certificate for tunnel " + getTunnelName(_tunnel);
-                    }        
-                } catch (IOException ioe) {       
-                    ksMsg = "Failed to create new self-signed certificate for tunnel " +
-                            getTunnelName(_tunnel) + ", check logs: " + ioe;
-                }        
-            }        
-        }        
-        if (cur == null) {
-            // creating new
-            cur = new TunnelController(config, "", true);
-            _group.addController(cur);
-            if (cur.getStartOnLoad())
-                cur.startTunnelBackground();
-        } else {
-            cur.setConfig(config, "");
-        }
-        // Only modify other shared tunnels
-        // if the current tunnel is shared, and of supported type
-        if (Boolean.parseBoolean(cur.getSharedClient()) && isClient(cur.getType())) {
-            // all clients use the same I2CP session, and as such, use the same I2CP options
-            List<TunnelController> controllers = _group.getControllers();
-
-            for (int i = 0; i < controllers.size(); i++) {
-                TunnelController c = controllers.get(i);
-
-                // Current tunnel modified by user, skip
-                if (c == cur) continue;
-
-                // Only modify this non-current tunnel
-                // if it belongs to a shared destination, and is of supported type
-                if (Boolean.parseBoolean(c.getSharedClient()) && isClient(c.getType())) {
-                    Properties cOpt = c.getConfig("");
-                    _config.updateTunnelQuantities(config);
-                    cOpt.setProperty("option.inbound.nickname", TunnelConfig.SHARED_CLIENT_NICKNAME);
-                    cOpt.setProperty("option.outbound.nickname", TunnelConfig.SHARED_CLIENT_NICKNAME);
-                    
-                    c.setConfig(cOpt, "");
-                }
-            }
-        }
-        
-        List<String> msgs = doSave();
-        if (ksMsg != null)
-            msgs.add(ksMsg);
         // FIXME name will be HTML escaped twice
-        return getMessages(msgs);
+        return getMessages(GeneralHelper.saveTunnel(_context, _group, _tunnel, _config));
     }
 
-    private List<String> doSave() { 
-        List<String> rv = _group.clearAllMessages();
-        try {
-            _group.saveConfig();
-            rv.add(0, _("Configuration changes saved"));
-        } catch (IOException ioe) {
-            _log.error("Failed to save config file", ioe);
-            rv.add(0, _("Failed to save configuration") + ": " + ioe.toString());
-        }
-        return rv;
-    } 
-
     /**
      *  Stop the tunnel, delete from config,
      *  rename the private key file if in the default directory
@@ -325,49 +246,8 @@ public class IndexBean {
     private String deleteTunnel() {
         if (!_removeConfirmed)
             return "Please confirm removal";
-        
-        TunnelController cur = getController(_tunnel);
-        if (cur == null)
-            return "Invalid tunnel number";
-        
-        List<String> msgs = _group.removeController(cur);
-        msgs.addAll(doSave());
-
-        // Rename private key file if it was a default name in
-        // the default directory, so it doesn't get reused when a new
-        // tunnel is created.
-        // Use configured file name if available, not the one from the form.
-        String pk = cur.getPrivKeyFile();
-        if (pk == null)
-            pk = _config.getPrivKeyFile();
-        if (pk != null && pk.startsWith("i2ptunnel") && pk.endsWith("-privKeys.dat") &&
-            ((!isClient(cur.getType())) || cur.getPersistentClientKey())) {
-            File pkf = new File(_context.getConfigDir(), pk);
-            if (pkf.exists()) {
-                String name = cur.getName();
-                if (name == null) {
-                    name = cur.getDescription();
-                    if (name == null) {
-                        name = cur.getType();
-                        if (name == null)
-                            name = Long.toString(_context.clock().now());
-                    }
-                }
-                name = name.replace(' ', '_').replace(':', '_').replace("..", "_").replace('/', '_').replace('\\', '_');
-                name = "i2ptunnel-deleted-" + name + '-' + _context.clock().now() + "-privkeys.dat";
-                File backupDir = new SecureFile(_context.getConfigDir(), TunnelController.KEY_BACKUP_DIR);
-                File to;
-                if (backupDir.isDirectory() || backupDir.mkdir())
-                    to = new File(backupDir, name);
-                else
-                    to = new File(_context.getConfigDir(), name);
-                boolean success = FileUtil.rename(pkf, to);
-                if (success)
-                    msgs.add("Private key file " + pkf.getAbsolutePath() +
-                             " renamed to " + to.getAbsolutePath());
-            }
-        }
-        return getMessages(msgs);
+
+        return getMessages(GeneralHelper.deleteTunnel(_context, _group, _tunnel, _config));
     }
     
     /**
@@ -436,9 +316,9 @@ public class IndexBean {
     }
     
     public String getTunnelName(int tunnel) {
-        TunnelController tun = getController(tunnel);
-        if (tun != null && tun.getName() != null)
-            return DataHelper.escapeHTML(tun.getName());
+        String name = GeneralHelper.getTunnelName(_group, tunnel);
+        if (name != null)
+            return DataHelper.escapeHTML(name);
         else
             return _("New Tunnel");
     }
@@ -1240,13 +1120,7 @@ public class IndexBean {
     ///
     
     protected TunnelController getController(int tunnel) {
-        if (tunnel < 0) return null;
-        if (_group == null) return null;
-        List<TunnelController> controllers = _group.getControllers();
-        if (controllers.size() > tunnel)
-            return controllers.get(tunnel); 
-        else
-            return null;
+        return GeneralHelper.getController(_group, tunnel);
     }
     
     private static String getMessages(List<String> msgs) {