diff --git a/core/src/saros/net/util/NetworkingUtils.java b/core/src/saros/net/util/NetworkingUtils.java
index 2932218185..f27b151451 100644
--- a/core/src/saros/net/util/NetworkingUtils.java
+++ b/core/src/saros/net/util/NetworkingUtils.java
@@ -4,7 +4,6 @@
 import java.net.InetAddress;
 import java.net.NetworkInterface;
 import java.net.SocketException;
-import java.util.ArrayList;
 import java.util.Enumeration;
 import java.util.LinkedList;
 import java.util.List;
@@ -78,25 +77,4 @@ public static Socks5Proxy getSocks5ProxySafe() {
 
     return proxy;
   }
-
-  /**
-   * Adds a specified IP (String) to the list of addresses of the Socks5Proxy. (the target attempts
-   * the stream host addresses one by one in the order of the list)
-   *
-   * @param ip String of the address of the Socks5Proxy (stream host)
-   * @param inFront boolean flag, if the address is to be inserted in front of the list. If <code>
-   *     false</code>, address is added at the end of the list.
-   */
-  public static void addProxyAddress(String ip, boolean inFront) {
-    Socks5Proxy proxy = getSocks5ProxySafe();
-
-    if (!inFront) {
-      proxy.addLocalAddress(ip);
-      return;
-    }
-    ArrayList<String> list = new ArrayList<String>(proxy.getLocalAddresses());
-    list.remove(ip);
-    list.add(0, ip);
-    proxy.replaceLocalAddresses(list);
-  }
 }
diff --git a/core/src/saros/net/xmpp/Socks5ProxySupport.java b/core/src/saros/net/xmpp/Socks5ProxySupport.java
new file mode 100644
index 0000000000..3073c15c18
--- /dev/null
+++ b/core/src/saros/net/xmpp/Socks5ProxySupport.java
@@ -0,0 +1,326 @@
+package saros.net.xmpp;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.apache.log4j.Logger;
+import org.bitlet.weupnp.GatewayDevice;
+import org.jivesoftware.smack.SmackConfiguration;
+import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy;
+import saros.net.stun.IStunService;
+import saros.net.upnp.IUPnPService;
+import saros.net.util.NetworkingUtils;
+import saros.util.ThreadUtils;
+
+/**
+ * Access class for accessing the Smack Socks5 proxy. It supports UPNP and STUN to retrieve possible
+ * Socks5 candidates and allows access to the local Smack Socks5 proxy behind gateways that supports
+ * UPNP.
+ */
+class Socks5ProxySupport {
+
+  private static final int STUN_DISCOVERY_TIMEOUT = 10000;
+
+  /* DO NOT CHANGE THE CONTENT OF THIS STRING, NEVER NEVER NEVER !!!
+   * The UNPN implementation will currently not overwrite present PORT mappings if they share not
+   * the same description. */
+  private static final String UPNP_PORT_MAPPING_DESCRIPTION = "Saros Socks5 TCP";
+
+  private static final Logger log = Logger.getLogger(Socks5ProxySupport.class);
+
+  private static final Object socks5AddressReplacementLock = new Object();
+
+  private final IUPnPService upnpService;
+  private final IStunService stunService;
+
+  /** The current gateway device to use for port mapping or <code>null</code>. */
+  private GatewayDevice device;
+
+  /** The current used Socks5 proxy or <code>null</code>. */
+  private Socks5Proxy socks5Proxy;
+
+  /**
+   * There is so much magic involved in Smack. Performing a disconnect shuts the proxy down so we
+   * need to remember the port.
+   */
+  private int socks5ProxyPort;
+
+  public Socks5ProxySupport(final IUPnPService upnpService, final IStunService stunService) {
+    this.upnpService = upnpService;
+    this.stunService = stunService;
+  }
+
+  /**
+   * Enables the Socks5 proxy. If the port number is negative the logic will try to find an unused
+   * port starting with the positive value of the port number up to 65535.
+   *
+   * @param port the port to use
+   * @param proxyAddresses collection containing addresses that should be published as public
+   *     addresses for connection purpose
+   * @param gatewayDeviceId the ID an UPNP device for port mapping or <code>null</code>
+   * @param useExternalGatewayDeviceAddress if <code>true</code> the public address of the device
+   *     will be published
+   * @param stunServerAddress address of a stun server to retrieve and publish public addresses or
+   *     <code>null</code>
+   * @param stunServerPort port of the stun server, not used if <code>stunServerAddress</code> is
+   *     <code>null</code>
+   * @return <code>true</code> if the proxy was successfully started, <code>false</code> if it could
+   *     not be started or is already running
+   */
+  public synchronized boolean enableProxy(
+      final int port,
+      final Collection<String> proxyAddresses,
+      final String gatewayDeviceId,
+      final boolean useExternalGatewayDeviceAddress,
+      final String stunServerAddress,
+      final int stunServerPort) {
+
+    if (socks5Proxy != null) return false;
+
+    SmackConfiguration.setLocalSocks5ProxyEnabled(true);
+    SmackConfiguration.setLocalSocks5ProxyPort(port);
+
+    socks5Proxy = Socks5Proxy.getSocks5Proxy();
+
+    if (socks5Proxy == null) {
+      log.warn("failed to start Socks5 proxy on port: " + port);
+      SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+      return false;
+    }
+
+    socks5ProxyPort = socks5Proxy.getPort();
+
+    // Unlikely but the connection was already lost, but signal that the proxy as running.
+    if (socks5ProxyPort <= 0) return true;
+
+    // Remove any addresses that Smack already discovered because we use our own logic.
+    socks5Proxy.replaceLocalAddresses(Collections.emptyList());
+
+    log.info(
+        "started Socks5 proxy on port: "
+            + socks5Proxy.getPort()
+            + " [listening on all interfaces]");
+
+    final List<String> proxyAddressesToPublish = new ArrayList<>();
+
+    if (proxyAddresses != null && proxyAddresses.isEmpty())
+      log.warn("Socks5 preconfigured addresses list is empty, using autodetect mode");
+
+    if (proxyAddresses == null || proxyAddresses.isEmpty()) {
+      NetworkingUtils.getAllNonLoopbackLocalIPAddresses(true)
+          .stream()
+          .map(InetAddress::getHostAddress)
+          .forEach(proxyAddressesToPublish::add);
+    } else {
+      proxyAddressesToPublish.addAll(proxyAddresses);
+    }
+
+    addSocks5ProxyAddresses(socks5Proxy, proxyAddressesToPublish, true);
+
+    // as STUN discovery can fail, take ages etc. do not block here
+    if (stunService != null && stunServerAddress != null && !stunServerAddress.isEmpty()) {
+
+      ThreadUtils.runSafeAsync(
+          "saros-stun-discovery",
+          log,
+          () -> {
+            discoverAndPublishStunAddresses(socks5Proxy, stunServerAddress, stunServerPort);
+          });
+    }
+
+    if (upnpService != null && gatewayDeviceId != null && !gatewayDeviceId.isEmpty()) {
+      device = getGatewayDevice(gatewayDeviceId);
+
+      if (device == null) {
+        log.warn(
+            "could not find a gateway device with id: + "
+                + gatewayDeviceId
+                + " in the current network environment");
+      } else {
+        mapPort(socks5Proxy, upnpService, device);
+
+        if (useExternalGatewayDeviceAddress) {
+          final InetAddress externalAddress = upnpService.getExternalAddress(device);
+
+          if (externalAddress != null) {
+            log.debug(
+                "obtained public IP address "
+                    + externalAddress
+                    + " from device: "
+                    + device.getFriendlyName());
+
+            addSocks5ProxyAddresses(
+                socks5Proxy, Collections.singletonList(externalAddress.getHostAddress()), true);
+          }
+        }
+      }
+    }
+
+    return true;
+  }
+
+  /**
+   * Stops the current Socks5 proxy if enabled and disables further usage of the Socks5 proxy. This
+   * method can be safely called to just prevent the global usage of the Socks5 proxy.
+   */
+  public synchronized void disableProxy() {
+
+    if (socks5Proxy == null) {
+      SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+      return;
+    }
+
+    socks5Proxy.stop();
+
+    SmackConfiguration.setLocalSocks5ProxyEnabled(false);
+
+    socks5Proxy = null;
+
+    log.info("stopped Socks5 proxy on port: " + socks5ProxyPort);
+
+    if (socks5ProxyPort > 0 && device != null) {
+      assert upnpService != null;
+      unmapPort(upnpService, device, socks5ProxyPort);
+    }
+
+    socks5ProxyPort = 0;
+    device = null;
+  }
+
+  private GatewayDevice getGatewayDevice(final String gatewayDeviceId) {
+    assert (upnpService != null);
+
+    final List<GatewayDevice> devices = upnpService.getGateways(false);
+
+    if (devices == null) {
+      log.warn("unable to retrieve gateway device(s) due to network failure");
+      return null;
+    }
+
+    for (GatewayDevice currentDevice : devices) {
+      if (gatewayDeviceId.equals(currentDevice.getUSN())) {
+        return currentDevice;
+      }
+    }
+
+    return null;
+  }
+
+  private static void mapPort(
+      final Socks5Proxy proxy, final IUPnPService upnpService, final GatewayDevice device) {
+
+    final int socks5ProxyPort = proxy.getPort();
+
+    if (socks5ProxyPort <= 0) return;
+
+    upnpService.deletePortMapping(device, socks5ProxyPort, IUPnPService.TCP);
+
+    if (!upnpService.createPortMapping(
+        device, socks5ProxyPort, IUPnPService.TCP, UPNP_PORT_MAPPING_DESCRIPTION)) {
+
+      log.warn(
+          "failed to create port mapping on device: "
+              + device.getFriendlyName()
+              + " ["
+              + socks5ProxyPort
+              + "|"
+              + IUPnPService.TCP
+              + "]");
+
+      return;
+    }
+
+    log.info(
+        "added port mapping on device: "
+            + device.getFriendlyName()
+            + " ["
+            + socks5ProxyPort
+            + "|"
+            + IUPnPService.TCP
+            + "]");
+  }
+
+  private static void unmapPort(
+      final IUPnPService upnpService, final GatewayDevice device, final int port) {
+
+    if (!upnpService.isMapped(device, port, IUPnPService.TCP)) return;
+
+    if (!upnpService.deletePortMapping(device, port, IUPnPService.TCP)) {
+      log.warn(
+          "failed to delete port mapping on device: "
+              + device.getFriendlyName()
+              + " ["
+              + port
+              + "|"
+              + IUPnPService.TCP
+              + "]");
+    }
+
+    log.info(
+        "removed port mapping on device: "
+            + device.getFriendlyName()
+            + " ["
+            + port
+            + "|"
+            + IUPnPService.TCP
+            + "]");
+  }
+
+  private void discoverAndPublishStunAddresses(
+      final Socks5Proxy proxy, final String stunServerAddress, final int stunServerPort) {
+
+    assert (stunService != null);
+
+    final Collection<InetSocketAddress> addresses =
+        stunService.discover(stunServerAddress, stunServerPort, STUN_DISCOVERY_TIMEOUT);
+
+    if (addresses.isEmpty()) {
+      log.warn(
+          "could not discover any public address using STUN server "
+              + stunServerAddress
+              + " (port="
+              + stunServerPort
+              + ")");
+
+      return;
+    }
+
+    // stun returns always literal IP addresses
+    final List<String> discoveredAddresses =
+        addresses.stream().map(InetSocketAddress::getHostString).collect(Collectors.toList());
+    log.debug(
+        "STUN discovery result for STUN server "
+            + stunServerAddress
+            + " (port="
+            + stunServerPort
+            + ") : "
+            + discoveredAddresses);
+
+    addSocks5ProxyAddresses(proxy, discoveredAddresses, true);
+  }
+
+  private static void addSocks5ProxyAddresses(
+      final Socks5Proxy proxy, final Collection<String> addresses, boolean inFront) {
+
+    synchronized (socks5AddressReplacementLock) {
+      final List<String> newAddresses = new ArrayList<>();
+
+      if (inFront) newAddresses.addAll(addresses);
+
+      newAddresses.addAll(proxy.getLocalAddresses());
+
+      if (!inFront) newAddresses.addAll(addresses);
+
+      final List<String> distinctAddresses =
+          newAddresses.stream().sequential().distinct().collect(Collectors.toList());
+
+      log.info("Socks5 proxy - public published IP addresses : " + distinctAddresses);
+
+      proxy.replaceLocalAddresses(distinctAddresses);
+    }
+  }
+}
diff --git a/core/src/saros/net/xmpp/XMPPConnectionService.java b/core/src/saros/net/xmpp/XMPPConnectionService.java
index 6dc24657ba..487fac8138 100644
--- a/core/src/saros/net/xmpp/XMPPConnectionService.java
+++ b/core/src/saros/net/xmpp/XMPPConnectionService.java
@@ -1,14 +1,10 @@
 package saros.net.xmpp;
 
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.CountDownLatch;
 import org.apache.log4j.Logger;
-import org.bitlet.weupnp.GatewayDevice;
 import org.jivesoftware.smack.Connection;
 import org.jivesoftware.smack.ConnectionConfiguration;
 import org.jivesoftware.smack.ConnectionListener;
@@ -18,16 +14,13 @@
 import org.jivesoftware.smack.XMPPException;
 import org.jivesoftware.smack.packet.Presence;
 import org.jivesoftware.smackx.ServiceDiscoveryManager;
-import org.jivesoftware.smackx.bytestreams.socks5.Socks5Proxy;
 import org.jivesoftware.smackx.entitycaps.EntityCapsManager;
 import org.jivesoftware.smackx.entitycaps.packet.CapsExtension;
 import saros.annotations.Component;
 import saros.net.ConnectionState;
 import saros.net.stun.IStunService;
 import saros.net.upnp.IUPnPService;
-import saros.net.util.NetworkingUtils;
 import saros.repackaged.picocontainer.annotations.Nullable;
-import saros.util.ThreadUtils;
 
 /**
  * This class is responsible for establishing XMPP connections and notifying registered listeners
@@ -38,14 +31,12 @@
  */
 @Component(module = "net")
 public class XMPPConnectionService {
+
   public static final String XMPP_CLIENT_IDENTIFIER = "https://saros-project.org";
   private static final String CAPS_HASH_ALGORITHM = "sha-1";
 
   private static final Logger log = Logger.getLogger(XMPPConnectionService.class);
 
-  // DO NOT CHANGE THE CONTENT OF THIS STRING, NEVER NEVER NEVER !!!
-  private static final String PORT_MAPPING_DESCRIPTION = "Saros Socks5 TCP";
-
   private Connection connection;
 
   private String resource;
@@ -59,18 +50,14 @@ public class XMPPConnectionService {
   /** The current port of the STUN server or <code>null</code>. */
   private int stunPort;
 
-  /** The current gateway device to use for port mapping or <code>null</code>. */
-  private GatewayDevice device;
-
+  /**
+   * Flag indicating if the public IP address of the gateway device should be published as Socks5
+   * candidate.
+   */
   private boolean useExternalGatewayDeviceAddress;
 
-  /** The listening port of the current Socks5Proxy. Is <b>-1</b> if the proxy is not running. */
-  private int socks5ProxyPort;
-
   private List<String> proxyAddresses;
 
-  private final Object portMappingLock = new Object();
-
   private JID localJID;
 
   private ConnectionState connectionState = ConnectionState.NOT_CONNECTED;
@@ -80,9 +67,7 @@ public class XMPPConnectionService {
   private final List<IConnectionListener> listeners =
       new CopyOnWriteArrayList<IConnectionListener>();
 
-  private final IStunService stunService;
-
-  private final IUPnPService upnpService;
+  private final Socks5ProxySupport socks5ProxySupport;
 
   private int packetReplyTimeout;
 
@@ -127,9 +112,7 @@ public void reconnectionSuccessful() {
 
   public XMPPConnectionService(
       @Nullable IUPnPService upnpService, @Nullable IStunService stunService) {
-    this.upnpService = upnpService;
-    this.stunService = stunService;
-
+    socks5ProxySupport = new Socks5ProxySupport(upnpService, stunService);
     packetReplyTimeout = Integer.getInteger("saros.net.smack.PACKET_REPLAY_TIMEOUT", 30000);
   }
 
@@ -180,28 +163,9 @@ public synchronized void configure(
     this.stunServer = stunServer;
     this.stunPort = stunPort;
 
-    this.device = null;
-
     if (this.stunServer != null && this.stunServer.isEmpty()) this.stunServer = null;
 
     if (this.gatewayDeviceID != null && this.gatewayDeviceID.isEmpty()) this.gatewayDeviceID = null;
-
-    if (this.gatewayDeviceID == null) return;
-
-    /*
-     * perform blocking tasks in the background meanwhile to speed up first
-     * connection attempt, currently it is only UPNP discovery
-     */
-
-    ThreadUtils.runSafeAsync(
-        "upnp-resolver",
-        log,
-        new Runnable() {
-          @Override
-          public void run() {
-            if (upnpService != null) upnpService.getGateways(true);
-          }
-        });
   }
 
   /**
@@ -410,175 +374,21 @@ private void initialzeNetworkComponents() {
      */
     SmackConfiguration.setPacketReplyTimeout(packetReplyTimeout);
 
-    SmackConfiguration.setLocalSocks5ProxyEnabled(isProxyEnabled);
-
-    if (!isProxyEnabled) return; // we are done, STUN and UPNP only affect Socks5
-
-    SmackConfiguration.setLocalSocks5ProxyPort(proxyPort);
-
-    final Socks5Proxy proxy = Socks5Proxy.getSocks5Proxy();
-
-    proxy.start();
-    socks5ProxyPort = proxy.getPort();
-
-    log.debug(
-        "started Socks5 proxy on port: " + socks5ProxyPort + " [listening on all interfaces]");
-
-    List<String> interfaceAddresses = new ArrayList<String>();
-
-    if (proxyAddresses != null) {
-      interfaceAddresses.addAll(proxyAddresses);
-
-      if (interfaceAddresses.isEmpty())
-        log.warn("Socks5 preconfigured addresses list is empty, using autodetect mode");
-      else log.debug("using preconfigured addresses: " + interfaceAddresses);
-    }
-
-    if (interfaceAddresses.isEmpty()) {
-      for (InetAddress interfaceAddress : NetworkingUtils.getAllNonLoopbackLocalIPAddresses(true)) {
-        interfaceAddresses.add(interfaceAddress.getHostAddress());
-      }
-      log.debug("using autodetected addresses: " + interfaceAddresses);
+    if (!isProxyEnabled) {
+      socks5ProxySupport.disableProxy();
+      return; // we are done, STUN and UPNP only affect Socks5
     }
 
-    proxy.replaceLocalAddresses(interfaceAddresses);
-
-    /*
-     * The public IP address from the STUN result may be added to late but
-     * this is definitely better then blocking the connection establishment
-     * for several seconds. Also it is very uncommon the a connection
-     * attempt to another is done just several seconds after the connection
-     * to the XMPP server is established
-     */
-    if (stunService != null && stunServer != null) {
-      ThreadUtils.runSafeAsync(
-          "stun-discovery",
-          log,
-          new Runnable() {
-            @Override
-            public void run() {
-              Collection<InetSocketAddress> addresses =
-                  stunService.discover(stunServer, stunPort, 10000);
-
-              for (InetSocketAddress address : addresses)
-                NetworkingUtils.addProxyAddress(address.getAddress().getHostAddress(), true);
-            }
-          });
-    }
-
-    final CountDownLatch mappingStart = new CountDownLatch(1);
-
-    if (gatewayDeviceID != null && upnpService != null) {
-      final String gatewayDeviceIDToFind = gatewayDeviceID;
-      ThreadUtils.runSafeAsync(
-          "upnp-portmapping",
-          log,
-          new Runnable() {
-            @Override
-            public void run() {
-              synchronized (portMappingLock) {
-                mappingStart.countDown();
-
-                List<GatewayDevice> devices = upnpService.getGateways(false);
-
-                if (devices == null) {
-                  log.warn("aborting UPNP port mapping due to network failure");
-                  return;
-                }
-
-                for (GatewayDevice currentDevice : devices) {
-                  if (gatewayDeviceIDToFind.equals(currentDevice.getUSN())) {
-                    device = currentDevice;
-                    break;
-                  }
-                }
-
-                if (device == null) {
-                  log.warn(
-                      "could not find gateway device with id: + "
-                          + gatewayDeviceID
-                          + " in the current network environment");
-                  return;
-                }
-
-                upnpService.deletePortMapping(device, socks5ProxyPort, IUPnPService.TCP);
-
-                log.debug(
-                    "creating port mapping on device: "
-                        + device.getFriendlyName()
-                        + " ["
-                        + socks5ProxyPort
-                        + "|"
-                        + IUPnPService.TCP
-                        + "]");
-
-                if (!upnpService.createPortMapping(
-                    device, socks5ProxyPort, IUPnPService.TCP, PORT_MAPPING_DESCRIPTION)) {
-
-                  log.warn(
-                      "failed to create port mapping on device: "
-                          + device.getFriendlyName()
-                          + " ["
-                          + socks5ProxyPort
-                          + "|"
-                          + IUPnPService.TCP
-                          + "]");
-
-                  device = null;
-                  return;
-                }
-
-                if (!useExternalGatewayDeviceAddress) return;
-
-                InetAddress externalAddress = upnpService.getExternalAddress(device);
-
-                if (externalAddress != null)
-                  NetworkingUtils.addProxyAddress(externalAddress.getHostAddress(), true);
-              }
-            }
-          });
-
-      try {
-        mappingStart.await();
-      } catch (InterruptedException e) {
-        Thread.currentThread().interrupt();
-      }
-    }
+    socks5ProxySupport.enableProxy(
+        proxyPort,
+        proxyAddresses,
+        gatewayDeviceID,
+        useExternalGatewayDeviceAddress,
+        stunServer,
+        stunPort);
   }
 
   private void uninitialzeNetworkComponents() {
-    if (socks5ProxyPort == -1) return;
-
-    Socks5Proxy.getSocks5Proxy().stop();
-
-    deleteMapping:
-    synchronized (portMappingLock) {
-      if (device == null) break deleteMapping;
-
-      if (!upnpService.isMapped(device, socks5ProxyPort, IUPnPService.TCP)) break deleteMapping;
-
-      log.debug(
-          "deleting port mapping on device: "
-              + device.getFriendlyName()
-              + " ["
-              + socks5ProxyPort
-              + "|"
-              + IUPnPService.TCP
-              + "]");
-
-      if (!upnpService.deletePortMapping(device, socks5ProxyPort, IUPnPService.TCP)) {
-        log.warn(
-            "failed to delete port mapping on device: "
-                + device.getFriendlyName()
-                + " ["
-                + socks5ProxyPort
-                + "|"
-                + IUPnPService.TCP
-                + "]");
-      }
-    }
-
-    socks5ProxyPort = -1;
-    device = null;
+    socks5ProxySupport.disableProxy();
   }
 }