Repository: abhinavsingh/proxy.py

Issue:
network interface binding option
hi,

I wish bind address option.
thank you.

ex) pc ips (1.1.1.2, 1.1.1.3, 1.1.1.4)

`# ip a

2: enp2s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether ****
    inet 1.1.1.2/24 brd 1.1.1.255 scope global enp2s0
    inet 1.1.1.3/24 brd 1.1.1.255 scope global enp2s0
    inet 1.1.1.4/24 brd 1.1.1.255 scope global enp2s0`

I wish choice 1.1.1.3

`import http.client
  
conn = http.client.HTTPConnection('xenosi.de', source_address=tuple(['1.1.1.3', 0]));

h = {}
h['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.114 Safari/537.36'
conn.request('GET', '/ip.php?json', headers=h);

res = conn.getresponse();
print(res.status, res.reason, res.read())`

result)
proxy --hostname=0.0.0.0 --bindip=1.1.1.3

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) Report
> Merging [#671](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (c39add0) into [develop](https://codecov.io/gh/abhinavsingh/proxy.py/commit/752146a14db6b56129cddf26b0d5dcdfe45252e9?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (752146a) will **decrease** coverage by `0.28%`.
> The diff coverage is `61.76%`.

[![Impacted file tree graph](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671/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/671?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     #671      +/-   ##
===========================================
- Coverage    74.35%   74.06%   -0.29%     
===========================================
  Files           84       86       +2     
  Lines         3252     3316      +64     
===========================================
+ Hits          2418     2456      +38     
- Misses         834      860      +26     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytest | `74.06% <61.76%> (-0.29%)` | :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/671?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | Coverage Δ | |
|---|---|---|
| [proxy/plugin/cloudflare\_dns.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcGx1Z2luL2Nsb3VkZmxhcmVfZG5zLnB5) | `51.42% <51.42%> (ø)` | |
| [proxy/plugin/custom\_dns\_resolver.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcGx1Z2luL2N1c3RvbV9kbnNfcmVzb2x2ZXIucHk=) | `57.89% <57.89%> (ø)` | |
| [proxy/http/proxy/server.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvaHR0cC9wcm94eS9zZXJ2ZXIucHk=) | `77.67% <85.71%> (+0.10%)` | :arrow_up: |
| [proxy/common/utils.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL3V0aWxzLnB5) | `94.73% <100.00%> (ø)` | |
| [proxy/core/connection/server.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29yZS9jb25uZWN0aW9uL3NlcnZlci5weQ==) | `100.00% <100.00%> (ø)` | |
| [proxy/http/proxy/plugin.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvaHR0cC9wcm94eS9wbHVnaW4ucHk=) | `100.00% <100.00%> (ø)` | |
| [proxy/plugin/\_\_init\_\_.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcGx1Z2luL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671?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/671?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Last update [752146a...c39add0](https://codecov.io/gh/abhinavsingh/proxy.py/pull/671?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/LICENSE b/LICENSE
index 35873c1..57b6da7 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2013-2022 by Abhinav Singh and contributors.
+Copyright (c) 2013-present by Abhinav Singh and contributors.
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification,
diff --git a/README.md b/README.md
index dbc567a..79d0838 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,8 @@
     - [Proxy Pool Plugin](#proxypoolplugin)
     - [FilterByClientIpPlugin](#filterbyclientipplugin)
     - [ModifyChunkResponsePlugin](#modifychunkresponseplugin)
+    - [CloudflareDnsResolverPlugin](#cloudflarednsresolverplugin)
+    - [CustomDnsResolverPlugin](#customdnsresolverplugin)
   - [HTTP Web Server Plugins](#http-web-server-plugins)
     - [Reverse Proxy](#reverse-proxy)
     - [Web Server Route](#web-server-route)
@@ -720,6 +722,36 @@ plugin
 
 Modify `ModifyChunkResponsePlugin` to your taste. Example, instead of sending hardcoded chunks, parse and modify the original `JSON` chunks received from the upstream server.
 
+### CloudflareDnsResolverPlugin
+
+This plugin uses `Cloudflare` hosted `DNS-over-HTTPS` [API](https://developers.cloudflare.com/1.1.1.1/encrypted-dns/dns-over-https/make-api-requests/dns-json) (json).
+
+Start `proxy.py` as:
+
+```bash
+❯ proxy \
+    --plugins proxy.plugin.CloudflareDnsResolverPlugin
+```
+
+By default, `CloudflareDnsResolverPlugin` runs in `security` mode (provides malware protection).  Use `--cloudflare-dns-mode family` to also enable
+adult content protection.
+
+### CustomDnsResolverPlugin
+
+This plugin demonstrate how to use a custom DNS resolution implementation with `proxy.py`.
+This example plugin currently uses Python's in-built resolution mechanism.  Customize code
+to your taste.  Example, query your custom DNS server, implement DoH or other mechanisms.
+
+Start `proxy.py` as:
+
+```bash
+❯ proxy \
+    --plugins proxy.plugin.CustomDnsResolverPlugin
+```
+
+`HttpProxyBasePlugin.resolve_dns` can also be used to configure `network interface` which
+must be used as the `source_address` for connection to the upstream server.
+
 ## HTTP Web Server Plugins
 
 ### Reverse Proxy
@@ -1013,7 +1045,9 @@ with TLS Interception:
 
 **This is a WIP and may not work as documented**
 
-Requires `paramiko` to work. See [requirements-tunnel.txt](https://github.com/abhinavsingh/proxy.py/blob/develop/requirements-tunnel.txt)
+Requires `paramiko` to work.
+
+See [requirements-tunnel.txt](https://github.com/abhinavsingh/proxy.py/blob/develop/requirements-tunnel.txt)
 
 ## Proxy Remote Requests Locally
 
@@ -1182,7 +1216,8 @@ if __name__ == '__main__':
 
 ## Loading Plugins
 
-You can, of course, list plugins to load in the input arguments list of `proxy.main` or `Proxy` constructor. Use the `--plugins` flag when starting from command line:
+You can, of course, list plugins to load in the input arguments list of `proxy.main` or
+`Proxy` constructor. Use the `--plugins` flag when starting from command line:
 
 ```python
 import proxy
@@ -1193,7 +1228,8 @@ if __name__ == '__main__':
   ])
 ```
 
-For simplicity you can pass the list of plugins to load as a keyword argument to `proxy.main` or the `Proxy` constructor:
+For simplicity you can pass the list of plugins to load as a keyword argument to `proxy.main` or
+the `Proxy` constructor:
 
 ```python
 import proxy
@@ -1353,8 +1389,8 @@ Contributors must start `proxy.py` from source to verify and develop new feature
 
 See [Run proxy.py from command line using repo source](#from-command-line-using-repo-source) for details.
 
-[![WARNING](https://img.shields.io/static/v1?label=MacOS&message=warning&color=red)]
-(https://github.com/abhinavsingh/proxy.py/issues/642#issuecomment-960819271) On `macOS`
+
+[![WARNING](https://img.shields.io/static/v1?label=MacOS&message=warning&color=red)](https://github.com/abhinavsingh/proxy.py/issues/642#issuecomment-960819271) On `macOS`
 you must install `Python` using `pyenv`, as `Python` installed via `homebrew` tends
 to be problematic.  See linked thread for more details.
 
@@ -1575,7 +1611,8 @@ FILE
 
 # Run Dashboard
 
-Dashboard is currently under development and not yet bundled with `pip` packages.  To run dashboard, you must checkout the source.
+Dashboard is currently under development and not yet bundled with `pip` packages.
+To run dashboard, you must checkout the source.
 
 Dashboard is written in Typescript and SCSS, so let's build it first using:
 
@@ -1606,9 +1643,12 @@ $ open http://localhost:8899/dashboard/
 
 ## Inspect Traffic
 
-Wait for embedded `Chrome Dev Console` to load.  Currently, detail about all traffic flowing through `proxy.py` is pushed to the `Inspect Traffic` tab.  However, received payloads are not yet integrated with the embedded dev console.
+Wait for embedded `Chrome Dev Console` to load.  Currently, detail about all traffic flowing
+through `proxy.py` is pushed to the `Inspect Traffic` tab.  However, received payloads are not
+yet integrated with the embedded dev console.
 
-Current functionality can be verified by opening the `Dev Console` of dashboard and inspecting the websocket connection that dashboard established with the `proxy.py` server.
+Current functionality can be verified by opening the `Dev Console` of dashboard and inspecting
+the websocket connection that dashboard established with the `proxy.py` server.
 
 [![Proxy.Py Dashboard Inspect Traffic](https://raw.githubusercontent.com/abhinavsingh/proxy.py/develop/Dashboard.png)](https://github.com/abhinavsingh/proxy.py)
 
diff --git a/proxy/common/utils.py b/proxy/common/utils.py
index a79cd8a..2fb802b 100644
--- a/proxy/common/utils.py
+++ b/proxy/common/utils.py
@@ -181,7 +181,7 @@ def wrap_socket(
 
 
 def new_socket_connection(
-        addr: Tuple[str, int], timeout: int = DEFAULT_TIMEOUT,
+        addr: Tuple[str, int], timeout: int = DEFAULT_TIMEOUT, source_address: Optional[Tuple[str, int]] = None,
 ) -> socket.socket:
     conn = None
     try:
@@ -205,7 +205,7 @@ def new_socket_connection(
         return conn
 
     # try to establish dual stack IPv4/IPv6 connection.
-    return socket.create_connection(addr, timeout=timeout)
+    return socket.create_connection(addr, timeout=timeout, source_address=source_address)
 
 
 class socket_connection(contextlib.ContextDecorator):
diff --git a/proxy/core/connection/server.py b/proxy/core/connection/server.py
index e88b1a8..fb92937 100644
--- a/proxy/core/connection/server.py
+++ b/proxy/core/connection/server.py
@@ -33,9 +33,11 @@ class TcpServerConnection(TcpConnection):
             raise TcpConnectionUninitializedException()
         return self._conn
 
-    def connect(self) -> None:
+    def connect(self, addr: Optional[Tuple[str, int]] = None, source_address: Optional[Tuple[str, int]] = None) -> None:
         if self._conn is None:
-            self._conn = new_socket_connection(self.addr)
+            self._conn = new_socket_connection(
+                addr or self.addr, source_address=source_address,
+            )
         self.closed = False
 
     def wrap(self, hostname: str, ca_file: Optional[str]) -> None:
diff --git a/proxy/http/proxy/plugin.py b/proxy/http/proxy/plugin.py
index 710f50d..be093f2 100644
--- a/proxy/http/proxy/plugin.py
+++ b/proxy/http/proxy/plugin.py
@@ -77,6 +77,22 @@ class HttpProxyBasePlugin(ABC):
         """Implementations must now read data over the socket."""
         return False  # pragma: no cover
 
+    def resolve_dns(self, host: str, port: int) -> Tuple[Optional[str], Optional[Tuple[str, int]]]:
+        """Resolve upstream server host to an IP address.
+
+        Optionally also override the source address to use for
+        connection with upstream server.
+
+        For upstream IP:
+        Return None to use default resolver available to the system.
+        Return ip address as string to use your custom resolver.
+
+        For source address:
+        Return None to use default source address
+        Return 2-tuple representing (host, port) to use as source address
+        """
+        return None, None
+
     @abstractmethod
     def before_upstream_connection(
             self, request: HttpParser,
diff --git a/proxy/http/proxy/server.py b/proxy/http/proxy/server.py
index 0792763..ec0a606 100644
--- a/proxy/http/proxy/server.py
+++ b/proxy/http/proxy/server.py
@@ -509,13 +509,30 @@ class HttpProxyPlugin(HttpProtocolHandlerPlugin):
                     'Connecting to upstream %s:%s' %
                     (text_(host), port),
                 )
-                self.server.connect()
+                # Invoke plugin.resolve_dns
+                upstream_ip, source_addr = None, None
+                for plugin in self.plugins.values():
+                    upstream_ip, source_addr = plugin.resolve_dns(
+                        text_(host), port,
+                    )
+                    if upstream_ip or source_addr:
+                        break
+                # Connect with overridden upstream IP and source address
+                # if any of the plugin returned a non-null value.
+                self.server.connect(
+                    addr=None if not upstream_ip else (
+                        upstream_ip, port,
+                    ), source_address=source_addr,
+                )
                 self.server.connection.setblocking(False)
                 logger.debug(
                     'Connected to upstream %s:%s' %
                     (text_(host), port),
                 )
             except Exception as e:  # TimeoutError, socket.gaierror
+                logger.exception(
+                    'Unable to connect with upstream server', exc_info=e,
+                )
                 self.server.closed = True
                 raise ProxyConnectionFailed(text_(host), port, repr(e)) from e
         else:
diff --git a/proxy/plugin/__init__.py b/proxy/plugin/__init__.py
index 8450e8f..6e2ce5e 100644
--- a/proxy/plugin/__init__.py
+++ b/proxy/plugin/__init__.py
@@ -21,6 +21,8 @@ from .proxy_pool import ProxyPoolPlugin
 from .filter_by_client_ip import FilterByClientIpPlugin
 from .filter_by_url_regex import FilterByURLRegexPlugin
 from .modify_chunk_response import ModifyChunkResponsePlugin
+from .custom_dns_resolver import CustomDnsResolverPlugin
+from .cloudflare_dns import CloudflareDnsResolverPlugin
 
 __all__ = [
     'CacheResponsesPlugin',
@@ -37,4 +39,6 @@ __all__ = [
     'FilterByClientIpPlugin',
     'ModifyChunkResponsePlugin',
     'FilterByURLRegexPlugin',
+    'CustomDnsResolverPlugin',
+    'CloudflareDnsResolverPlugin',
 ]
diff --git a/proxy/plugin/cloudflare_dns.py b/proxy/plugin/cloudflare_dns.py
new file mode 100644
index 0000000..eaa4514
--- /dev/null
+++ b/proxy/plugin/cloudflare_dns.py
@@ -0,0 +1,94 @@
+# -*- 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 logging
+
+try:
+    import httpx
+except ImportError:
+    pass
+
+from typing import Optional, Tuple
+
+from ..common.flag import flags
+from ..http.parser import HttpParser
+from ..http.proxy import HttpProxyBasePlugin
+
+logger = logging.getLogger(__name__)
+
+
+flags.add_argument(
+    '--cloudflare-dns-mode',
+    type=str,
+    default='security',
+    help='Default: security.  Either "security" (for malware protection) ' +
+    'or "family" (for malware and adult content protection)',
+)
+
+
+class CloudflareDnsResolverPlugin(HttpProxyBasePlugin):
+    """This plugin uses Cloudflare DNS resolver to provide protection
+    against malwares and adult content.  Implementation uses DoH specification.
+
+    See https://developers.cloudflare.com/1.1.1.1/1.1.1.1-for-families
+    See https://developers.cloudflare.com/1.1.1.1/encrypted-dns/dns-over-https/make-api-requests/dns-json
+
+    NOTE: For this plugin to work, make sure to bypass proxy for 1.1.1.1
+
+    NOTE: This plugin requires additional dependency because DoH mandates
+    a HTTP2 complaint client.  Install `httpx` dependency as:
+
+    pip install "httpx[http2]"
+    """
+
+    def resolve_dns(self, host: str, port: int) -> Tuple[Optional[str], Optional[Tuple[str, int]]]:
+        try:
+            context = httpx.create_ssl_context(http2=True)
+            # TODO: Support resolution via Authority (SOA) to add support for
+            # AAAA (IPv6) query
+            r = httpx.get(
+                'https://{0}.cloudflare-dns.com/dns-query?name={1}&type=A'.format(
+                    self.flags.cloudflare_dns_mode, host,
+                ),
+                headers={'accept': 'application/dns-json'},
+                verify=context,
+                timeout=httpx.Timeout(timeout=5.0),
+                proxies={
+                    'all://': None,
+                },
+            )
+            if r.status_code != 200:
+                return None, None
+            response = r.json()
+            answers = response.get('Answer', [])
+            if len(answers) == 0:
+                return None, None
+            # TODO: Utilize TTL to cache response locally
+            # instead of making a DNS query repeatedly for the same host.
+            return answers[0]['data'], None
+        except Exception as e:
+            logger.exception('Unable to resolve DNS-over-HTTPS', exc_info=e)
+            return None, None
+
+    def before_upstream_connection(
+            self, request: HttpParser,
+    ) -> Optional[HttpParser]:
+        return request
+
+    def handle_client_request(
+            self, request: HttpParser,
+    ) -> Optional[HttpParser]:
+        return request
+
+    def handle_upstream_chunk(self, chunk: memoryview) -> memoryview:
+        return chunk
+
+    def on_upstream_connection_close(self) -> None:
+        pass
diff --git a/proxy/plugin/custom_dns_resolver.py b/proxy/plugin/custom_dns_resolver.py
new file mode 100644
index 0000000..d714b5c
--- /dev/null
+++ b/proxy/plugin/custom_dns_resolver.py
@@ -0,0 +1,52 @@
+# -*- 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
+
+from typing import Optional, Tuple
+
+from ..http.parser import HttpParser
+from ..http.proxy import HttpProxyBasePlugin
+
+
+class CustomDnsResolverPlugin(HttpProxyBasePlugin):
+    """This plugin demonstrate how to use your own custom DNS resolver."""
+
+    def resolve_dns(self, host: str, port: int) -> Tuple[Optional[str], Optional[Tuple[str, int]]]:
+        """Here we are using in-built python resolver for demonstration.
+
+        Ideally you would like to query your custom DNS server or even use DoH to make
+        real sense out of this plugin.
+
+        2nd parameter returned is None.  Return a 2-tuple to configure underlying interface
+        to use for connection to the upstream server.
+        """
+        try:
+            return socket.getaddrinfo(host, port, proto=socket.IPPROTO_TCP)[0][4][0], None
+        except socket.gaierror:
+            # Ideally we can also thrown HttpRequestRejected or HttpProtocolException here
+            # Returning None simply fallback to core generated exceptions.
+            return None, None
+
+    def before_upstream_connection(
+            self, request: HttpParser,
+    ) -> Optional[HttpParser]:
+        return request
+
+    def handle_client_request(
+            self, request: HttpParser,
+    ) -> Optional[HttpParser]:
+        return request
+
+    def handle_upstream_chunk(self, chunk: memoryview) -> memoryview:
+        return chunk
+
+    def on_upstream_connection_close(self) -> None:
+        pass


Test patch:
diff --git a/tests/common/test_utils.py b/tests/common/test_utils.py
index 655ec74..6d6217a 100644
--- a/tests/common/test_utils.py
+++ b/tests/common/test_utils.py
@@ -43,7 +43,9 @@ class TestSocketConnectionUtils(unittest.TestCase):
     @mock.patch('socket.create_connection')
     def test_new_socket_connection_dual(self, mock_socket: mock.Mock) -> None:
         conn = new_socket_connection(self.addr_dual)
-        mock_socket.assert_called_with(self.addr_dual, timeout=DEFAULT_TIMEOUT)
+        mock_socket.assert_called_with(
+            self.addr_dual, timeout=DEFAULT_TIMEOUT, source_address=None,
+        )
         self.assertEqual(conn, mock_socket.return_value)
 
     @mock.patch('proxy.common.utils.new_socket_connection')
diff --git a/tests/http/test_http_proxy.py b/tests/http/test_http_proxy.py
index f00737e..13a55a5 100644
--- a/tests/http/test_http_proxy.py
+++ b/tests/http/test_http_proxy.py
@@ -60,6 +60,7 @@ class TestHttpProxyPlugin(unittest.TestCase):
         self.plugin.return_value.read_from_descriptors.return_value = False
         self.plugin.return_value.before_upstream_connection.side_effect = lambda r: r
         self.plugin.return_value.handle_client_request.side_effect = lambda r: r
+        self.plugin.return_value.resolve_dns.return_value = None, None
 
         self._conn.recv.return_value = build_http_request(
             b'GET', b'http://upstream.host/not-found.html',
diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py
index 43989a5..bbbc6d5 100644
--- a/tests/http/test_http_proxy_tls_interception.py
+++ b/tests/http/test_http_proxy_tls_interception.py
@@ -133,6 +133,7 @@ class TestHttpProxyTlsInterception(unittest.TestCase):
         self.proxy_plugin.return_value.read_from_descriptors.return_value = False
         self.proxy_plugin.return_value.before_upstream_connection.side_effect = lambda r: r
         self.proxy_plugin.return_value.handle_client_request.side_effect = lambda r: r
+        self.proxy_plugin.return_value.resolve_dns.return_value = None, None
 
         self.mock_selector.return_value.select.side_effect = [
             [(