diff --git a/res/values/strings.xml b/res/values/strings.xml
index 81ed8b75d85256dea3f108f7bff2ae6bfa476b84..27bb49d469faed1ab836e3bb30b6d542e5e4239f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -89,6 +89,8 @@
 
     <string name="i2ptunnel_not_initialized">Tunnels are not initialized yet, please reload in two minutes.</string>
     <string name="i2ptunnel_new_tunnel">New Tunnel</string>
+    <string name="i2ptunnel_msg_config_saved">Configuration changes saved</string>
+    <string name="i2ptunnel_msg_config_save_failed">Failed to save configuration</string>
 
     <string name="i2ptunnel_wizard_k_client_server">Client or Server</string>
     <string name="i2ptunnel_wizard_v_client">Client tunnel</string>
diff --git a/src/net/i2p/android/i2ptunnel/fragment/TunnelListFragment.java b/src/net/i2p/android/i2ptunnel/fragment/TunnelListFragment.java
index 4bb2d0035a6deef124a3a64fe8ae28935ac43a63..a69d558e6341b3808bdd80a1b6909255ac2d2b07 100644
--- a/src/net/i2p/android/i2ptunnel/fragment/TunnelListFragment.java
+++ b/src/net/i2p/android/i2ptunnel/fragment/TunnelListFragment.java
@@ -6,6 +6,7 @@ import net.i2p.android.i2ptunnel.activity.TunnelWizardActivity;
 import net.i2p.android.i2ptunnel.adapter.TunnelEntryAdapter;
 import net.i2p.android.i2ptunnel.loader.TunnelEntry;
 import net.i2p.android.i2ptunnel.loader.TunnelEntryLoader;
+import net.i2p.android.i2ptunnel.util.TunnelConfig;
 import net.i2p.android.router.R;
 import net.i2p.i2ptunnel.TunnelControllerGroup;
 import android.app.Activity;
@@ -157,6 +158,9 @@ public class TunnelListFragment extends ListFragment
         if (requestCode == TUNNEL_WIZARD_REQUEST) {
             if (resultCode == Activity.RESULT_OK) {
                 Bundle tunnelData = data.getExtras().getBundle(TUNNEL_WIZARD_DATA);
+                TunnelConfig cfg = TunnelConfig.createFromWizard(getActivity(), mGroup, tunnelData);
+                TunnelEntry tunnel = TunnelEntry.createNewTunnel(getActivity(), mGroup, cfg);
+                mAdapter.add(tunnel);
             }
         }
     }
diff --git a/src/net/i2p/android/i2ptunnel/loader/TunnelEntry.java b/src/net/i2p/android/i2ptunnel/loader/TunnelEntry.java
index 9f9aa757aa845bc7066b42ce2501a40035df3dec..a0f2913fb380e690d0adf74017679bd9cb54ead8 100644
--- a/src/net/i2p/android/i2ptunnel/loader/TunnelEntry.java
+++ b/src/net/i2p/android/i2ptunnel/loader/TunnelEntry.java
@@ -1,11 +1,17 @@
 package net.i2p.android.i2ptunnel.loader;
 
+import java.util.List;
+
 import android.content.Context;
 import android.graphics.drawable.Drawable;
+import android.widget.Toast;
+import net.i2p.android.i2ptunnel.util.TunnelConfig;
+import net.i2p.android.i2ptunnel.util.TunnelUtil;
 import net.i2p.android.router.R;
 import net.i2p.data.Destination;
 import net.i2p.data.PrivateKeyFile;
 import net.i2p.i2ptunnel.TunnelController;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
 
 public class TunnelEntry {
     public static final int RUNNING = 1;
@@ -17,6 +23,20 @@ public class TunnelEntry {
     private final TunnelController mController;
     private final int mId;
 
+    public static TunnelEntry createNewTunnel(
+            Context ctx,
+            TunnelControllerGroup tcg,
+            TunnelConfig cfg) {
+        int tunnelId = tcg.getControllers().size();
+        List<String> msgs = TunnelUtil.saveTunnel(
+                ctx, tcg, -1, cfg.getConfig());
+        // TODO: Do something else with the other messages.
+        Toast.makeText(ctx.getApplicationContext(),
+                msgs.get(0), Toast.LENGTH_LONG).show();
+        TunnelController cur = TunnelUtil.getController(tcg, tunnelId);
+        return new TunnelEntry(ctx, cur, tunnelId);
+    }
+
     public TunnelEntry(Context context, TunnelController controller, int id) {
         mContext = context;
         mController = controller;
@@ -46,44 +66,7 @@ public class TunnelEntry {
     }
 
     public String getType() {
-        if ("client".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_client);
-        else if ("httpclient".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_httpclient);
-        else if ("ircclient".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_ircclient);
-        else if ("server".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_server);
-        else if ("httpserver".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_httpserver);
-        else if ("sockstunnel".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_sockstunnel);
-        else if ("socksirctunnel".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_socksirctunnel);
-        else if ("connectclient".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_connectclient);
-        else if ("ircserver".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_ircserver);
-        else if ("streamrclient".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_streamrclient);
-        else if ("streamrserver".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_streamrserver);
-        else if ("httpbidirserver".equals(mController.getType()))
-            return mContext.getResources()
-                    .getString(R.string.i2ptunnel_type_httpbidirserver);
-        else
-            return mController.getType();
+        return TunnelUtil.getTypeName(mController.getType(), mContext);
     }
 
     public String getDescription() {
@@ -108,17 +91,7 @@ public class TunnelEntry {
     }
 
     public boolean isClient() {
-        return isClient(mController.getType());
-    }
-
-    public static boolean isClient(String type) {
-        return ( ("client".equals(type)) ||
-                 ("httpclient".equals(type)) ||
-                 ("sockstunnel".equals(type)) ||
-                 ("socksirctunnel".equals(type)) ||
-                 ("connectclient".equals(type)) ||
-                 ("streamrclient".equals(type)) ||
-                 ("ircclient".equals(type)));
+        return TunnelUtil.isClient(mController.getType());
     }
 
     /* Client tunnel data */
diff --git a/src/net/i2p/android/i2ptunnel/util/TunnelConfig.java b/src/net/i2p/android/i2ptunnel/util/TunnelConfig.java
new file mode 100644
index 0000000000000000000000000000000000000000..5bbb41b367502ca16c5b8926fa4dbb121b2f231d
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/util/TunnelConfig.java
@@ -0,0 +1,668 @@
+package net.i2p.android.i2ptunnel.util;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.concurrent.ConcurrentHashMap;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.os.Bundle;
+
+import net.i2p.I2PAppContext;
+import net.i2p.android.router.R;
+import net.i2p.android.wizard.model.Page;
+import net.i2p.i2ptunnel.I2PTunnelConnectClient;
+import net.i2p.i2ptunnel.I2PTunnelHTTPClient;
+import net.i2p.i2ptunnel.I2PTunnelHTTPClientBase;
+import net.i2p.i2ptunnel.I2PTunnelIRCClient;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+import net.i2p.util.ConcurrentHashSet;
+import net.i2p.util.PasswordManager;
+
+public class TunnelConfig {
+    protected final I2PAppContext _context;
+
+    private String _type;
+    private String _name;
+    private String _description;
+    private String _i2cpHost;
+    private String _i2cpPort;
+    private String _tunnelDepth;
+    private String _tunnelQuantity;
+    private String _tunnelVariance;
+    private String _tunnelBackupQuantity;
+    private boolean _connectDelay;
+    private String _customOptions;
+    private String _proxyList;
+    private String _port;
+    private String _reachableBy;
+    private String _targetDestination;
+    private String _targetHost;
+    private String _targetPort;
+    private String _spoofedHost;
+    private String _privKeyFile;
+    private String _profile;
+    private boolean _startOnLoad;
+    private boolean _sharedClient;
+    private final Set<String> _booleanOptions;
+    private final Map<String, String> _otherOptions;
+    private String _newProxyUser;
+    private String _newProxyPW;
+
+    static final String CLIENT_NICKNAME = "shared clients";
+
+    public static TunnelConfig createFromWizard(
+            Context ctx, TunnelControllerGroup tcg, Bundle data) {
+        // Get the Bundle keys
+        Resources res = ctx.getResources();
+
+        String kClientServer = res.getString(R.string.i2ptunnel_wizard_k_client_server);
+        String kType = res.getString(R.string.i2ptunnel_wizard_k_type);
+
+        String kName = res.getString(R.string.i2ptunnel_wizard_k_name);
+        String kDesc = res.getString(R.string.i2ptunnel_wizard_k_desc);
+        String kDest = res.getString(R.string.i2ptunnel_wizard_k_dest);
+        String kOutproxies = res.getString(R.string.i2ptunnel_wizard_k_outproxies);
+        String kTargetHost = res.getString(R.string.i2ptunnel_wizard_k_target_host);
+        String kTargetPort = res.getString(R.string.i2ptunnel_wizard_k_target_port);
+        String kReachableOn = res.getString(R.string.i2ptunnel_wizard_k_reachable_on);
+        String kBindingPort = res.getString(R.string.i2ptunnel_wizard_k_binding_port);
+        String kAutoStart = res.getString(R.string.i2ptunnel_wizard_k_auto_start);
+
+        // Create the TunnelConfig
+        TunnelConfig cfg = new TunnelConfig();
+
+        // Get/set the tunnel wizard settings
+        String clientServer = data.getBundle(kClientServer).getString(Page.SIMPLE_DATA_KEY);
+        String typeName = data.getBundle(clientServer + ":" + kType).getString(Page.SIMPLE_DATA_KEY);
+        String type = TunnelUtil.getTypeFromName(typeName, ctx);
+        cfg.setType(type);
+
+        String name = data.getBundle(kName).getString(Page.SIMPLE_DATA_KEY);
+        cfg.setName(name);
+
+        String desc = data.getBundle(kDesc).getString(Page.SIMPLE_DATA_KEY);
+        cfg.setDescription(desc);
+
+        String dest = null;
+        Bundle pageData = data.getBundle(kDest);
+        if (pageData != null) dest = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setTargetDestination(dest);
+
+        String outproxies = null;
+        pageData = data.getBundle(kOutproxies);
+        if (pageData != null) outproxies = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setProxyList(outproxies);
+
+        String targetHost = null;
+        pageData = data.getBundle(kTargetHost);
+        if (pageData != null) targetHost = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setTargetHost(targetHost);
+
+        String targetPort = null;
+        pageData = data.getBundle(kTargetPort);
+        if (pageData != null) targetPort = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setTargetPort(targetPort);
+
+        String reachableOn = null;
+        pageData = data.getBundle(kReachableOn);
+        if (pageData != null) reachableOn = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setReachableBy(reachableOn);
+
+        String bindingPort = null;
+        pageData = data.getBundle(kBindingPort);
+        if (pageData != null) bindingPort = pageData.getString(Page.SIMPLE_DATA_KEY);
+        cfg.setPort(bindingPort);
+
+        boolean autoStart = data.getBundle(kAutoStart).getBoolean(Page.SIMPLE_DATA_KEY);
+        cfg.setStartOnLoad(autoStart);
+
+        // Set sensible defaults for a new tunnel
+        cfg.setTunnelDepth("3");
+        cfg.setTunnelVariance("0");
+        cfg.setTunnelQuantity("2");
+        cfg.setTunnelBackupQuantity("0");
+        cfg.setClientHost("internal");
+        cfg.setClientport("internal");
+        cfg.setCustomOptions("");
+        if (!"streamrclient".equals(type)) {
+            cfg.setProfile("bulk");
+            cfg.setReduceCount("1");
+            cfg.setReduceTime("20");
+        }
+        if (TunnelUtil.isClient(type)) { /* Client-only defaults */
+            if (!"streamrclient".equals(type)) {
+                cfg.setNewDest("0");
+                cfg.setCloseTime("30");
+            }
+            if ("httpclient".equals(type) ||
+                    "connectclient".equals(type) ||
+                    "sockstunnel".equals(type) |
+                    "socksirctunnel".equals(type)) {
+                cfg.setProxyUsername("");
+                cfg.setProxyPassword("");
+                cfg.setOutproxyUsername("");
+                cfg.setOutproxyPassword("");
+            }
+            if ("httpclient".equals(type))
+                cfg.setJumpList("http://i2host.i2p/cgi-bin/i2hostjump?\nhttp://stats.i2p/cgi-bin/jump.cgi?a=");
+        } else { /* Server-only defaults */
+            cfg.setPrivKeyFile(TunnelUtil.getPrivateKeyFile(tcg, -1));
+            cfg.setEncrypt("");
+            cfg.setEncryptKey("");
+            cfg.setAccessMode("0");
+            cfg.setAccessList("");
+            cfg.setLimitMinute("0");
+            cfg.setLimitHour("0");
+            cfg.setLimitDay("0");
+            cfg.setTotalMinute("0");
+            cfg.setTotalHour("0");
+            cfg.setTotalDay("0");
+            cfg.setMaxStreams("0");
+        }
+
+        return cfg;
+    }
+
+    public TunnelConfig() {
+        _context = I2PAppContext.getGlobalContext();
+        _booleanOptions = new ConcurrentHashSet<String>(4);
+        _otherOptions = new ConcurrentHashMap<String,String>(4);
+    }
+
+    /**
+     * What type of tunnel (httpclient, ircclient, client, or server).  This is 
+     * required when adding a new tunnel.
+     *
+     */
+    public void setType(String type) { 
+        _type = (type != null ? type.trim() : null);   
+    }
+    String getType() { return _type; }
+
+    /** Short name of the tunnel */
+    public void setName(String name) { 
+        _name = (name != null ? name.trim() : null);
+    }
+    /** one line description */
+    public void setDescription(String description) { 
+        _description = (description != null ? description.trim() : null);
+    }
+    /** I2CP host the router is on, ignored when in router context */
+    public void setClientHost(String host) {
+        _i2cpHost = (host != null ? host.trim() : null);
+    }
+    /** I2CP port the router is on, ignored when in router context */
+    public void setClientport(String port) {
+        _i2cpPort = (port != null ? port.trim() : null);
+    }
+    /** how many hops to use for inbound tunnels */
+    public void setTunnelDepth(String tunnelDepth) { 
+        _tunnelDepth = (tunnelDepth != null ? tunnelDepth.trim() : null);
+    }
+    /** how many parallel inbound tunnels to use */
+    public void setTunnelQuantity(String tunnelQuantity) { 
+        _tunnelQuantity = (tunnelQuantity != null ? tunnelQuantity.trim() : null);
+    }
+    /** how much randomisation to apply to the depth of tunnels */
+    public void setTunnelVariance(String tunnelVariance) { 
+        _tunnelVariance = (tunnelVariance != null ? tunnelVariance.trim() : null);
+    }
+    /** how many tunnels to hold in reserve to guard against failures */
+    public void setTunnelBackupQuantity(String tunnelBackupQuantity) { 
+        _tunnelBackupQuantity = (tunnelBackupQuantity != null ? tunnelBackupQuantity.trim() : null);
+    }
+    /** what I2P session overrides should be used */
+    public void setCustomOptions(String customOptions) { 
+        _customOptions = (customOptions != null ? customOptions.trim() : null);
+    }
+    /** what HTTP outproxies should be used (httpclient specific) */
+    public void setProxyList(String proxyList) { 
+        _proxyList = (proxyList != null ? proxyList.trim() : null);
+    }
+    /** what port should this client/httpclient/ircclient listen on */
+    public void setPort(String port) { 
+        _port = (port != null ? port.trim() : null);
+    }
+    /** 
+     * what interface should this client/httpclient/ircclient listen on
+     */
+    public void setReachableBy(String reachableBy) { 
+        _reachableBy = (reachableBy != null ? reachableBy.trim() : null);
+    }
+    /** What peer does this client tunnel point at */
+    public void setTargetDestination(String dest) { 
+        _targetDestination = (dest != null ? dest.trim() : null);
+    }
+    /** What host does this server tunnel point at */
+    public void setTargetHost(String host) { 
+        _targetHost = (host != null ? host.trim() : null);
+    }
+    /** What port does this server tunnel point at */
+    public void setTargetPort(String port) { 
+        _targetPort = (port != null ? port.trim() : null);
+    }
+    /** What host does this http server tunnel spoof */
+    public void setSpoofedHost(String host) { 
+        _spoofedHost = (host != null ? host.trim() : null);
+    }
+    /** What filename is this server tunnel's private keys stored in */
+    public void setPrivKeyFile(String file) { 
+        _privKeyFile = (file != null ? file.trim() : null);
+    }
+    /**
+     * If called with true, we want this tunnel to start whenever it is
+     * loaded (aka right now and whenever the router is started up)
+     */
+    public void setStartOnLoad(boolean val) {
+        _startOnLoad = val;
+    }
+    public void setShared(boolean val) {
+        _sharedClient=val;
+    }
+    public void setConnectDelay(String moo) {
+        _connectDelay = true;
+    }
+    public void setProfile(String profile) { 
+        _profile = profile; 
+    }
+
+    public void setReduce(String moo) {
+        _booleanOptions.add("i2cp.reduceOnIdle");
+    }
+    public void setClose(String moo) {
+        _booleanOptions.add("i2cp.closeOnIdle");
+    }
+    public void setEncrypt(String moo) {
+        _booleanOptions.add("i2cp.encryptLeaseSet");
+    }
+
+    /** @since 0.8.9 */
+    public void setDCC(String moo) {
+        _booleanOptions.add(I2PTunnelIRCClient.PROP_DCC);
+    }
+
+    protected static final String PROP_ENABLE_ACCESS_LIST = "i2cp.enableAccessList";
+    protected static final String PROP_ENABLE_BLACKLIST = "i2cp.enableBlackList";
+
+    public void setAccessMode(String val) {
+        if ("1".equals(val))
+            _booleanOptions.add(PROP_ENABLE_ACCESS_LIST);
+        else if ("2".equals(val))
+            _booleanOptions.add(PROP_ENABLE_BLACKLIST);
+    }
+
+    public void setDelayOpen(String moo) {
+        _booleanOptions.add("i2cp.delayOpen");
+    }
+    public void setNewDest(String val) {
+        if ("1".equals(val))
+            _booleanOptions.add("i2cp.newDestOnResume");
+        else if ("2".equals(val))
+            _booleanOptions.add("persistentClientKey");
+    }
+
+    public void setReduceTime(String val) {
+        if (val != null) {
+            try {
+                _otherOptions.put("i2cp.reduceIdleTime", "" + (Integer.parseInt(val.trim()) * 60*1000));
+            } catch (NumberFormatException nfe) {}
+        }
+    }
+    public void setReduceCount(String val) {
+        if (val != null)
+            _otherOptions.put("i2cp.reduceQuantity", val.trim());
+    }
+    public void setEncryptKey(String val) {
+        if (val != null)
+            _otherOptions.put("i2cp.leaseSetKey", val.trim());
+    }
+
+    public void setAccessList(String val) {
+        if (val != null)
+            _otherOptions.put("i2cp.accessList", val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ","));
+    }
+
+    public void setJumpList(String val) {
+        if (val != null)
+            _otherOptions.put(I2PTunnelHTTPClient.PROP_JUMP_SERVERS, val.trim().replace("\r\n", ",").replace("\n", ",").replace(" ", ","));
+    }
+
+    public void setCloseTime(String val) {
+        if (val != null) {
+            try {
+                _otherOptions.put("i2cp.closeIdleTime", "" + (Integer.parseInt(val.trim()) * 60*1000));
+            } catch (NumberFormatException nfe) {}
+        }
+    }
+
+    /** all proxy auth @since 0.8.2 */
+    public void setProxyAuth(String s) {
+        if (s != null)
+            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_AUTH, I2PTunnelHTTPClientBase.DIGEST_AUTH);
+    }
+
+    public void setProxyUsername(String s) {
+        if (s != null)
+            _newProxyUser = s.trim();
+    }
+
+    public void setProxyPassword(String s) {
+        if (s != null)
+            _newProxyPW = s.trim();
+    }
+
+    public void setOutproxyAuth(String s) {
+        _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH, I2PTunnelHTTPClientBase.DIGEST_AUTH);
+    }
+
+    public void setOutproxyUsername(String s) {
+        if (s != null)
+            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_USER, s.trim());
+    }
+
+    public void setOutproxyPassword(String s) {
+        if (s != null)
+            _otherOptions.put(I2PTunnelHTTPClientBase.PROP_OUTPROXY_PW, s.trim());
+    }
+
+    /** all of these are @since 0.8.3 */
+    protected static final String PROP_MAX_CONNS_MIN = "i2p.streaming.maxConnsPerMinute";
+    protected static final String PROP_MAX_CONNS_HOUR = "i2p.streaming.maxConnsPerHour";
+    protected static final String PROP_MAX_CONNS_DAY = "i2p.streaming.maxConnsPerDay";
+    protected static final String PROP_MAX_TOTAL_CONNS_MIN = "i2p.streaming.maxTotalConnsPerMinute";
+    protected static final String PROP_MAX_TOTAL_CONNS_HOUR = "i2p.streaming.maxTotalConnsPerHour";
+    protected static final String PROP_MAX_TOTAL_CONNS_DAY = "i2p.streaming.maxTotalConnsPerDay";
+    protected static final String PROP_MAX_STREAMS = "i2p.streaming.maxConcurrentStreams";
+
+    public void setLimitMinute(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_MIN, s.trim());
+    }
+
+    public void setLimitHour(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_HOUR, s.trim());
+    }
+
+    public void setLimitDay(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_CONNS_DAY, s.trim());
+    }
+
+    public void setTotalMinute(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_MIN, s.trim());
+    }
+
+    public void setTotalHour(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_HOUR, s.trim());
+    }
+
+    public void setTotalDay(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_TOTAL_CONNS_DAY, s.trim());
+    }
+
+    public void setMaxStreams(String s) {
+        if (s != null)
+            _otherOptions.put(PROP_MAX_STREAMS, s.trim());
+    }
+
+    /**
+     * Based on all provided data, create a set of configuration parameters 
+     * suitable for use in a TunnelController.  This will replace (not add to)
+     * any existing parameters, so this should return a comprehensive mapping.
+     *
+     */
+    public Properties getConfig() {
+        Properties config = new Properties();
+        updateConfigGeneric(config);
+        
+        if ((TunnelUtil.isClient(_type) && !"streamrclient".equals(_type)) || "streamrserver".equals(_type)) {
+            // streamrserver uses interface
+            if (_reachableBy != null)
+                config.setProperty("interface", _reachableBy);
+            else
+                config.setProperty("interface", "");
+        } else {
+            // streamrclient uses targetHost
+            if (_targetHost != null)
+                config.setProperty("targetHost", _targetHost);
+        }
+
+        if (TunnelUtil.isClient(_type)) {
+            // generic client stuff
+            if (_port != null)
+                config.setProperty("listenPort", _port);
+            config.setProperty("sharedClient", _sharedClient + "");
+            for (String p : _booleanClientOpts)
+                config.setProperty("option." + p, "" + _booleanOptions.contains(p));
+            for (String p : _otherClientOpts)
+                if (_otherOptions.containsKey(p))
+                    config.setProperty("option." + p, _otherOptions.get(p));
+        } else {
+            // generic server stuff
+            if (_targetPort != null)
+                config.setProperty("targetPort", _targetPort);
+            for (String p : _booleanServerOpts)
+                config.setProperty("option." + p, "" + _booleanOptions.contains(p));
+            for (String p : _otherServerOpts)
+                if (_otherOptions.containsKey(p))
+                    config.setProperty("option." + p, _otherOptions.get(p));
+        }
+
+        // generic proxy stuff
+        if ("httpclient".equals(_type) || "connectclient".equals(_type) || 
+            "sockstunnel".equals(_type) ||"socksirctunnel".equals(_type)) {
+            for (String p : _booleanProxyOpts)
+                config.setProperty("option." + p, "" + _booleanOptions.contains(p));
+            if (_proxyList != null)
+                config.setProperty("proxyList", _proxyList);
+        }
+
+        // Proxy auth including migration to MD5
+        if ("httpclient".equals(_type) || "connectclient".equals(_type)) {
+            // Migrate even if auth is disabled
+            // go get the old from custom options that updateConfigGeneric() put in there
+            String puser = "option." + I2PTunnelHTTPClientBase.PROP_USER;
+            String user = config.getProperty(puser);
+            String ppw = "option." + I2PTunnelHTTPClientBase.PROP_PW;
+            String pw = config.getProperty(ppw);
+            if (user != null && pw != null && user.length() > 0 && pw.length() > 0) {
+                String pmd5 = "option." + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
+                              user + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SUFFIX;
+                if (config.getProperty(pmd5) == null) {
+                    // not in there, migrate
+                    String realm = _type.equals("httpclient") ? I2PTunnelHTTPClient.AUTH_REALM
+                                                              : I2PTunnelConnectClient.AUTH_REALM;
+                    String hex = PasswordManager.md5Hex(realm, user, pw);
+                    if (hex != null) {
+                        config.setProperty(pmd5, hex);
+                        config.remove(puser);
+                        config.remove(ppw);
+                    }
+                }
+            }
+            // New user/password
+            String auth = _otherOptions.get(I2PTunnelHTTPClientBase.PROP_AUTH);
+            if (auth != null && !auth.equals("false")) {
+                if (_newProxyUser != null && _newProxyPW != null &&
+                    _newProxyUser.length() > 0 && _newProxyPW.length() > 0) {
+                    String pmd5 = "option." + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_PREFIX +
+                                  _newProxyUser + I2PTunnelHTTPClientBase.PROP_PROXY_DIGEST_SUFFIX;
+                    String realm = _type.equals("httpclient") ? I2PTunnelHTTPClient.AUTH_REALM
+                                                              : I2PTunnelConnectClient.AUTH_REALM;
+                    String hex = PasswordManager.md5Hex(realm, _newProxyUser, _newProxyPW);
+                    if (hex != null)
+                        config.setProperty(pmd5, hex);
+                }
+            }
+        }
+
+        if ("ircclient".equals(_type) || "client".equals(_type) || "streamrclient".equals(_type)) {
+            if (_targetDestination != null)
+                config.setProperty("targetDestination", _targetDestination);
+        } else if ("httpserver".equals(_type) || "httpbidirserver".equals(_type)) {
+            if (_spoofedHost != null)
+                config.setProperty("spoofedHost", _spoofedHost);
+        }
+        if ("httpbidirserver".equals(_type)) {
+            if (_port != null)
+                config.setProperty("listenPort", _port);
+            if (_reachableBy != null)
+                config.setProperty("interface", _reachableBy);
+            else if (_targetHost != null)
+                config.setProperty("interface", _targetHost);
+            else
+                config.setProperty("interface", "");
+        }
+
+        if ("ircclient".equals(_type)) {
+            boolean dcc = _booleanOptions.contains(I2PTunnelIRCClient.PROP_DCC);
+            config.setProperty("option." + I2PTunnelIRCClient.PROP_DCC,
+                               "" + dcc);
+            // add some sane server options since they aren't in the GUI (yet)
+            if (dcc) {
+                config.setProperty("option." + PROP_MAX_CONNS_MIN, "3");
+                config.setProperty("option." + PROP_MAX_CONNS_HOUR, "10");
+                config.setProperty("option." + PROP_MAX_TOTAL_CONNS_MIN, "5");
+                config.setProperty("option." + PROP_MAX_TOTAL_CONNS_HOUR, "25");
+            }
+        }
+
+        return config;
+    }
+    
+    private static final String _noShowOpts[] = {
+        "inbound.length", "outbound.length", "inbound.lengthVariance", "outbound.lengthVariance",
+        "inbound.backupQuantity", "outbound.backupQuantity", "inbound.quantity", "outbound.quantity",
+        "inbound.nickname", "outbound.nickname", "i2p.streaming.connectDelay", "i2p.streaming.maxWindowSize",
+        I2PTunnelIRCClient.PROP_DCC
+        };
+    private static final String _booleanClientOpts[] = {
+        "i2cp.reduceOnIdle", "i2cp.closeOnIdle", "i2cp.newDestOnResume", "persistentClientKey", "i2cp.delayOpen"
+        };
+    private static final String _booleanProxyOpts[] = {
+        I2PTunnelHTTPClientBase.PROP_OUTPROXY_AUTH
+        };
+    private static final String _booleanServerOpts[] = {
+        "i2cp.reduceOnIdle", "i2cp.encryptLeaseSet", PROP_ENABLE_ACCESS_LIST, PROP_ENABLE_BLACKLIST
+        };
+    private static final String _otherClientOpts[] = {
+        "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.closeIdleTime",
+        "outproxyUsername", "outproxyPassword",
+        I2PTunnelHTTPClient.PROP_JUMP_SERVERS,
+        I2PTunnelHTTPClientBase.PROP_AUTH
+        };
+    private static final String _otherServerOpts[] = {
+        "i2cp.reduceIdleTime", "i2cp.reduceQuantity", "i2cp.leaseSetKey", "i2cp.accessList",
+         PROP_MAX_CONNS_MIN, PROP_MAX_CONNS_HOUR, PROP_MAX_CONNS_DAY,
+         PROP_MAX_TOTAL_CONNS_MIN, PROP_MAX_TOTAL_CONNS_HOUR, PROP_MAX_TOTAL_CONNS_DAY,
+         PROP_MAX_STREAMS
+        };
+
+    /**
+     *  do NOT add these to noShoOpts, we must leave them in for HTTPClient and ConnectCLient
+     *  so they will get migrated to MD5
+     *  TODO migrate socks to MD5
+     */
+    private static final String _otherProxyOpts[] = {
+        "proxyUsername", "proxyPassword"
+        };
+
+    protected static final Set<String> _noShowSet = new HashSet<String>(64);
+    protected static final Set<String> _nonProxyNoShowSet = new HashSet<String>(4);
+    static {
+        _noShowSet.addAll(Arrays.asList(_noShowOpts));
+        _noShowSet.addAll(Arrays.asList(_booleanClientOpts));
+        _noShowSet.addAll(Arrays.asList(_booleanProxyOpts));
+        _noShowSet.addAll(Arrays.asList(_booleanServerOpts));
+        _noShowSet.addAll(Arrays.asList(_otherClientOpts));
+        _noShowSet.addAll(Arrays.asList(_otherServerOpts));
+        _nonProxyNoShowSet.addAll(Arrays.asList(_otherProxyOpts));
+    }
+
+    private void updateConfigGeneric(Properties config) {
+        config.setProperty("type", _type);
+        if (_name != null)
+            config.setProperty("name", _name);
+        if (_description != null)
+            config.setProperty("description", _description);
+        if (!_context.isRouterContext()) {
+            if (_i2cpHost != null)
+                config.setProperty("i2cpHost", _i2cpHost);
+            if ( (_i2cpPort != null) && (_i2cpPort.trim().length() > 0) ) {
+                config.setProperty("i2cpPort", _i2cpPort);
+            } else {
+                config.setProperty("i2cpPort", "7654");
+            }
+        }
+        if (_privKeyFile != null)
+            config.setProperty("privKeyFile", _privKeyFile);
+        
+        if (_customOptions != null) {
+            StringTokenizer tok = new StringTokenizer(_customOptions);
+            while (tok.hasMoreTokens()) {
+                String pair = tok.nextToken();
+                int eq = pair.indexOf('=');
+                if ( (eq <= 0) || (eq >= pair.length()) )
+                    continue;
+                String key = pair.substring(0, eq);
+                if (_noShowSet.contains(key))
+                    continue;
+                // leave in for HTTP and Connect so it can get migrated to MD5
+                // hide for SOCKS until migrated to MD5
+                if ((!"httpclient".equals(_type)) &&
+                    (! "connectclient".equals(_type)) &&
+                    _nonProxyNoShowSet.contains(key))
+                    continue;
+                String val = pair.substring(eq+1);
+                config.setProperty("option." + key, val);
+            }
+        }
+
+        config.setProperty("startOnLoad", _startOnLoad + "");
+
+        if (_tunnelQuantity != null) {
+            config.setProperty("option.inbound.quantity", _tunnelQuantity);
+            config.setProperty("option.outbound.quantity", _tunnelQuantity);
+        }
+        if (_tunnelDepth != null) {
+            config.setProperty("option.inbound.length", _tunnelDepth);
+            config.setProperty("option.outbound.length", _tunnelDepth);
+        }
+        if (_tunnelVariance != null) {
+            config.setProperty("option.inbound.lengthVariance", _tunnelVariance);
+            config.setProperty("option.outbound.lengthVariance", _tunnelVariance);
+        }
+        if (_tunnelBackupQuantity != null) {
+            config.setProperty("option.inbound.backupQuantity", _tunnelBackupQuantity);
+            config.setProperty("option.outbound.backupQuantity", _tunnelBackupQuantity);
+        }
+        if (_connectDelay)
+            config.setProperty("option.i2p.streaming.connectDelay", "1000");
+        else
+            config.setProperty("option.i2p.streaming.connectDelay", "0");
+        if (TunnelUtil.isClient(_type) && _sharedClient) {
+            config.setProperty("option.inbound.nickname", CLIENT_NICKNAME);
+            config.setProperty("option.outbound.nickname", CLIENT_NICKNAME);
+        } else if (_name != null) {
+            config.setProperty("option.inbound.nickname", _name);
+            config.setProperty("option.outbound.nickname", _name);
+        }
+        if ("interactive".equals(_profile))
+            // This was 1 which doesn't make much sense
+            // The real way to make it interactive is to make the streaming lib
+            // MessageInputStream flush faster but there's no option for that yet,
+            // Setting it to 16 instead of the default but not sure what good that is either.
+            config.setProperty("option.i2p.streaming.maxWindowSize", "16");
+        else
+            config.remove("option.i2p.streaming.maxWindowSize");
+    }
+}
diff --git a/src/net/i2p/android/i2ptunnel/util/TunnelUtil.java b/src/net/i2p/android/i2ptunnel/util/TunnelUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..cd73db5767c54fcba8b325d5bf89653d8ddcb9dc
--- /dev/null
+++ b/src/net/i2p/android/i2ptunnel/util/TunnelUtil.java
@@ -0,0 +1,241 @@
+package net.i2p.android.i2ptunnel.util;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+
+import android.content.Context;
+import android.content.res.Resources;
+
+import net.i2p.I2PAppContext;
+import net.i2p.android.router.R;
+import net.i2p.android.router.util.Util;
+import net.i2p.i2ptunnel.TunnelController;
+import net.i2p.i2ptunnel.TunnelControllerGroup;
+import net.i2p.util.FileUtil;
+import net.i2p.util.SecureFile;
+
+public abstract class TunnelUtil {
+    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(Context ctx,
+            TunnelControllerGroup tcg,
+            int tunnelId,
+            Properties config) {
+        // Get current tunnel controller
+        TunnelController cur = getController(tcg, tunnelId);
+
+        if (config == null) {
+            List<String> ret = new ArrayList<String>();
+            ret.add("Invalid params");
+            return ret;
+        }
+        
+        if (cur == null) {
+            // creating new
+            cur = new TunnelController(config, "", true);
+            tcg.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 = 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()) && isClient(c.getType())) {
+                    Properties cOpt = c.getConfig("");
+                    if (config.getProperty("option.inbound.quantity") != null)
+                        cOpt.setProperty("option.inbound.quantity", config.getProperty("option.inbound.quantity"));
+                    if (config.getProperty("option.outbound.quantity") != null)
+                        cOpt.setProperty("option.outbound.quantity", config.getProperty("option.outbound.quantity"));
+                    if (config.getProperty("option.inbound.length") != null)
+                        cOpt.setProperty("option.inbound.length", config.getProperty("option.inbound.length"));
+                    if (config.getProperty("option.outbound.length") != null)
+                        cOpt.setProperty("option.outbound.length", config.getProperty("option.outbound.length"));
+                    if (config.getProperty("option.inbound.lengthVariance") != null)
+                        cOpt.setProperty("option.inbound.lengthVariance", config.getProperty("option.inbound.lengthVariance"));
+                    if (config.getProperty("option.outbound.lengthVariance") != null)
+                        cOpt.setProperty("option.outbound.lengthVariance", config.getProperty("option.outbound.lengthVariance"));
+                    if (config.getProperty("option.inbound.backupQuantity") != null)
+                        cOpt.setProperty("option.inbound.backupQuantity", config.getProperty("option.inbound.backupQuantity"));
+                    if (config.getProperty("option.outbound.backupQuantity") != null)
+                        cOpt.setProperty("option.outbound.backupQuantity", config.getProperty("option.outbound.backupQuantity"));
+                    cOpt.setProperty("option.inbound.nickname", TunnelConfig.CLIENT_NICKNAME);
+                    cOpt.setProperty("option.outbound.nickname", TunnelConfig.CLIENT_NICKNAME);
+                    
+                    c.setConfig(cOpt, "");
+                }
+            }
+        }
+
+        return doSave(ctx, tcg);
+    }
+
+    /**
+     *  Stop the tunnel, delete from config,
+     *  rename the private key file if in the default directory
+     */
+    public static List<String> deleteTunnel(Context ctx, TunnelControllerGroup tcg, int tunnelId) {
+        List<String> msgs;        
+        TunnelController cur = getController(tcg, tunnelId);
+        if (cur == null) {
+            msgs = new ArrayList<String>();
+            msgs.add("Invalid tunnel number");
+            return msgs;
+        }
+        
+        msgs = tcg.removeController(cur);
+        msgs.addAll(doSave(ctx, 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 = _privKeyFile;
+        if (pk != null && pk.startsWith("i2ptunnel") && pk.endsWith("-privKeys.dat") &&
+            ((!isClient(cur.getType())) || cur.getPersistentClientKey())) {
+            I2PAppContext context = I2PAppContext.getGlobalContext();
+            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 = "i2ptunnel-deleted-" + name.replace(' ', '_') + '-' + 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;
+    }
+
+    private static List<String> doSave(Context ctx, TunnelControllerGroup tcg) { 
+        List<String> rv = tcg.clearAllMessages();
+        try {
+            tcg.saveConfig();
+            rv.add(0, ctx.getResources().getString(R.string.i2ptunnel_msg_config_saved));
+        } catch (IOException ioe) {
+            Util.e("Failed to save config file", ioe);
+            rv.add(0, ctx.getResources().getString(R.string.i2ptunnel_msg_config_save_failed) + ": " + ioe.toString());
+        }
+        return rv;
+    }
+
+    /* General tunnel data for any type */
+
+    public static String getTypeFromName(String typeName, Context ctx) {
+        Resources res = ctx.getResources();
+        if (res.getString(R.string.i2ptunnel_type_client).equals(typeName))
+            return "client";
+        else if (res.getString(R.string.i2ptunnel_type_httpclient).equals(typeName))
+            return "httpclient";
+        else if (res.getString(R.string.i2ptunnel_type_ircclient).equals(typeName))
+            return "ircclient";
+        else if (res.getString(R.string.i2ptunnel_type_server).equals(typeName))
+            return "server";
+        else if (res.getString(R.string.i2ptunnel_type_httpserver).equals(typeName))
+            return "httpserver";
+        else if (res.getString(R.string.i2ptunnel_type_sockstunnel).equals(typeName))
+            return "sockstunnel";
+        else if (res.getString(R.string.i2ptunnel_type_socksirctunnel).equals(typeName))
+            return "socksirctunnel";
+        else if (res.getString(R.string.i2ptunnel_type_connectclient).equals(typeName))
+            return "connectclient";
+        else if (res.getString(R.string.i2ptunnel_type_ircserver).equals(typeName))
+            return "ircserver";
+        else if (res.getString(R.string.i2ptunnel_type_streamrclient).equals(typeName))
+            return "streamrclient";
+        else if (res.getString(R.string.i2ptunnel_type_streamrserver).equals(typeName))
+            return "streamrserver";
+        else if (res.getString(R.string.i2ptunnel_type_httpbidirserver).equals(typeName))
+            return "httpbidirserver";
+        else
+            return typeName;
+    }
+
+    public static String getTypeName(String type, Context context) {
+        Resources res = context.getResources();
+        if ("client".equals(type))
+            return res.getString(R.string.i2ptunnel_type_client);
+        else if ("httpclient".equals(type))
+            return res.getString(R.string.i2ptunnel_type_httpclient);
+        else if ("ircclient".equals(type))
+            return res.getString(R.string.i2ptunnel_type_ircclient);
+        else if ("server".equals(type))
+            return res.getString(R.string.i2ptunnel_type_server);
+        else if ("httpserver".equals(type))
+            return res.getString(R.string.i2ptunnel_type_httpserver);
+        else if ("sockstunnel".equals(type))
+            return res.getString(R.string.i2ptunnel_type_sockstunnel);
+        else if ("socksirctunnel".equals(type))
+            return res.getString(R.string.i2ptunnel_type_socksirctunnel);
+        else if ("connectclient".equals(type))
+            return res.getString(R.string.i2ptunnel_type_connectclient);
+        else if ("ircserver".equals(type))
+            return res.getString(R.string.i2ptunnel_type_ircserver);
+        else if ("streamrclient".equals(type))
+            return res.getString(R.string.i2ptunnel_type_streamrclient);
+        else if ("streamrserver".equals(type))
+            return res.getString(R.string.i2ptunnel_type_streamrserver);
+        else if ("httpbidirserver".equals(type))
+            return res.getString(R.string.i2ptunnel_type_httpbidirserver);
+        else
+            return type;
+    }
+
+    public static boolean isClient(String type) {
+        return ( ("client".equals(type)) ||
+                 ("httpclient".equals(type)) ||
+                 ("sockstunnel".equals(type)) ||
+                 ("socksirctunnel".equals(type)) ||
+                 ("connectclient".equals(type)) ||
+                 ("streamrclient".equals(type)) ||
+                 ("ircclient".equals(type)));
+    }
+
+    public static String getPrivateKeyFile(TunnelControllerGroup tcg, int tunnel) {
+        TunnelController tun = getController(tcg, tunnel);
+        if (tun != null && tun.getPrivKeyFile() != null)
+            return tun.getPrivKeyFile();
+        if (tunnel < 0)
+            tunnel = tcg == null ? 999 : tcg.getControllers().size();
+        return "i2ptunnel" + tunnel + "-privKeys.dat";
+    }
+}