├── .gitignore ├── requirements.txt ├── assets ├── cdn_trust.png ├── comparison.png ├── proxy_config.png ├── proxy_search.png ├── cert_settings.png ├── security_risk.png └── cert_authority_import.png ├── ban_check.py ├── domains.yaml ├── addon.py └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .vscode -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyyaml>=6.0 2 | mitmproxy>=8.0 -------------------------------------------------------------------------------- /assets/cdn_trust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetera/sni-proxy/HEAD/assets/cdn_trust.png -------------------------------------------------------------------------------- /assets/comparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetera/sni-proxy/HEAD/assets/comparison.png -------------------------------------------------------------------------------- /assets/proxy_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetera/sni-proxy/HEAD/assets/proxy_config.png -------------------------------------------------------------------------------- /assets/proxy_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetera/sni-proxy/HEAD/assets/proxy_search.png -------------------------------------------------------------------------------- /assets/cert_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetera/sni-proxy/HEAD/assets/cert_settings.png -------------------------------------------------------------------------------- /assets/security_risk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetera/sni-proxy/HEAD/assets/security_risk.png -------------------------------------------------------------------------------- /assets/cert_authority_import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Xetera/sni-proxy/HEAD/assets/cert_authority_import.png -------------------------------------------------------------------------------- /ban_check.py: -------------------------------------------------------------------------------- 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.*?)/(?P.*?)/(?P.*?) 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)) 24 | -------------------------------------------------------------------------------- /domains.yaml: -------------------------------------------------------------------------------- 1 | # Modify the domains list in this document 2 | 3 | # The SNI value that will be used for all domains. 4 | # Some sites like nordvpn.com check to make sure this 5 | # is a valid domain. 6 | default_sni: google.com 7 | 8 | # All root domains that will be replaced with the default SNI above. 9 | # This also includes CDN domains that sites sometimes use outside of their 10 | # own root domain 11 | domains: 12 | # VPN 13 | - privateinternetaccess.com 14 | - expressvpn.com # Not working | AWS Cloudfront blocks all domain fronting 15 | - ipvanish.com # Not working | Cloudflare 16 | 17 | # news / information 18 | - dw.com 19 | - jinpanel.com 20 | - wikileaks.com 21 | - wikileaks.org 22 | 23 | # social media 24 | - twitter.com # not normally blocked, but just in case 25 | - twimg.com 26 | 27 | # nsfw 28 | - pornhub.com 29 | - redtube.com 30 | - phncdn.com 31 | - youporn.com 32 | - xnxx.com 33 | - xnxx-cdn.com 34 | - xvideos.com 35 | - xvideos-cdn.com 36 | - nhentai.net # Blocked by local cloudflare POP traffic 37 | 38 | # Delicate Domains don't work without including a substring of the original domain 39 | # in their SNI field. They get garbage values appended to fool the censor without 40 | # breaking the target server. 41 | # If something doesn't work in the `domains` field. Try moving it over to here 42 | delicate_domains: 43 | # VPN 44 | - nordvpn.com 45 | - nordcdn.com 46 | - nordaccount.com 47 | 48 | # nsfw 49 | - xhamster.com 50 | - hentaihaven.xxx # Blocked by local cloudflare POP traffic 51 | -------------------------------------------------------------------------------- /addon.py: -------------------------------------------------------------------------------- 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()] 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 | ![attempting to view wikileaks on 2 browers. One not working with a proxy, and the other working with sni-proxy](./assets/comparison.png) 6 | 7 | > Left: Chrome (regular connection) 8 | > 9 | > Right: Firefox (proxied connection) 10 | 11 | # Usage 12 | 13 | As always, start with cloning the repo locally and make sure you're in the root folder. 14 | 15 | 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. 16 | 17 | 1. Install [mitmproxy](https://mitmproxy.org/). 18 | 19 | 2. Install [python 3](https://www.python.org/downloads/) 20 | 21 | 3. Install the dependencies with pip `pip3 install -r requirements.txt` 22 | 23 | To simply start the proxy: `mitmproxy --ssl-insecure -s addon.py -p 8080` 24 | 25 | ## Certificate 26 | 27 | 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. 28 | 29 | Add the mitmproxy root CA to Firefox by visiting http://mitm.it and downloading the cert `.pem` file. 30 | 31 | > 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. 32 | 33 | ![](./assets/cert_settings.png) 34 | 35 | ![](./assets/cert_authority_import.png) 36 | 37 | Just select the downloaded file at this point. 38 | 39 | ## Proxy 40 | 41 | Firefox needs to be configured to use mitmproxy. 42 | 43 | ![](./assets/proxy_search.png) 44 | 45 | ![](./assets/proxy_config.png) 46 | 47 | 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. 48 | 49 | By itself it's not great for privacy, but with SNI tampering it's quite useful. 50 | 51 | --- 52 | 53 | 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. 54 | 55 | ## Debugging issues 56 | 57 | ### My pages are loading but they look like they're from 1998 with no CSS. 58 | 59 | 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. 60 | 61 | ![](./assets/cdn_trust.png) 62 | 63 | ![](./assets/security_risk.png) 64 | 65 | ### I'm getting `sslv3 alert handshake failure` 66 | 67 | 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. 68 | 69 | This method unfortunately can't unblock every website. 70 | --------------------------------------------------------------------------------