Skip to content

Commit 2d74284

Browse files
committed
initial commit
0 parents  commit 2d74284

12 files changed

+195
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
__pycache__
2+
.vscode

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# sni-proxy
2+
3+
Bypass SNI-based censorship for sites that don't look at the Server Name Indication field in the TLS handshake. (Example list in [domains.yaml](./domains.yaml))
4+
5+
# Usage
6+
7+
As always, start with cloning the repo locally and make sure you're in the root folder.
8+
9+
0. Make sure you're using a [DoH/DoT enabled DNS server](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/encrypted-dns-browsers/) for website lookups and not your ISP's DNS otherwise this entire thing won't even work. Cloudflare's 1.1.1.1 is pretty good.
10+
11+
1. Install [mitmproxy](https://mitmproxy.org/).
12+
13+
2. Install [python 3](https://www.python.org/downloads/)
14+
15+
3. Install the dependencies with pip `pip3 install -r requirements.txt`
16+
17+
To simply start the proxy: `mitmproxy --ssl-insecure -s addon.py -p 8080`
18+
19+
## Certificate
20+
21+
For testing, I recommend using a separate browser like Firefox which keeps its own proxy/certificate settings separate from the system unlike chrome. Trying to proxy everything that's going on in your computer can be a bit of a hassle for when the proxy goes down.
22+
23+
Add the mitmproxy root CA to Firefox by visiting http://mitm.it and downloading the cert `.pem` file.
24+
25+
> If the site tells you that your traffic isn't being routed through the proxy, make sure you're on the `http` version of the site and not `https`. And also that you have mitmproxy running lol.
26+
27+
![](./assets/cert_settings.png)
28+
29+
![](./assets/cert_authority_import.png)
30+
31+
Just select the downloaded file at this point.
32+
33+
## Proxy
34+
35+
Firefox needs to be configured to use mitmproxy.
36+
37+
![](./assets/proxy_search.png)
38+
39+
![](./assets/proxy_config.png)
40+
41+
Make sure to also enable DNS over HTTPS in the settings below. You should basically always be using this as long as you don't live in China or Russia where it's blocked.
42+
43+
By itself it's not great for privacy, but with SNI tampering it's quite useful.
44+
45+
---
46+
47+
Congrats, you should be able to access some blocked websites now. If you run into another site you need unblocked, just add it into [domains.yaml](./domains.yaml) and pray the server doesn't look at SNI values.
48+
49+
## Debugging issues
50+
51+
### My pages are loading but they look like they're from 1998 with no CSS.
52+
53+
Chances are the site you're on is loading assets from other domains that the browser doesn't trust. The easiest way to get around this is to open devtools on the network tab and double click on the failing request to trust the certificate it presents temporarily.
54+
55+
![](./assets/cdn_trust.png)
56+
57+
![](./assets/security_risk.png)
58+
59+
### I'm getting `sslv3 alert handshake failure`
60+
61+
The site you're visiting terminated the TLS handshake because it cares about the SNI field. This is usually caused by the site being behind cloudflare or a similar multi-tenant reverse proxy. This can _sometimes_ be fixed by moving the domain you want to block into the `delicate_domains` field so that the proxy sends a substring of the correct SNI and adds some garbage at the end to fool the censor, but it will often not work.
62+
63+
This method unfortunately can't unblock every website.

addon.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
from typing import Iterable
2+
from mitmproxy import tls, http, connection, addonmanager
3+
from mitmproxy.addons.tlsconfig import TlsConfig
4+
from mitmproxy.http import HTTPFlow
5+
import yaml
6+
7+
8+
def matches_any(domains: Iterable[str], domain: str) -> bool:
9+
return any(to_unblock in domain for to_unblock in domains)
10+
11+
12+
class SNIProxy:
13+
config = TlsConfig()
14+
sni: str
15+
domains: Iterable[str]
16+
delicate_domains: Iterable[str]
17+
18+
def load(self, _loader: addonmanager.Loader):
19+
with open("./domains.yaml") as f:
20+
out = yaml.safe_load(f)
21+
self.domains = out.get("domains", [])
22+
self.delicate_domains = out.get("delicate_domains", [])
23+
self.sni = out.get("default_sni", "google.com")
24+
25+
def request(self, flow: HTTPFlow):
26+
if flow.request.scheme == "http":
27+
# Firefox displays an annoying message if it thinks its
28+
# captive portal check is being intercepted by a mitm redirect
29+
30+
# we also need mitm.it to be accessible in http so mitmproxy can
31+
# change the response to be a cert download page
32+
if flow.request.host in ("detectportal.firefox.com", "mitm.it"):
33+
# TBH I don't think this is working for firefox at all lol
34+
return
35+
36+
# Always redirect other non-https requests to https
37+
# since they will be blocked without TLS
38+
flow.response = http.Response.make(
39+
307,
40+
b"",
41+
{"Location": flow.request.url.replace("http://", "https://")},
42+
)
43+
44+
def tls_start_server(self, data: tls.TlsData):
45+
if isinstance(data.conn, connection.Server):
46+
(domain, _) = data.conn.address
47+
if matches_any(self.domains, domain):
48+
data.context.client.sni = self.sni
49+
elif matches_any(self.delicate_domains, domain):
50+
data.context.client.sni = rf"{domain}."
51+
52+
self.config.tls_start_server(data)
53+
54+
55+
addons = [SNIProxy()]

assets/cdn_trust.png

27.4 KB
Loading

assets/cert_authority_import.png

61.9 KB
Loading

assets/cert_settings.png

114 KB
Loading

assets/proxy_config.png

80 KB
Loading

assets/proxy_search.png

33.4 KB
Loading

assets/security_risk.png

211 KB
Loading

ban_check.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Optional
2+
import requests
3+
import re
4+
import datetime
5+
6+
7+
def check_turkish_ban_date(site: str) -> Optional[datetime.date]:
8+
"""
9+
Looks up when a site (domain + tld) was banned in turkey.
10+
Returns None if the site isn't banned.
11+
Works regardless of caller IP.
12+
Usage: `check_turkish_ban_date('wikileaks.org')`
13+
"""
14+
reg = re.compile(f"{site}, (?P<day>.*?)/(?P<month>.*?)/(?P<year>.*?) tarihli")
15+
16+
response = requests.get("http://195.175.254.2", headers={"Host": site})
17+
result = reg.search(response.text)
18+
19+
if not result:
20+
return None
21+
22+
(day, month, year) = result.groups()
23+
return datetime.date(int(year), int(month), int(day))

0 commit comments

Comments
 (0)