Repository: abhinavsingh/proxy.py
Pull request #1031 (2022-01-21 13:21:42)

[Multiport] Prepare for listening on multiple ports
- Add `--ports` flag, which is a list of integers, defaults to empty list
- Usage:
  - `--ports 8900`: Will result in listening on 8899 and 8900
  - `--port 9000 --ports 9001 9002`: Will result in listening on 9000-9002

codecov[bot]: # [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) Report
> Merging [#1031](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (c5cd8d0) into [develop](https://codecov.io/gh/abhinavsingh/proxy.py/commit/1569146d70b09cc2d91d07cc971c2329095cbbb9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (1569146) will **decrease** coverage by `6.05%`.
> The diff coverage is `82.16%`.

[![Impacted file tree graph](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/graphs/tree.svg?width=650&height=150&src=pr&token=Zh9J7b4la2&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)

```diff
@@             Coverage Diff             @@
##           develop    #1031      +/-   ##
===========================================
- Coverage    85.83%   79.77%   -6.06%     
===========================================
  Files          146      150       +4     
  Lines         6396     6454      +58     
  Branches       648      605      -43     
===========================================
- Hits          5490     5149     -341     
- Misses         800     1181     +381     
- Partials       106      124      +18     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytest | `79.77% <82.16%> (-5.87%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | Coverage Δ | |
|---|---|---|
| [proxy/core/acceptor/\_\_init\_\_.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9hY2NlcHRvci9fX2luaXRfXy5weQ==) | `100.00% <ø> (ø)` | |
| [proxy/core/listener/unix.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9saXN0ZW5lci91bml4LnB5) | `52.94% <52.94%> (ø)` | |
| [tests/core/test\_listener.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvY29yZS90ZXN0X2xpc3RlbmVyLnB5) | `66.66% <66.66%> (-33.34%)` | :arrow_down: |
| [proxy/core/listener/pool.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9saXN0ZW5lci9wb29sLnB5) | `67.85% <67.85%> (ø)` | |
| [proxy/proxy.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcHJveHkucHk=) | `88.42% <76.92%> (-11.58%)` | :arrow_down: |
| [proxy/core/listener/base.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9saXN0ZW5lci9iYXNlLnB5) | `78.78% <78.78%> (ø)` | |
| [proxy/common/flag.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL2ZsYWcucHk=) | `88.32% <100.00%> (-2.13%)` | :arrow_down: |
| [proxy/core/acceptor/pool.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9hY2NlcHRvci9wb29sLnB5) | `94.44% <100.00%> (+0.21%)` | :arrow_up: |
| [proxy/core/listener/\_\_init\_\_.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9saXN0ZW5lci9fX2luaXRfXy5weQ==) | `100.00% <100.00%> (ø)` | |
| [proxy/core/listener/tcp.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9saXN0ZW5lci90Y3AucHk=) | `100.00% <100.00%> (ø)` | |
| ... and [26 more](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Last update [1569146...c5cd8d0](https://codecov.io/gh/abhinavsingh/proxy.py/pull/1031?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).


Patch:
diff --git a/README.md b/README.md
index ad7de4e..c8e812e 100644
--- a/README.md
+++ b/README.md
@@ -2251,8 +2251,8 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
           [--tunnel-remote-port TUNNEL_REMOTE_PORT] [--enable-events]
           [--threadless] [--threaded] [--num-workers NUM_WORKERS]
           [--local-executor LOCAL_EXECUTOR] [--backlog BACKLOG]
-          [--hostname HOSTNAME] [--port PORT] [--port-file PORT_FILE]
-          [--unix-socket-path UNIX_SOCKET_PATH]
+          [--hostname HOSTNAME] [--port PORT] [--ports PORTS [PORTS ...]]
+          [--port-file PORT_FILE] [--unix-socket-path UNIX_SOCKET_PATH]
           [--num-acceptors NUM_ACCEPTORS] [--version] [--log-level LOG_LEVEL]
           [--log-file LOG_FILE] [--log-format LOG_FORMAT]
           [--open-file-limit OPEN_FILE_LIMIT]
@@ -2270,13 +2270,14 @@ usage: -m [-h] [--tunnel-hostname TUNNEL_HOSTNAME] [--tunnel-port TUNNEL_PORT]
           [--proxy-pool PROXY_POOL] [--enable-web-server]
           [--enable-static-server] [--static-server-dir STATIC_SERVER_DIR]
           [--min-compression-length MIN_COMPRESSION_LENGTH]
-          [--pac-file PAC_FILE] [--pac-file-url-path PAC_FILE_URL_PATH]
+          [--enable-reverse-proxy] [--pac-file PAC_FILE]
+          [--pac-file-url-path PAC_FILE_URL_PATH]
           [--cloudflare-dns-mode CLOUDFLARE_DNS_MODE]
           [--filtered-upstream-hosts FILTERED_UPSTREAM_HOSTS]
           [--filtered-client-ips FILTERED_CLIENT_IPS]
           [--filtered-url-regex-config FILTERED_URL_REGEX_CONFIG]
 
-proxy.py v2.4.0rc7.dev28+gfbd7b46.d20220120
+proxy.py v2.4.0rc8.dev7+g1871027
 
 options:
   -h, --help            show this help message and exit
@@ -2315,9 +2316,12 @@ options:
                         executors, instead of using underlying OS kernel
                         scheduling algorithm.
   --backlog BACKLOG     Default: 100. Maximum number of pending connections to
-                        proxy server
+                        proxy server.
   --hostname HOSTNAME   Default: 127.0.0.1. Server IP address.
-  --port PORT           Default: 8899. Server port.
+  --port PORT           Default: 8899. Server port. To listen on more ports,
+                        pass them using --ports flag.
+  --ports PORTS [PORTS ...]
+                        Default: None. Additional ports to listen on.
   --port-file PORT_FILE
                         Default: None. Save server port numbers. Useful when
                         using --port=0 ephemeral mode.
@@ -2420,6 +2424,8 @@ options:
   --min-compression-length MIN_COMPRESSION_LENGTH
                         Default: 20 bytes. Sets the minimum length of a
                         response that will be compressed (gzipped).
+  --enable-reverse-proxy
+                        Default: False. Whether to enable reverse proxy core.
   --pac-file PAC_FILE   A file (Proxy Auto Configuration) or string to serve
                         when the server receives a direct file request. Using
                         this option enables proxy.HttpWebServerPlugin.
diff --git a/proxy/common/flag.py b/proxy/common/flag.py
index b9f420f..ec846ee 100644
--- a/proxy/common/flag.py
+++ b/proxy/common/flag.py
@@ -302,6 +302,7 @@ class FlagParser:
             # assert args.unix_socket_path is None
             args.family = socket.AF_INET6 if args.hostname.version == 6 else socket.AF_INET
         args.port = cast(int, opts.get('port', args.port))
+        args.ports = cast(Optional[List[int]], opts.get('ports', args.ports))
         args.backlog = cast(int, opts.get('backlog', args.backlog))
         num_workers = opts.get('num_workers', args.num_workers)
         args.num_workers = cast(
diff --git a/proxy/core/acceptor/__init__.py b/proxy/core/acceptor/__init__.py
index e5e3f60..4fd3077 100644
--- a/proxy/core/acceptor/__init__.py
+++ b/proxy/core/acceptor/__init__.py
@@ -14,11 +14,9 @@
 """
 from .pool import AcceptorPool
 from .acceptor import Acceptor
-from .listener import Listener
 
 
 __all__ = [
-    'Listener',
     'Acceptor',
     'AcceptorPool',
 ]
diff --git a/proxy/core/acceptor/listener.py b/proxy/core/acceptor/listener.py
deleted file mode 100644
index 0caef04..0000000
--- a/proxy/core/acceptor/listener.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    proxy.py
-    ~~~~~~~~
-    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
-    Network monitoring, controls & Application development, testing, debugging.
-
-    :copyright: (c) 2013-present by Abhinav Singh and contributors.
-    :license: BSD, see LICENSE for more details.
-
-    .. spelling::
-
-       acceptor
-"""
-import os
-import socket
-import logging
-import argparse
-from typing import Any, Optional
-
-from ...common.flag import flags
-from ...common.constants import (
-    DEFAULT_PORT, DEFAULT_BACKLOG, DEFAULT_PORT_FILE, DEFAULT_IPV4_HOSTNAME,
-)
-
-
-flags.add_argument(
-    '--backlog',
-    type=int,
-    default=DEFAULT_BACKLOG,
-    help='Default: 100. Maximum number of pending connections to proxy server',
-)
-
-flags.add_argument(
-    '--hostname',
-    type=str,
-    default=str(DEFAULT_IPV4_HOSTNAME),
-    help='Default: 127.0.0.1. Server IP address.',
-)
-
-flags.add_argument(
-    '--port',
-    type=int,
-    default=DEFAULT_PORT,
-    help='Default: 8899. Server port.',
-)
-
-flags.add_argument(
-    '--port-file',
-    type=str,
-    default=DEFAULT_PORT_FILE,
-    help='Default: None. Save server port numbers. Useful when using --port=0 ephemeral mode.',
-)
-
-flags.add_argument(
-    '--unix-socket-path',
-    type=str,
-    default=None,
-    help='Default: None. Unix socket path to use.  ' +
-    'When provided --host and --port flags are ignored',
-)
-
-logger = logging.getLogger(__name__)
-
-
-class Listener:
-
-    def __init__(self, flags: argparse.Namespace) -> None:
-        self.flags = flags
-        # Set after binding to a port.
-        # Stored here separately because ephemeral ports can be used.
-        self._port: Optional[int] = None
-        self._socket: Optional[socket.socket] = None
-
-    def __enter__(self) -> 'Listener':
-        self.setup()
-        return self
-
-    def __exit__(self, *args: Any) -> None:
-        self.shutdown()
-
-    def fileno(self) -> Optional[int]:
-        if not self._socket:
-            return None
-        return self._socket.fileno()
-
-    def setup(self) -> None:
-        if self.flags.unix_socket_path:
-            self._listen_unix_socket()
-        else:
-            self._listen_server_port()
-        if self.flags.unix_socket_path:
-            logger.info(
-                'Listening on %s' %
-                self.flags.unix_socket_path,
-            )
-        else:
-            logger.info(
-                'Listening on %s:%s' %
-                (self.flags.hostname, self._port),
-            )
-
-    def shutdown(self) -> None:
-        assert self._socket
-        self._socket.close()
-        if self.flags.unix_socket_path:
-            os.remove(self.flags.unix_socket_path)
-
-    def _listen_unix_socket(self) -> None:
-        self._socket = socket.socket(self.flags.family, socket.SOCK_STREAM)
-        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
-        self._socket.bind(self.flags.unix_socket_path)
-        self._socket.listen(self.flags.backlog)
-        self._socket.setblocking(False)
-
-    def _listen_server_port(self) -> None:
-        self._socket = socket.socket(self.flags.family, socket.SOCK_STREAM)
-        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
-        # self._socket.setsockopt(socket.SOL_TCP, socket.TCP_FASTOPEN, 5)
-        self._socket.bind((str(self.flags.hostname), self.flags.port))
-        self._socket.listen(self.flags.backlog)
-        self._socket.setblocking(False)
-        self._port = self._socket.getsockname()[1]
diff --git a/proxy/core/acceptor/pool.py b/proxy/core/acceptor/pool.py
index 99299cf..e7d16f9 100644
--- a/proxy/core/acceptor/pool.py
+++ b/proxy/core/acceptor/pool.py
@@ -22,7 +22,7 @@ from multiprocessing import connection
 from multiprocessing.reduction import send_handle
 
 from .acceptor import Acceptor
-from .listener import Listener
+from ..listener import ListenerPool
 from ...common.flag import flags
 from ...common.constants import DEFAULT_NUM_ACCEPTORS
 
@@ -64,7 +64,7 @@ class AcceptorPool:
     def __init__(
             self,
             flags: argparse.Namespace,
-            listener: Listener,
+            listeners: ListenerPool,
             executor_queues: List[connection.Connection],
             executor_pids: List[int],
             executor_locks: List['multiprocessing.synchronize.Lock'],
@@ -72,7 +72,7 @@ class AcceptorPool:
     ) -> None:
         self.flags = flags
         # File descriptor to use for accepting new work
-        self.listener: Listener = listener
+        self.listeners: ListenerPool = listeners
         # Available executors
         self.executor_queues: List[connection.Connection] = executor_queues
         self.executor_pids: List[int] = executor_pids
@@ -109,14 +109,16 @@ class AcceptorPool:
             ),
         )
         # Send file descriptor to all acceptor processes.
-        fd = self.listener.fileno()
-        for index in range(self.flags.num_acceptors):
-            send_handle(
-                self.fd_queues[index],
-                fd,
-                self.acceptors[index].pid,
-            )
-            self.fd_queues[index].close()
+        for listener in self.listeners.pool:
+            fd = listener.fileno()
+            assert fd is not None
+            for index in range(self.flags.num_acceptors):
+                send_handle(
+                    self.fd_queues[index],
+                    fd,
+                    self.acceptors[index].pid,
+                )
+                self.fd_queues[index].close()
 
     def shutdown(self) -> None:
         logger.info('Shutting down %d acceptors' % self.flags.num_acceptors)
diff --git a/proxy/core/listener/__init__.py b/proxy/core/listener/__init__.py
new file mode 100644
index 0000000..d2c89e7
--- /dev/null
+++ b/proxy/core/listener/__init__.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+
+    .. spelling::
+
+       pre
+"""
+from .tcp import TcpSocketListener
+from .pool import ListenerPool
+from .unix import UnixSocketListener
+
+
+__all__ = [
+    'UnixSocketListener',
+    'TcpSocketListener',
+    'ListenerPool',
+]
diff --git a/proxy/core/listener/base.py b/proxy/core/listener/base.py
new file mode 100644
index 0000000..f3fab4a
--- /dev/null
+++ b/proxy/core/listener/base.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import os
+import socket
+import logging
+import argparse
+from abc import ABC, abstractmethod
+from typing import Any, Optional
+
+from ...common.flag import flags
+from ...common.constants import DEFAULT_BACKLOG
+
+
+flags.add_argument(
+    '--backlog',
+    type=int,
+    default=DEFAULT_BACKLOG,
+    help='Default: 100. Maximum number of pending connections to proxy server.',
+)
+
+logger = logging.getLogger(__name__)
+
+
+class BaseListener(ABC):
+    """Base listener class.
+
+    For usage provide a listen method implementation."""
+
+    def __init__(self, flags: argparse.Namespace) -> None:
+        self.flags = flags
+        self._socket: Optional[socket.socket] = None
+
+    @abstractmethod
+    def listen(self) -> socket.socket:
+        raise NotImplementedError()
+
+    def __enter__(self) -> 'BaseListener':
+        self.setup()
+        return self
+
+    def __exit__(self, *args: Any) -> None:
+        self.shutdown()
+
+    def fileno(self) -> Optional[int]:
+        if not self._socket:
+            return None
+        return self._socket.fileno()
+
+    def setup(self) -> None:
+        self._socket = self.listen()
+
+    def shutdown(self) -> None:
+        assert self._socket
+        self._socket.close()
+        if self.flags.unix_socket_path:
+            os.remove(self.flags.unix_socket_path)
diff --git a/proxy/core/listener/pool.py b/proxy/core/listener/pool.py
new file mode 100644
index 0000000..25835fa
--- /dev/null
+++ b/proxy/core/listener/pool.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import argparse
+from typing import TYPE_CHECKING, Any, List
+
+from .tcp import TcpSocketListener
+from .unix import UnixSocketListener
+
+
+if TYPE_CHECKING:
+    from .base import BaseListener
+
+
+class ListenerPool:
+    """Provides abstraction around starting multiple listeners
+    based upon flags."""
+
+    def __init__(self, flags: argparse.Namespace) -> None:
+        self.flags = flags
+        self.pool: List['BaseListener'] = []
+
+    def __enter__(self) -> 'ListenerPool':
+        self.setup()
+        return self
+
+    def __exit__(self, *args: Any) -> None:
+        self.shutdown()
+
+    def setup(self) -> None:
+        if self.flags.unix_socket_path:
+            ulistener = UnixSocketListener(flags=self.flags)
+            ulistener.setup()
+            self.pool.append(ulistener)
+        else:
+            listener = TcpSocketListener(flags=self.flags)
+            listener.setup()
+            self.pool.append(listener)
+
+    def shutdown(self) -> None:
+        for listener in self.pool:
+            listener.shutdown()
+        self.pool.clear()
diff --git a/proxy/core/listener/tcp.py b/proxy/core/listener/tcp.py
new file mode 100644
index 0000000..68df2f5
--- /dev/null
+++ b/proxy/core/listener/tcp.py
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import socket
+import logging
+from typing import Any, Optional
+
+from .base import BaseListener
+from ...common.flag import flags
+from ...common.constants import (
+    DEFAULT_PORT, DEFAULT_PORT_FILE, DEFAULT_IPV4_HOSTNAME,
+)
+
+
+flags.add_argument(
+    '--hostname',
+    type=str,
+    default=str(DEFAULT_IPV4_HOSTNAME),
+    help='Default: 127.0.0.1. Server IP address.',
+)
+
+flags.add_argument(
+    '--port',
+    type=int,
+    default=DEFAULT_PORT,
+    help='Default: 8899.  Server port.  To listen on more ports, pass them using --ports flag.',
+)
+
+flags.add_argument(
+    '--ports',
+    action='append',
+    nargs='+',
+    default=None,
+    help='Default: None.  Additional ports to listen on.',
+)
+
+flags.add_argument(
+    '--port-file',
+    type=str,
+    default=DEFAULT_PORT_FILE,
+    help='Default: None. Save server port numbers. Useful when using --port=0 ephemeral mode.',
+)
+
+logger = logging.getLogger(__name__)
+
+
+class TcpSocketListener(BaseListener):
+    """Tcp listener."""
+
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        # Set after binding to a port.
+        #
+        # Stored here separately for ephemeral port discovery.
+        self._port: Optional[int] = None
+        super().__init__(*args, **kwargs)
+
+    def listen(self) -> socket.socket:
+        sock = socket.socket(self.flags.family, socket.SOCK_STREAM)
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+        # s.setsockopt(socket.SOL_TCP, socket.TCP_FASTOPEN, 5)
+        sock.bind((str(self.flags.hostname), self.flags.port))
+        sock.listen(self.flags.backlog)
+        sock.setblocking(False)
+        self._port = sock.getsockname()[1]
+        logger.info(
+            'Listening on %s:%s' %
+            (self.flags.hostname, self._port),
+        )
+        return sock
diff --git a/proxy/core/listener/unix.py b/proxy/core/listener/unix.py
new file mode 100644
index 0000000..fa84f96
--- /dev/null
+++ b/proxy/core/listener/unix.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Pluggable, TLS interception capable proxy server focused on
+    Network monitoring, controls & Application development, testing, debugging.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import socket
+import logging
+
+from .base import BaseListener
+from ...common.flag import flags
+
+
+flags.add_argument(
+    '--unix-socket-path',
+    type=str,
+    default=None,
+    help='Default: None. Unix socket path to use.  ' +
+    'When provided --host and --port flags are ignored',
+)
+
+logger = logging.getLogger(__name__)
+
+
+class UnixSocketListener(BaseListener):
+    """Unix socket domain listener."""
+
+    def listen(self) -> socket.socket:
+        sock = socket.socket(self.flags.family, socket.SOCK_STREAM)
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+        sock.bind(self.flags.unix_socket_path)
+        sock.listen(self.flags.backlog)
+        sock.setblocking(False)
+        logger.info(
+            'Listening on %s' %
+            self.flags.unix_socket_path,
+        )
+        return sock
diff --git a/proxy/proxy.py b/proxy/proxy.py
index 5725ffb..fcf8f79 100644
--- a/proxy/proxy.py
+++ b/proxy/proxy.py
@@ -14,14 +14,15 @@ import time
 import pprint
 import signal
 import logging
-from typing import Any, List, Optional
+from typing import TYPE_CHECKING, Any, List, Optional, cast
 
 from .core.ssh import SshTunnelListener, SshHttpProtocolHandler
 from .core.work import ThreadlessPool
 from .core.event import EventManager
 from .common.flag import FlagParser, flags
 from .common.utils import bytes_
-from .core.acceptor import Listener, AcceptorPool
+from .core.acceptor import AcceptorPool
+from .core.listener import ListenerPool
 from .common.constants import (
     IS_WINDOWS, DEFAULT_PLUGINS, DEFAULT_VERSION, DEFAULT_LOG_FILE,
     DEFAULT_PID_FILE, DEFAULT_LOG_LEVEL, DEFAULT_BASIC_AUTH,
@@ -30,6 +31,10 @@ from .common.constants import (
 )
 
 
+if TYPE_CHECKING:
+    from .core.listener import TcpSocketListener
+
+
 logger = logging.getLogger(__name__)
 
 
@@ -154,7 +159,7 @@ class Proxy:
 
     def __init__(self, input_args: Optional[List[str]] = None, **opts: Any) -> None:
         self.flags = FlagParser.initialize(input_args, **opts)
-        self.listener: Optional[Listener] = None
+        self.listeners: Optional[ListenerPool] = None
         self.executors: Optional[ThreadlessPool] = None
         self.acceptors: Optional[AcceptorPool] = None
         self.event_manager: Optional[EventManager] = None
@@ -184,12 +189,15 @@ class Proxy:
         self._write_pid_file()
         # We setup listeners first because of flags.port override
         # in case of ephemeral port being used
-        self.listener = Listener(flags=self.flags)
-        self.listener.setup()
+        self.listeners = ListenerPool(flags=self.flags)
+        self.listeners.setup()
         # Override flags.port to match the actual port
         # we are listening upon.  This is necessary to preserve
         # the server port when `--port=0` is used.
-        self.flags.port = self.listener._port
+        self.flags.port = cast(
+            'TcpSocketListener',
+            self.listeners.pool[0],
+        )._port
         self._write_port_file()
         # Setup EventManager
         if self.flags.enable_events:
@@ -210,7 +218,7 @@ class Proxy:
         # Setup acceptors
         self.acceptors = AcceptorPool(
             flags=self.flags,
-            listener=self.listener,
+            listeners=self.listeners,
             executor_queues=self.executors.work_queues if self.executors else [],
             executor_pids=self.executors.work_pids if self.executors else [],
             executor_locks=self.executors.work_locks if self.executors else [],
@@ -245,10 +253,10 @@ class Proxy:
         if self.flags.enable_events:
             assert self.event_manager is not None
             self.event_manager.shutdown()
-        assert self.listener
-        self.listener.shutdown()
-        self._delete_port_file()
-        self._delete_pid_file()
+        if self.listeners:
+            self.listeners.shutdown()
+            self._delete_port_file()
+            self._delete_pid_file()
 
     @property
     def remote_executors_enabled(self) -> bool:


Test patch:
diff --git a/tests/core/test_acceptor_pool.py b/tests/core/test_acceptor_pool.py
index d35b80e..d550901 100644
--- a/tests/core/test_acceptor_pool.py
+++ b/tests/core/test_acceptor_pool.py
@@ -20,10 +20,12 @@ class TestAcceptorPool(unittest.TestCase):
     @mock.patch('proxy.core.acceptor.pool.send_handle')
     @mock.patch('multiprocessing.Pipe')
     @mock.patch('proxy.core.acceptor.pool.Acceptor')
-    @mock.patch('proxy.core.acceptor.Listener')
+    @mock.patch('proxy.core.listener.pool.ListenerPool')
+    @mock.patch('proxy.core.listener.pool.TcpSocketListener')
     def test_setup_and_shutdown(
             self,
-            mock_listener: mock.Mock,
+            mock_tcp_socket_listener: mock.Mock,
+            mock_listener_pool: mock.Mock,
             mock_acceptor: mock.Mock,
             mock_pipe: mock.Mock,
             mock_send_handle: mock.Mock,
@@ -39,8 +41,11 @@ class TestAcceptorPool(unittest.TestCase):
         )
         self.assertEqual(flags.num_acceptors, num_acceptors)
 
+        mock_listener_pool.return_value.pool = [
+            mock_tcp_socket_listener.return_value,
+        ]
         pool = AcceptorPool(
-            flags=flags, listener=mock_listener.return_value,
+            flags=flags, listeners=mock_listener_pool.return_value,
             executor_queues=[], executor_pids=[], executor_locks=[],
         )
         pool.setup()
@@ -70,7 +75,7 @@ class TestAcceptorPool(unittest.TestCase):
 
         acceptor1.start.assert_called_once()
         acceptor2.start.assert_called_once()
-        mock_listener.return_value.fileno.assert_called_once()
+        mock_tcp_socket_listener.return_value.fileno.assert_called_once()
 
         acceptor1.join.assert_not_called()
         acceptor2.join.assert_not_called()
diff --git a/tests/core/test_listener.py b/tests/core/test_listener.py
index 79ab28a..6af6593 100644
--- a/tests/core/test_listener.py
+++ b/tests/core/test_listener.py
@@ -17,7 +17,7 @@ import unittest
 from unittest import mock
 
 from proxy.common.flag import FlagParser
-from proxy.core.acceptor import Listener
+from proxy.core.listener import TcpSocketListener, UnixSocketListener
 from proxy.common.constants import IS_WINDOWS
 
 
@@ -27,7 +27,7 @@ class TestListener(unittest.TestCase):
     def test_setup_and_teardown(self, mock_socket: mock.Mock) -> None:
         sock = mock_socket.return_value
         flags = FlagParser.initialize(port=0)
-        listener = Listener(flags)
+        listener = TcpSocketListener(flags)
         listener.setup()
         mock_socket.assert_called_with(
             socket.AF_INET6 if flags.hostname.version == 6 else socket.AF_INET,
@@ -66,7 +66,7 @@ class TestListener(unittest.TestCase):
         sock = mock_socket.return_value
         sock_path = os.path.join(tempfile.gettempdir(), 'proxy.sock')
         flags = FlagParser.initialize(unix_socket_path=sock_path)
-        listener = Listener(flags)
+        listener = UnixSocketListener(flags)
         listener.setup()
 
         mock_socket.assert_called_with(
diff --git a/tests/test_main.py b/tests/test_main.py
index 718fa17..d0bc8c4 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -78,6 +78,7 @@ class TestMain(unittest.TestCase):
         mock_args.port_file = DEFAULT_PORT_FILE
         mock_args.enable_ssh_tunnel = DEFAULT_ENABLE_SSH_TUNNEL
         mock_args.enable_reverse_proxy = DEFAULT_ENABLE_REVERSE_PROXY
+        mock_args.unix_socket_path = None
 
     @mock.patch('os.remove')
     @mock.patch('os.path.exists')
@@ -87,10 +88,10 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     @mock.patch('proxy.proxy.ThreadlessPool')
-    @mock.patch('proxy.proxy.Listener')
+    @mock.patch('proxy.proxy.ListenerPool')
     def test_entry_point(
             self,
-            mock_listener: mock.Mock,
+            mock_listener_pool: mock.Mock,
             mock_executor_pool: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
@@ -102,6 +103,7 @@ class TestMain(unittest.TestCase):
     ) -> None:
         pid_file = os.path.join(tempfile.gettempdir(), 'pid')
         mock_sleep.side_effect = KeyboardInterrupt()
+        mock_initialize.return_value.unix_socket_path = None
         mock_initialize.return_value.local_executor = 0
         mock_initialize.return_value.enable_events = False
         mock_initialize.return_value.pid_file = pid_file
@@ -109,7 +111,7 @@ class TestMain(unittest.TestCase):
         mock_initialize.return_value.enable_ssh_tunnel = False
         entry_point()
         mock_event_manager.assert_not_called()
-        mock_listener.assert_called_once_with(
+        mock_listener_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
         )
         mock_executor_pool.assert_called_once_with(
@@ -118,7 +120,7 @@ class TestMain(unittest.TestCase):
         )
         mock_acceptor_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
-            listener=mock_listener.return_value,
+            listeners=mock_listener_pool.return_value,
             executor_queues=mock_executor_pool.return_value.work_queues,
             executor_pids=mock_executor_pool.return_value.work_pids,
             executor_locks=mock_executor_pool.return_value.work_locks,
@@ -126,7 +128,7 @@ class TestMain(unittest.TestCase):
         )
         mock_acceptor_pool.return_value.setup.assert_called_once()
         mock_acceptor_pool.return_value.shutdown.assert_called_once()
-        mock_listener.return_value.shutdown.assert_called_once()
+        mock_listener_pool.return_value.shutdown.assert_called_once()
         mock_sleep.assert_called()
 
         mock_open.assert_called_with(pid_file, 'wb')
@@ -141,10 +143,10 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     @mock.patch('proxy.proxy.ThreadlessPool')
-    @mock.patch('proxy.proxy.Listener')
+    @mock.patch('proxy.proxy.ListenerPool')
     def test_main_with_no_flags(
             self,
-            mock_listener: mock.Mock,
+            mock_listener_pool: mock.Mock,
             mock_executor_pool: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
@@ -152,13 +154,14 @@ class TestMain(unittest.TestCase):
             mock_sleep: mock.Mock,
     ) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
+        mock_initialize.return_value.unix_socket_path = None
         mock_initialize.return_value.local_executor = 0
         mock_initialize.return_value.enable_events = False
         mock_initialize.return_value.port_file = None
         mock_initialize.return_value.enable_ssh_tunnel = False
         main()
         mock_event_manager.assert_not_called()
-        mock_listener.assert_called_once_with(
+        mock_listener_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
         )
         mock_executor_pool.assert_called_once_with(
@@ -167,7 +170,7 @@ class TestMain(unittest.TestCase):
         )
         mock_acceptor_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
-            listener=mock_listener.return_value,
+            listeners=mock_listener_pool.return_value,
             executor_queues=mock_executor_pool.return_value.work_queues,
             executor_pids=mock_executor_pool.return_value.work_pids,
             executor_locks=mock_executor_pool.return_value.work_locks,
@@ -175,7 +178,7 @@ class TestMain(unittest.TestCase):
         )
         mock_acceptor_pool.return_value.setup.assert_called_once()
         mock_acceptor_pool.return_value.shutdown.assert_called_once()
-        mock_listener.return_value.shutdown.assert_called_once()
+        mock_listener_pool.return_value.shutdown.assert_called_once()
         mock_sleep.assert_called()
 
     @mock.patch('time.sleep')
@@ -183,10 +186,10 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     @mock.patch('proxy.proxy.ThreadlessPool')
-    @mock.patch('proxy.proxy.Listener')
+    @mock.patch('proxy.proxy.ListenerPool')
     def test_enable_events(
             self,
-            mock_listener: mock.Mock,
+            mock_listener_pool: mock.Mock,
             mock_executor_pool: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
@@ -194,6 +197,7 @@ class TestMain(unittest.TestCase):
             mock_sleep: mock.Mock,
     ) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
+        mock_initialize.return_value.unix_socket_path = None
         mock_initialize.return_value.local_executor = 0
         mock_initialize.return_value.enable_events = True
         mock_initialize.return_value.port_file = None
@@ -202,7 +206,7 @@ class TestMain(unittest.TestCase):
         mock_event_manager.assert_called_once()
         mock_event_manager.return_value.setup.assert_called_once()
         mock_event_manager.return_value.shutdown.assert_called_once()
-        mock_listener.assert_called_once_with(
+        mock_listener_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
         )
         mock_executor_pool.assert_called_once_with(
@@ -211,7 +215,7 @@ class TestMain(unittest.TestCase):
         )
         mock_acceptor_pool.assert_called_once_with(
             flags=mock_initialize.return_value,
-            listener=mock_listener.return_value,
+            listeners=mock_listener_pool.return_value,
             event_queue=mock_event_manager.return_value.queue,
             executor_queues=mock_executor_pool.return_value.work_queues,
             executor_pids=mock_executor_pool.return_value.work_pids,
@@ -219,7 +223,7 @@ class TestMain(unittest.TestCase):
         )
         mock_acceptor_pool.return_value.setup.assert_called_once()
         mock_acceptor_pool.return_value.shutdown.assert_called_once()
-        mock_listener.return_value.shutdown.assert_called_once()
+        mock_listener_pool.return_value.shutdown.assert_called_once()
         mock_sleep.assert_called()
 
     @mock.patch('time.sleep')
@@ -228,10 +232,10 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     @mock.patch('proxy.proxy.ThreadlessPool')
-    @mock.patch('proxy.proxy.Listener')
+    @mock.patch('proxy.proxy.ListenerPool')
     def test_enable_dashboard(
             self,
-            mock_listener: mock.Mock,
+            mock_listener_pool: mock.Mock,
             mock_executor_pool: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
@@ -267,7 +271,7 @@ class TestMain(unittest.TestCase):
             mock_executor_pool.return_value.setup.assert_called_once()
         mock_acceptor_pool.assert_called_once()
         mock_acceptor_pool.return_value.setup.assert_called_once()
-        mock_listener.return_value.setup.assert_called_once()
+        mock_listener_pool.return_value.setup.assert_called_once()
 
     @mock.patch('time.sleep')
     @mock.patch('proxy.common.plugins.Plugins.load')
@@ -275,10 +279,10 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     @mock.patch('proxy.proxy.ThreadlessPool')
-    @mock.patch('proxy.proxy.Listener')
+    @mock.patch('proxy.proxy.ListenerPool')
     def test_enable_devtools(
             self,
-            mock_listener: mock.Mock,
+            mock_listener_pool: mock.Mock,
             mock_executor_pool: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
@@ -308,7 +312,7 @@ class TestMain(unittest.TestCase):
             mock_executor_pool.return_value.setup.assert_called_once()
         mock_acceptor_pool.assert_called_once()
         mock_acceptor_pool.return_value.setup.assert_called_once()
-        mock_listener.return_value.setup.assert_called_once()
+        mock_listener_pool.return_value.setup.assert_called_once()
 
     @mock.patch('time.sleep')
     @mock.patch('proxy.common.plugins.Plugins.load')
@@ -316,14 +320,14 @@ class TestMain(unittest.TestCase):
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     @mock.patch('proxy.proxy.ThreadlessPool')
-    @mock.patch('proxy.proxy.Listener')
+    @mock.patch('proxy.proxy.ListenerPool')
     @mock.patch('proxy.proxy.SshHttpProtocolHandler')
     @mock.patch('proxy.proxy.SshTunnelListener')
     def test_enable_ssh_tunnel(
             self,
             mock_ssh_tunnel_listener: mock.Mock,
             mock_ssh_http_proto_handler: mock.Mock,
-            mock_listener: mock.Mock,
+            mock_listener_pool: mock.Mock,
             mock_executor_pool: mock.Mock,
             mock_acceptor_pool: mock.Mock,
             mock_event_manager: mock.Mock,
@@ -350,7 +354,7 @@ class TestMain(unittest.TestCase):
             mock_executor_pool.return_value.setup.assert_called_once()
         mock_acceptor_pool.assert_called_once()
         mock_acceptor_pool.return_value.setup.assert_called_once()
-        mock_listener.return_value.setup.assert_called_once()
+        mock_listener_pool.return_value.setup.assert_called_once()
         mock_ssh_http_proto_handler.assert_called_once()
         mock_ssh_tunnel_listener.assert_called_once()
         mock_ssh_tunnel_listener.return_value.setup.assert_called_once()