Repository: mitmproxy/mitmproxy

Issue:
tls_early_post_process_client_hello, unsupported protocol 
Mitmproxy: 7.0.2
Python:    3.9.5
OpenSSL:   OpenSSL 1.1.1k  25 Mar 2021
Platform:  Windows-10-10.0.19043-SP0

```
127.0.0.1:53924: Client TLS handshake failed. The client may not trust the proxy's certificate for www.drumlinsecurity.co.uk (OpenSSL Error([('SSL routines', 'tls_early_post_process_client_hello', 'unsupported protocol')]))
```

but 6.0.2 is ok.




Discussion:
We need more info to reproduce this. Could you provide a PCAP?
> We need more info to reproduce this. Could you provide a PCAP?

thx.
Can it be sent by email?
Email is on my profile if you have sensitive data in there. :)
Sent mail, please check
Thanks for sending along the PCAP! Can you reproduce this with
```
mitmdump --set tls_version_client_min=SSL3 --set tls_version_server_min=SSL3
```
?
> Thanks for sending along the PCAP! Can you reproduce this with
> 
> ```
> mitmdump --set tls_version_client_min=SSL3 --set tls_version_server_min=SSL3
> ```
> 
> 

Thank you very much.

- mitmdump --set tls_version_client_min=SSL3 
After setting it is correct.
I analysis possible that the client only supports ssl3.


- but 
set mitmdump --set tls_version_client_**max**=SSL3 --set tls_version_server_min=SSL3
is Fail.
why?
> 127.0.0.1:59879: Unable to establish TLS connection with server (connection closed). Trying to establish TLS with client anyway.

![20215](https://user-images.githubusercontent.com/1232444/130378516-acd1d031-e0ac-4d2b-8dc4-b5f7cac1cd61.jpg)

Reset after three handshake
The version order is: SSLv3, TLS 1.0, TLS 1.1, TLS 1.2, TLS 1.3.

Your client only supports TLS 1.0. We had a bug in 7.0.2 which disabled the TLS 1.0 option, so I had to use SSL3, which is one lower.

Resolving patch:
diff --git a/mitmproxy/proxy/context.py b/mitmproxy/proxy/context.py
--- a/mitmproxy/proxy/context.py
+++ b/mitmproxy/proxy/context.py
@@ -34,15 +34,10 @@ def fork(self) -> "Context":
         return ret
 
     def __repr__(self):
-        layers = "\n    ".join(repr(l) for l in self.layers)
-        if layers:
-            layers = f"[\n    {layers}\n  ]"
-        else:
-            layers = "[]"
         return (
             f"Context(\n"
             f"  {self.client!r},\n"
             f"  {self.server!r},\n"
-            f"  layers={layers}\n"
+            f"  layers=[{self.layers!r}]\n"
             f")"
         )
diff --git a/mitmproxy/proxy/layers/tls.py b/mitmproxy/proxy/layers/tls.py
--- a/mitmproxy/proxy/layers/tls.py
+++ b/mitmproxy/proxy/layers/tls.py
@@ -213,8 +213,12 @@ def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bo
                 err = last_err[2]
             elif last_err == ('SSL routines', 'ssl3_get_record', 'wrong version number') and data[:4].isascii():
                 err = f"The remote server does not speak TLS."
-            else:  # pragma: no cover
-                # TODO: Add test case once we find one.
+            elif last_err == ('SSL routines', 'ssl3_read_bytes', 'tlsv1 alert protocol version'):
+                err = (
+                    f"The remote server and mitmproxy cannot agree on a TLS version to use. "
+                    f"You may need to adjust mitmproxy's tls_version_server_min option."
+                )
+            else:
                 err = f"OpenSSL {e!r}"
             self.conn.error = err
             return False, err
@@ -428,6 +432,11 @@ def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]:
         level: Literal["warn", "info"] = "warn"
         if err.startswith("Cannot parse ClientHello"):
             pass
+        elif "('SSL routines', 'tls_early_post_process_client_hello', 'unsupported protocol')" in err:
+            err = (
+                f"Client and mitmproxy cannot agree on a TLS version to use. "
+                f"You may need to adjust mitmproxy's tls_version_client_min option."
+            )
         elif "unknown ca" in err or "bad certificate" in err:
             err = f"The client does not trust the proxy's certificate for {dest} ({err})"
         elif err == "connection closed":


Test patch:
diff --git a/test/mitmproxy/proxy/layers/test_tls.py b/test/mitmproxy/proxy/layers/test_tls.py
--- a/test/mitmproxy/proxy/layers/test_tls.py
+++ b/test/mitmproxy/proxy/layers/test_tls.py
@@ -86,8 +86,13 @@ def test_parse_client_hello():
 class SSLTest:
     """Helper container for Python's builtin SSL object."""
 
-    def __init__(self, server_side: bool = False, alpn: typing.Optional[typing.List[str]] = None,
-                 sni: typing.Optional[bytes] = b"example.mitmproxy.org"):
+    def __init__(
+        self,
+        server_side: bool = False,
+        alpn: typing.Optional[typing.List[str]] = None,
+        sni: typing.Optional[bytes] = b"example.mitmproxy.org",
+        max_ver: typing.Optional[ssl.TLSVersion] = None,
+    ):
         self.inc = ssl.MemoryBIO()
         self.out = ssl.MemoryBIO()
         self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER if server_side else ssl.PROTOCOL_TLS_CLIENT)
@@ -104,6 +109,8 @@ def __init__(self, server_side: bool = False, alpn: typing.Optional[typing.List[
                 certfile=tlsdata.path("../../net/data/verificationcerts/trusted-leaf.crt"),
                 keyfile=tlsdata.path("../../net/data/verificationcerts/trusted-leaf.key"),
             )
+        if max_ver:
+            self.ctx.maximum_version = max_ver
 
         self.obj = self.ctx.wrap_bio(
             self.inc,
@@ -162,7 +169,10 @@ def reply_tls_start_client(alpn: typing.Optional[bytes] = None, *args, **kwargs)
     """
 
     def make_client_conn(tls_start: tls.TlsStartData) -> None:
+        # ssl_context = SSL.Context(Method.TLS_METHOD)
+        # ssl_context.set_min_proto_version(SSL.TLS1_3_VERSION)
         ssl_context = SSL.Context(SSL.SSLv23_METHOD)
+        ssl_context.set_options(SSL.OP_NO_SSLv3 | SSL.OP_NO_TLSv1 | SSL.OP_NO_TLSv1_1 | SSL.OP_NO_TLSv1_2)
         ssl_context.use_privatekey_file(
             tlsdata.path("../../net/data/verificationcerts/trusted-leaf.key")
         )
@@ -184,7 +194,10 @@ def reply_tls_start_server(alpn: typing.Optional[bytes] = None, *args, **kwargs)
     """
 
     def make_server_conn(tls_start: tls.TlsStartData) -> None:
+        # ssl_context = SSL.Context(Method.TLS_METHOD)
+        # ssl_context.set_min_proto_version(SSL.TLS1_3_VERSION)
         ssl_context = SSL.Context(SSL.SSLv23_METHOD)
+        ssl_context.set_options(SSL.OP_NO_SSLv3 | SSL.OP_NO_TLSv1 | SSL.OP_NO_TLSv1_1 | SSL.OP_NO_TLSv1_2)
         ssl_context.load_verify_locations(
             cafile=tlsdata.path("../../net/data/verificationcerts/trusted-root.crt")
         )
@@ -337,6 +350,39 @@ def test_remote_speaks_no_tls(self, tctx):
                 << commands.CloseConnection(tctx.server)
         )
 
+    def test_unsupported_protocol(self, tctx: context.Context):
+        """Test the scenario where the server only supports an outdated TLS version by default."""
+        playbook = tutils.Playbook(tls.ServerTLSLayer(tctx))
+        tctx.server.address = ("example.mitmproxy.org", 443)
+        tctx.server.state = ConnectionState.OPEN
+        tctx.server.sni = "example.mitmproxy.org"
+
+        # noinspection PyTypeChecker
+        tssl = SSLTest(server_side=True, max_ver=ssl.TLSVersion.TLSv1_2)
+
+        # send ClientHello
+        data = tutils.Placeholder(bytes)
+        assert (
+            playbook
+            << tls.TlsStartServerHook(tutils.Placeholder())
+            >> reply_tls_start_server()
+            << commands.SendData(tctx.server, data)
+        )
+
+        # receive ServerHello
+        tssl.bio_write(data())
+        with pytest.raises(ssl.SSLError):
+            tssl.do_handshake()
+
+        # send back error
+        assert (
+            playbook
+            >> events.DataReceived(tctx.server, tssl.bio_read())
+            << commands.Log("Server TLS handshake failed. The remote server and mitmproxy cannot agree on a TLS version"
+                            " to use. You may need to adjust mitmproxy's tls_version_server_min option.", "warn")
+            << commands.CloseConnection(tctx.server)
+        )
+
 
 def make_client_tls_layer(
         tctx: context.Context,
@@ -564,3 +610,20 @@ def test_immediate_disconnect(self, tctx: context.Context, close_at):
                             "client does not trust the proxy's certificate.", "info")
             << commands.CloseConnection(tctx.client)
         )
+
+    def test_unsupported_protocol(self, tctx: context.Context):
+        """Test the scenario where the client only supports an outdated TLS version by default."""
+        playbook, client_layer, tssl_client = make_client_tls_layer(tctx, max_ver=ssl.TLSVersion.TLSv1_2)
+        playbook.logs = True
+
+        assert (
+            playbook
+            >> events.DataReceived(tctx.client, tssl_client.bio_read())
+            << tls.TlsClienthelloHook(tutils.Placeholder())
+            >> tutils.reply()
+            << tls.TlsStartClientHook(tutils.Placeholder())
+            >> reply_tls_start_client()
+            << commands.Log("Client TLS handshake failed. Client and mitmproxy cannot agree on a TLS version to "
+                            "use. You may need to adjust mitmproxy's tls_version_client_min option.", "warn")
+            << commands.CloseConnection(tctx.client)
+        )
diff --git a/test/mitmproxy/proxy/tutils.py b/test/mitmproxy/proxy/tutils.py
--- a/test/mitmproxy/proxy/tutils.py
+++ b/test/mitmproxy/proxy/tutils.py
@@ -2,6 +2,7 @@
 import difflib
 import itertools
 import re
+import textwrap
 import traceback
 import typing
 
@@ -65,6 +66,7 @@ def _fmt_entry(x: PlaybookEntry):
     x = str(x)
     x = re.sub('Placeholder:None', '<unset placeholder>', x, flags=re.IGNORECASE)
     x = re.sub('Placeholder:', '', x, flags=re.IGNORECASE)
+    x = textwrap.indent(x, "     ")[5:]
     return f"{arrow} {x}"