Repository: mitmproxy/mitmproxy

Issue:
Wrong SNI hostname used for reverse HTTPS proxy
#### Problem Description
When using mitmproxy in reverse proxy mode, it attempts to use the mitmproxy's domain for SNI in the outer TLS connection, fails to validate that connection and returns "502 Bad Gateway" -- "Certificate verify failed: Hostname mismatch"

#### Steps to reproduce the behavior:
1. Start mitmproxy in reverse proxy mode, e.g.

```mitmproxy --mode reverse:https://www.chimieparistech.psl.eu@8081 --certs locahost=localhost.pem```

3. Get https://localhost:8081/ with Chrome or Edge

mitmproxy logs...
```warn: [10:40:24.473][[::1]:62455] Server TLS handshake failed. Certificate verify failed: hostname mismatch```
... and returns to the browser ...
502 Bad Gateway, Certificate verify failed: hostname mismatch.

A packet capture of the traffic shows that mitmproxy sends at least 2 client Hello packets to the Chimieparistech server. In the first one the SNI is www.chimieparistech.psl.eu but right after, the second one has for SNI : localhost.

Problem does not repeat when using Firefox or curl. 

#### System Information
Mitmproxy: 9.0.1
Python:    3.11.2
OpenSSL:   OpenSSL 3.0.7 1 Nov 2022
Platform:  Windows-10-10.0.22621-SP0
Chrome : Version 113.0.5672.127)
Edge : Version 113.0.1774.50)


Discussion:
Can you reproduce this on the main branch?
Yes I can reproduce it on the main branch.
I forgot an important fact : problem happens only one out of two times. Page loads correctly in the browser, but when I reload it fails. Or I click on a link, it fails but when I reload it loads correctly.
Thank you.
Thanks, that was the missing bit! Here's a simplified reproducer:
```
$ mitmdump --mode reverse:https://www.chimieparistech.psl.eu@8081 --set proxy_debug -vvv
$ curl https://localhost:8081/robots.txt https://localhost:8081/robots.txt -kq --rate 6/m
```
The problem is in this snippet here - not sure yet what's the best way to fix it:
https://github.com/mitmproxy/mitmproxy/blob/ab9429e3f6f831290f2a104a25b084f3f25b6452/mitmproxy/proxy/layers/http/__init__.py#L1040-L1046

Resolving patch:
diff --git a/mitmproxy/proxy/layers/http/__init__.py b/mitmproxy/proxy/layers/http/__init__.py
--- a/mitmproxy/proxy/layers/http/__init__.py
+++ b/mitmproxy/proxy/layers/http/__init__.py
@@ -1043,7 +1043,12 @@ def get_connection(
                     self.mode == HTTPMode.transparent
                     and event.address == self.context.server.address
                 ):
-                    context.server.sni = self.context.client.sni or event.address[0]
+                    # reverse proxy mode may set self.context.server.sni, which takes precedence.
+                    context.server.sni = (
+                        self.context.server.sni
+                        or self.context.client.sni
+                        or event.address[0]
+                    )
                 else:
                     context.server.sni = event.address[0]
                 if context.server.transport_protocol == "tcp":


Test patch:
diff --git a/test/mitmproxy/proxy/layers/http/test_http.py b/test/mitmproxy/proxy/layers/http/test_http.py
--- a/test/mitmproxy/proxy/layers/http/test_http.py
+++ b/test/mitmproxy/proxy/layers/http/test_http.py
@@ -1432,6 +1432,29 @@ def test_transparent_sni(tctx):
     assert server().sni == "example.com"
 
 
+def test_reverse_sni(tctx):
+    """Test that we use the destination address as SNI in reverse mode."""
+    tctx.client.sni = "localhost"
+    tctx.server.address = ("192.0.2.42", 443)
+    tctx.server.tls = True
+    tctx.server.sni = "example.local"
+
+    flow = Placeholder(HTTPFlow)
+
+    server = Placeholder(Server)
+    assert (
+        Playbook(http.HttpLayer(tctx, HTTPMode.transparent))
+        >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n")
+        << http.HttpRequestHeadersHook(flow)
+        >> reply()
+        << http.HttpRequestHook(flow)
+        >> reply()
+        << OpenConnection(server)
+    )
+    assert server().address == ("192.0.2.42", 443)
+    assert server().sni == "example.local"
+
+
 def test_original_server_disconnects(tctx):
     """Test that we correctly handle the case where the initial server conn is just closed."""
     tctx.server.state = ConnectionState.OPEN