{"id":49656625,"url":"https://github.com/mayersscott/rkn-block-checker","last_synced_at":"2026-05-10T14:02:31.603Z","repository":{"id":355593249,"uuid":"1228122765","full_name":"MayersScott/rkn-block-checker","owner":"MayersScott","description":"Diagnose RKN/TSPU internet blocks layer by layer (DNS, TCP, TLS, HTTP)","archived":false,"fork":false,"pushed_at":"2026-05-06T08:30:39.000Z","size":56,"stargazers_count":100,"open_issues_count":0,"forks_count":4,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-05-06T10:50:10.626Z","etag":null,"topics":["censorship","cli","dns","dpi","network-diagnostics","networking","python","rkn","tls","tspu"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/MayersScott.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-03T16:15:38.000Z","updated_at":"2026-05-06T10:45:03.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/MayersScott/rkn-block-checker","commit_stats":null,"previous_names":["mayersscott/rkn-block-checker"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/MayersScott/rkn-block-checker","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MayersScott%2Frkn-block-checker","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MayersScott%2Frkn-block-checker/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MayersScott%2Frkn-block-checker/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MayersScott%2Frkn-block-checker/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/MayersScott","download_url":"https://codeload.github.com/MayersScott/rkn-block-checker/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/MayersScott%2Frkn-block-checker/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32734391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-07T02:14:30.463Z","status":"ssl_error","status_checked_at":"2026-05-07T02:14:29.405Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["censorship","cli","dns","dpi","network-diagnostics","networking","python","rkn","tls","tspu"],"created_at":"2026-05-06T10:02:12.514Z","updated_at":"2026-05-07T11:01:10.301Z","avatar_url":"https://github.com/MayersScott.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RKN Block Checker\n\n[![PyPI version](https://img.shields.io/pypi/v/rkn-block-checker.svg)](https://pypi.org/project/rkn-block-checker/)[![CI](https://github.com/MayersScott/rkn-block-checker/actions/workflows/ci.yml/badge.svg)](https://github.com/MayersScott/rkn-block-checker/actions/workflows/ci.yml)\n[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)\n[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)\n\nA small CLI that figures out whether the connection you're sitting on is in an\nRKN/TSPU-blocked zone — and, more usefully, **what kind** of block it is\n(DNS poisoning, TCP reset, TLS DPI on SNI, or an ISP stub page).\n\nThe point isn't \"site X doesn't open.\" Browsers already tell you that. The\npoint is to look at each layer of the stack independently and report *where*\nit broke. That tells you a lot more about your situation than a generic\n\"this site can't be reached\" page.\n\n## Example output\n\n![rkn-check sample output](docs/sample-output.svg)\n\n\u003cdetails\u003e\n\u003csummary\u003eSame output as plain text\u003c/summary\u003e\n\n```text\n======================================================================\n  RKN Block Checker\n======================================================================\n  IP:       95.165.xxx.xxx\n  ISP:      AS12389 Rostelecom\n  Location: Moscow, Moscow, RU\n----------------------------------------------------------------------\n\nWhitelist (should always work)\n  name          verdict            TCP     TLS     PLT  status\n  ------------------------------------------------------------\n  gosuslugi     ✓ OK              18ms    42ms   380ms  200\n  yandex        ✓ OK               8ms    25ms    95ms  200\n  sberbank      ✓ OK              12ms    38ms   250ms  200\n  vk            ✓ OK               9ms    28ms   180ms  200\n  ...\n\nBlacklist (RKN-restricted)\n  name          verdict            TCP     TLS     PLT  status\n  ------------------------------------------------------------\n  instagram     ✗ TLS BLOCK       22ms       —       —  —\n    └ TLS reset — DPI cutting on SNI (typical RKN/TSPU)\n  twitter/x     ✗ TLS BLOCK       24ms       —       —  —\n    └ TLS timeout — silent drop after ClientHello\n  rutracker     ✗ HTTP STUB       18ms    45ms   120ms  200\n    └ response body matches an ISP stub-page marker\n  protonvpn     ✗ DNS BLOCK          —       —       —  —\n    └ system DNS doesn't resolve, DoH does — DNS poisoning\n  ...\n\n======================================================================\n  Summary\n----------------------------------------------------------------------\n  Whitelist: 21/21 working\n  Blacklist: 3/15 open, 12/15 blocked\n\n  → You ARE in an RKN-blocked zone.\n\n  Block types in the blacklist:\n    ✗ TLS BLOCK: 8\n    ✗ DNS BLOCK: 2\n    ✗ HTTP STUB: 2\n======================================================================\n```\n\n\u003c/details\u003e\n\n## Install\n\nPython 3.10+.\n\n```bash\npip install rkn-block-checker\nrkn-check\n```\n\nOr from source:\n\n```bash\ngit clone https://github.com/MayersScott/rkn-block-checker.git\ncd rkn-block-checker\npip install -e .\nrkn-check\n```\n\n## Usage\n\n```text\nrkn-check [-h] [--json] [--white] [--black] [--timeout TIMEOUT]\n          [--workers WORKERS] [-v]\n```\n\n| flag | what it does |\n|------|--------------|\n| `--json` | machine-readable JSON instead of the colored report |\n| `--white` | only the control (whitelist) targets |\n| `--black` | only the blacklist targets |\n| `--timeout` | per-probe timeout in seconds (default 5.0) |\n| `--workers` | thread pool size for parallel checks (default 10) |\n| `-v` / `-vv` | logging at INFO / DEBUG |\n\n## JSON output\n\n`--json` emits one object containing `self_info` (the IP/ISP block from the\nheader) and the two result lists. Every result is the full per-target probe\ntrace: which DNS resolver returned what, whether TCP and TLS succeeded with\ntimings, the HTTP status, the verdict, and human-readable notes.\n\nA trimmed sample (full version: [`docs/sample-output.json`](docs/sample-output.json)):\n\n```json\n{\n  \"self_info\": {\n    \"ip\": \"95.165.xxx.xxx\",\n    \"city\": \"Moscow\",\n    \"country\": \"RU\",\n    \"org\": \"AS12389 Rostelecom\"\n  },\n  \"whitelist\": [\n    {\n      \"name\": \"gosuslugi\",\n      \"url\": \"https://www.gosuslugi.ru/\",\n      \"verdict\": \"OK\",\n      \"notes\": [],\n      \"sys_ip\": \"95.181.182.36\",\n      \"doh_ip\": \"95.181.182.36\",\n      \"dns_mismatch\": false,\n      \"tcp_ok\": true,  \"tcp_time_ms\": 18.4,\n      \"tls_ok\": true,  \"tls_time_ms\": 42.1, \"tls_cert_cn\": \"*.gosuslugi.ru\",\n      \"status_code\": 200, \"plt_ms\": 380.7\n    }\n  ],\n  \"blacklist\": [\n    {\n      \"name\": \"instagram\",\n      \"url\": \"https://www.instagram.com/\",\n      \"verdict\": \"TLS_BLOCK\",\n      \"notes\": [\"TLS reset — DPI cutting on SNI (typical RKN/TSPU)\"],\n      \"sys_ip\": \"157.240.20.174\", \"doh_ip\": \"157.240.20.174\",\n      \"tcp_ok\": true,  \"tcp_time_ms\": 22.4,\n      \"tls_ok\": false, \"tls_error\": \"connection reset by peer\"\n    },\n    {\n      \"name\": \"protonvpn\",\n      \"url\": \"https://protonvpn.com/\",\n      \"verdict\": \"DNS_BLOCK\",\n      \"notes\": [\"system DNS doesn't resolve, DoH does — DNS poisoning\"],\n      \"sys_ip\": null, \"doh_ip\": \"185.70.40.182\",\n      \"dns_error\": \"system resolver failed, DoH succeeded\",\n      \"tcp_ok\": false\n    }\n  ]\n}\n```\n\n`verdict` is one of `OK`, `DNS_BLOCK`, `TCP_RESET`, `TLS_BLOCK`, `HTTP_STUB`,\n`TIMEOUT`, `DOWN`, or `UNKNOWN`. The probe trace fields (`sys_ip`, `tcp_ok`,\n`tls_ok`, etc.) are always present so you can tell *why* a verdict was reached\n— a `TLS_BLOCK` with `tcp_ok: true` is the DPI-on-SNI signature; one with\n`tcp_ok: false` would mean something else failed first.\n\nPipes nicely into `jq`:\n\n```bash\n# names of every blocked site\nrkn-check --json | jq -r '.blacklist[] | select(.verdict != \"OK\") | .name'\n\n# count by block type\nrkn-check --json | jq '.blacklist | group_by(.verdict) | map({verdict: .[0].verdict, count: length})'\n\n# only DPI-style blocks (TCP fine, TLS dies)\nrkn-check --json | jq '.blacklist[] | select(.verdict == \"TLS_BLOCK\" and .tcp_ok)'\n```\n\n## How it works\n\nFor each target the tool walks DNS → TCP → TLS → HTTP and stops at the first\nthing that fails. Whichever layer broke becomes the verdict.\n\n| layer | probe | what a failure means |\n|------:|-------|----------------------|\n| DNS  | system resolver vs Cloudflare DoH | if only the system fails, the ISP is poisoning DNS — the cheapest, oldest form of blocking |\n| TCP  | plain TCP handshake on :443 | a `RST` is IP-level blackholing. Rare — most ISPs don't bother |\n| TLS  | TLS handshake with SNI = target host | reset/timeout *here* (with TCP working fine) is the classic TSPU/DPI signature: the middlebox sees the SNI and tears the connection down |\n| HTTP | `GET` after handshake completes | 451, or an ISP stub page returning 200 with a \"blocked by RKN\" body |\n\nTwo probes are worth calling out:\n\n**System DNS vs DoH.** The cheapest way to \"block\" a site is to make the\nISP's DNS lie. Every host is resolved twice — once via `socket` (which uses\nwhatever resolver the OS is configured for, usually the ISP's) and once via\nCloudflare's DoH endpoint, which the ISP can't intercept. Disagreement is\nthe smoking gun.\n\n**TLS handshake with SNI.** Modern TSPU equipment doesn't drop the TCP\nconnection — it lets you connect, reads the SNI extension out of the\nClientHello, and *then* sends a RST or simply stops responding. So we have\nto actually start the TLS handshake to see this. A `TLS_BLOCK` after a clean\n`TCP_OK` is the unambiguous fingerprint of DPI-based blocking.\n\n## Layout\n\n```text\nrkn_checker/\n  __main__.py     # python -m rkn_checker\n  cli.py          # argparse + entry point\n  core.py         # orchestrates DNS -\u003e TCP -\u003e TLS -\u003e HTTP\n  dns.py          # system resolver + Cloudflare DoH\n  network.py      # raw TCP and TLS probes\n  http.py         # HTTP GET + stub-page detection\n  output.py       # colored CLI report\n  targets.py      # whitelist, blacklist, stub markers\n  models.py       # CheckResult, Verdict\ntests/            # pytest, all network calls mocked\n```\n\n## Tests\n\n```bash\npip install -e \".[dev]\"\npytest\n```\n\nNo network calls in the test suite — every probe is mocked, so it runs the\nsame in CI, on a plane, or behind a corporate proxy.\n\n## Releasing\n\nReleases are pushed to PyPI automatically by the `release.yml` workflow when a\n`v*` tag is pushed. The workflow uses\n[PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — no API\ntoken in repo secrets.\n\nOne-time setup on PyPI: add a pending publisher pointing at this repo, the\n`release.yml` workflow, and the `pypi` environment. Then to ship `0.2.1`:\n\n```bash\n# bump version in pyproject.toml first, commit\ngit tag v0.2.1\ngit push origin v0.2.1\n```\n\nThe workflow checks that the tag matches `pyproject.toml`'s version, builds\nsdist + wheel, runs `twine check --strict`, publishes to PyPI, and attaches\nthe artifacts to a GitHub Release with auto-generated notes.\n\n## Caveats\n\n- IPv4 only. Some Russian ISPs treat IPv6 differently (often less filtered)\n  but the v4 path is what users actually experience in practice.\n- The target lists are hard-coded (~20 sites per category). That's enough\n  for a verdict but won't catch a block that affects only one specific\n  resource. To extend — `rkn_checker/targets.py`.\n- One-shot snapshot, no retries, no longitudinal tracking. If you want to\n  monitor a connection over time, run `rkn-check --json` from cron.\n- Stub markers are mostly Russian-language phrases; false positives on\n  unrelated sites that happen to contain the same words are theoretically\n  possible but I haven't seen one yet.\n\n## License\n\nMIT.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmayersscott%2Frkn-block-checker","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmayersscott%2Frkn-block-checker","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmayersscott%2Frkn-block-checker/lists"}