[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-2680":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":17,"stars7d":18,"stars30d":19,"stars90d":16,"forks30d":16,"starsTrendScore":20,"compositeScore":21,"rankGlobal":10,"rankLanguage":10,"license":22,"archived":23,"fork":23,"defaultBranch":24,"hasWiki":25,"hasPages":23,"topics":26,"createdAt":10,"pushedAt":10,"updatedAt":37,"readmeContent":38,"aiSummary":39,"trendingCount":16,"starSnapshotCount":16,"syncStatus":40,"lastSyncTime":41,"discoverSource":42},2680,"rkn-block-checker","MayersScott\u002Frkn-block-checker","MayersScott","Diagnose RKN\u002FTSPU internet blocks layer by layer (DNS, TCP, TLS, HTTP)","",null,"Python",1506,61,23,3,0,18,123,405,54,18.38,"MIT License",false,"main",true,[27,28,29,30,31,32,33,34,35,36],"censorship","cli","dns","dpi","network-diagnostics","networking","python","rkn","tls","tspu","2026-06-12 02:00:43","# RKN Block Checker\n\n[![PyPI version](https:\u002F\u002Fimg.shields.io\u002Fpypi\u002Fv\u002Frkn-block-checker.svg)](https:\u002F\u002Fpypi.org\u002Fproject\u002Frkn-block-checker\u002F)\n[![CI](https:\u002F\u002Fgithub.com\u002FMayersScott\u002Frkn-block-checker\u002Factions\u002Fworkflows\u002Fci.yml\u002Fbadge.svg)](https:\u002F\u002Fgithub.com\u002FMayersScott\u002Frkn-block-checker\u002Factions\u002Fworkflows\u002Fci.yml)\n[![Python](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Fpython-3.10%2B-blue)](https:\u002F\u002Fwww.python.org\u002Fdownloads\u002F)\n[![License: MIT](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002Flicense-MIT-green.svg)](LICENSE)\n\nA small CLI that figures out whether the connection you're sitting on is in\nan RKN\u002FTSPU-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\n*where* it broke. That tells you a lot more about your situation than a\ngeneric \"this site can't be reached\" page.\n\n## Quick start\n\n```bash\npip install rkn-block-checker\nrkn-check\n```\n\nThat's it. The tool probes a built-in list of sites, classifies each\nfailure by layer, and prints a verdict. No config, no setup, nothing to\nedit.\n\n## Example output\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     ~ LIKELY TLS DPI          22ms       -       -  -\n    └ TLS reset right after ClientHello - consistent with SNI-based DPI\n  twitter\u002Fx     ~ LIKELY TLS DPI          24ms       -       -  -\n    └ TLS handshake silently dropped - consistent with DPI filtering\n  rutracker     ✗ HTTP STUB               18ms    45ms   120ms  200\n    └ response body matches a known ISP stub-page marker\n  protonvpn     ✗ DNS                        -       -       -  -\n    └ system DNS doesn't resolve, DoH does - consistent with DNS poisoning\n\n======================================================================\n  Summary\n----------------------------------------------------------------------\n  Whitelist: 21\u002F21 working\n  Blacklist: 3\u002F15 open, 12\u002F15 blocked\n\n  → Likely in an RKN-blocked zone (medium confidence).\n    Most blacklist failures match censorship patterns (TLS DPI, TCP RST),\n    but those signals can also be caused by server-side issues. A control\n    vantage point would confirm.\n\n  Block types in the blacklist:\n    ~ LIKELY TLS DPI: 8\n    ✗ DNS: 2\n    ✗ HTTP STUB: 2\n======================================================================\n```\n\nVerdict labels are **calibrated by confidence**: `✗` means a high-confidence\ndiagnosis (e.g. DNS poisoning confirmed by DoH, HTTP 451, a known stub-page\nmarker), `~ LIKELY` means a known censorship pattern matched but a single\nsignal can't rule out a server-side issue, and `?` means the symptom is\nambiguous. The summary line says so plainly - \"high confidence\", \"medium\nconfidence\", or \"inconclusive\" - and never claims more certainty than the\nunderlying signals support.\n\n## Why this exists\n\nIf a site doesn't open, your browser tells you that. But if you want to\n*do* something about it - pick the right circumvention tool, file a useful\nbug report, or just understand what's happening to your traffic - you need\nto know which part of the network stack is actually being interfered with.\n\nDifferent censorship mechanisms leave different fingerprints:\n\n- **DNS poisoning** is the cheapest and oldest. The ISP's resolver lies\n  about a domain.\n- **TCP reset** is IP-level blackholing. Rare in practice - most ISPs\n  don't bother.\n- **TLS DPI on SNI** is the modern TSPU\u002FRKN signature. The middlebox\n  watches for the SNI extension in the TLS ClientHello and tears the\n  connection down once it sees a blocked hostname.\n- **HTTP stub pages** are the polite kind: an ISP-controlled page served\n  back with a \"blocked by RKN\" body, often with status 200 or the\n  rarer-but-explicit 451.\n\n`rkn-check` walks DNS → TCP → TLS → HTTP for each target and stops at the\nfirst thing that fails. Whichever layer broke becomes the verdict.\n\n## Common scenarios\n\n### Just diagnose the connection you're on\n\n```bash\nrkn-check\n```\n\nProbes the built-in lists (~21 control sites, ~15 RKN-restricted), prints\na per-site report and a summary verdict.\n\n### Check a single URL\n\n```bash\nrkn-check --url https:\u002F\u002Fexample.com\nrkn-check --url example.com --url google.com    # repeat for several\n```\n\nSkips the built-in lists entirely and runs an ad-hoc check against just\nthe URLs you pass. No summary verdict - there's no control group to\ncompare against. Use this when you want to know \"did *this one site* come\nthrough?\" without paying for a full scan.\n\n### Pipe to `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)\n                       | 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### Use your own target lists\n\n```bash\nrkn-check --black-file my-list.txt\nrkn-check --white-file my-control.json --black-file my-targets.json\n```\n\nSee [Custom target lists](#custom-target-lists) below for the file format.\n\n### Run from cron and store JSON over time\n\n```bash\nrkn-check --json --no-self-info > \"snapshots\u002F$(date -I).json\"\n```\n\n`--no-self-info` skips the public-IP lookup so the tool doesn't hit\n`ipinfo.io` on every cron tick (and so the resulting JSON doesn't carry\nyour IP).\n\n## Usage\n\n```text\nrkn-check [-h] [--json] [--white] [--black]\n          [--white-file PATH] [--black-file PATH] [--url URL]\n          [--timeout TIMEOUT] [--workers WORKERS] [-v]\n          [--no-self-info] [--identify]\n```\n\n| flag | what it does |\n|------|--------------|\n| `--json` | emit machine-readable JSON instead of the colored report |\n| `--white` | check only the control (whitelist) targets |\n| `--black` | check only the blacklist targets |\n| `--white-file PATH` | replace the built-in whitelist with a `.txt` or `.json` file |\n| `--black-file PATH` | replace the built-in blacklist with a `.txt` or `.json` file |\n| `--url URL` | probe a single URL or hostname; repeat for several. Skips built-in lists |\n| `--timeout T` | per-probe timeout in seconds (default 5.0) |\n| `--workers N` | thread pool size for parallel checks (default 10) |\n| `--no-self-info` | skip the public-IP lookup at the top of the report |\n| `--identify` | send a self-identifying User-Agent instead of a generic Chrome one. See [Privacy](#privacy-and-threat-model) |\n| `-v` \u002F `-vv` | logging at INFO \u002F DEBUG |\n\n`--white` and `--black` are mutually exclusive. `--url` cannot be combined\nwith `--white`\u002F`--black`\u002F`--white-file`\u002F`--black-file` - ad-hoc mode runs\nonly the URLs you pass.\n\n## How it works\n\nFor each target the tool walks DNS → TCP → TLS → HTTP and stops at the\nfirst thing that fails. Whichever layer broke becomes the verdict.\n\n| layer | probe | what a failure means |\n|------:|-------|----------------------|\n| DNS  | system resolver vs Cloudflare DoH, full address sets compared | sets agree but the system fails alone → DNS poisoning. Disjoint sets → transparent rewriting |\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\u002Ftimeout *here*, with TCP working fine, is the classic TSPU\u002FDPI 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 Roskomnadzor\" body |\n\nTwo probes are worth calling out:\n\n**System DNS vs DoH, set-based.** The cheapest way to \"block\" a site is\nto make the ISP's DNS lie. Every host is resolved twice - once via\n`getaddrinfo` (which uses whatever resolver the OS is configured for,\nusually the ISP's) and once via Cloudflare's DoH endpoint, which the ISP\ncan't intercept. The two **sets** of returned IPs are then compared:\ndisagreement only counts when the sets are **completely disjoint**. Any\nshared address is treated as load balancing, not poisoning - large sites\ntypically rotate the order of multiple A-records on every query, and\ncomparing only the first IP from each side produces false positives on\nevery other run.\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\nhave to actually start the TLS handshake to see this. A `TLS_BLOCK` after\na clean `TCP_OK` is the unambiguous fingerprint of DPI-based blocking.\n\n## Verdicts and confidence\n\nEvery result carries both a verdict and a confidence level. The verdict\nsays **what kind** of failure happened; the confidence says how\ntrustworthy the diagnosis is.\n\n| verdict | meaning |\n|---------|---------|\n| `OK` | the site loaded normally |\n| `DNS_BLOCK` | system DNS doesn't resolve while DoH does - consistent with poisoning |\n| `TCP_RESET` | TCP handshake answered with RST |\n| `TLS_BLOCK` | TCP succeeded but TLS handshake was reset, dropped, or otherwise killed (typical DPI on SNI) |\n| `HTTP_STUB` | the response was a known ISP stub page or HTTP 451 |\n| `TIMEOUT` | something timed out, not enough to classify further |\n| `DOWN` | resolution and connectivity both failed in ways that aren't censorship-shaped |\n| `UNKNOWN` | unexpected error, see notes |\n\nConfidence levels:\n\n- **HIGH** - two independent signals agree (e.g. DNS poisoning confirmed\n  by DoH, an explicit HTTP 451, a known stub-page marker in the body).\n- **MEDIUM** - a known censorship pattern matches, but the signal alone\n  doesn't rule out a server-side issue or a flaky path (TLS reset right\n  after ClientHello, TCP RST mid-stream).\n- **LOW** - symptom is ambiguous (a generic timeout, an unclassified\n  failure).\n\nThe summary line at the bottom mirrors this. With most blacklist failures\nmatching high-confidence patterns it says \"Likely in an RKN-blocked zone\n(high confidence)\". If most signals are medium it lowers the claim. And\nwhen the **whitelist** itself is mostly failing it doesn't claim either\nway - without a working baseline you can't separate censorship from a\nbroken uplink, so the summary becomes \"Inconclusive\".\n\n## Privacy and threat model\n\n`rkn-check` is a diagnostic tool, not a circumvention tool. But the\npeople running it are typically already under network surveillance of\nsome kind, so the defaults are chosen to minimize the footprint a single\nrun leaves behind.\n\n**User-Agent.** The default UA is a generic Chrome-on-Windows string with\nthe full set of browser-like headers (`Accept`, `Accept-Language`,\n`Sec-Fetch-*`, etc.). The earlier `Mozilla\u002F5.0 (RKN-Checker)` default was\nunique enough to fingerprint a tool run in any logs along the path -\nincluding, in some jurisdictions, VPN-provider logs that get handed to\nregulators on request. A generic UA blends the request in with normal\ntraffic. If you *want* to be seen as diagnostic tooling - for example\nwhen probing infrastructure you control - pass `--identify` to switch to\na self-identifying UA (`rkn-block-checker\u002F\u003Cver>`).\n\n**Public-IP lookup.** By default the tool fetches your IP\u002FISP\u002Flocation\nfrom `ipinfo.io` and prints it at the top of the report. This is purely\nfor the human reading the report - the diagnosis itself doesn't depend\non it. Pass `--no-self-info` to skip that lookup entirely; that's also\nthe right thing to do in cron scripts and in CI.\n\n**No telemetry.** The tool doesn't phone home. The only outbound\nconnections are: the per-target probes you asked for, the DoH lookup to\n`cloudflare-dns.com` (always on - it's the control side of the DNS\ncomparison), and the optional `ipinfo.io` lookup unless you disabled it.\n\n**No exfil of probe results.** Results are printed to stdout. They go\nnowhere else.\n\n## JSON output\n\n`--json` emits a single object containing `self_info` (the IP\u002FISP block\nfrom the header, or `null` if `--no-self-info` is set) and the result\nlists. Every result is the full per-target probe trace - which DNS\nresolvers returned what, whether TCP and TLS succeeded with timings, the\nHTTP status, the verdict, the confidence level, and human-readable notes.\n\nA trimmed sample (full version: [`docs\u002Fsample-output.json`](docs\u002Fsample-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:\u002F\u002Fwww.gosuslugi.ru\u002F\",\n      \"verdict\": \"OK\",\n      \"confidence\": \"HIGH\",\n      \"notes\": [],\n      \"sys_ip\": \"95.181.182.36\",\n      \"doh_ip\": \"95.181.182.36\",\n      \"sys_ips\": [\"95.181.182.36\"],\n      \"doh_ips\": [\"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,\n      \"tls_cert_cn\": \"*.gosuslugi.ru\",\n      \"status_code\": 200, \"plt_ms\": 380.7\n    }\n  ],\n  \"blacklist\": [\n    {\n      \"name\": \"instagram\",\n      \"url\": \"https:\u002F\u002Fwww.instagram.com\u002F\",\n      \"verdict\": \"TLS_BLOCK\",\n      \"confidence\": \"MEDIUM\",\n      \"notes\": [\"TLS reset right after ClientHello - consistent with SNI-based DPI filtering\"],\n      \"tcp_ok\": true,  \"tcp_time_ms\": 22.4,\n      \"tls_ok\": false, \"tls_error\": \"connection reset during TLS\"\n    },\n    {\n      \"name\": \"protonvpn\",\n      \"url\": \"https:\u002F\u002Fprotonvpn.com\u002F\",\n      \"verdict\": \"DNS_BLOCK\",\n      \"confidence\": \"HIGH\",\n      \"notes\": [\"system DNS doesn't resolve, DoH does - consistent with DNS poisoning\"],\n      \"sys_ip\": null, \"doh_ip\": \"185.70.40.182\",\n      \"sys_ips\": [], \"doh_ips\": [\"185.70.40.182\"],\n      \"dns_error\": \"system resolver failed, DoH succeeded\",\n      \"tcp_ok\": false\n    }\n  ]\n}\n```\n\n`sys_ip` \u002F `doh_ip` carry the lowest-sorted address from each set for\nbackward compatibility; `sys_ips` \u002F `doh_ips` carry the full sorted\nlists. The probe trace fields are always present so you can tell *why* a\nverdict was reached - a `TLS_BLOCK` with `tcp_ok: true` is the DPI-on-SNI\nsignature; one with `tcp_ok: false` would mean something else failed\nfirst.\n\n## Custom target lists\n\n`--white-file` and `--black-file` accept either JSON or plain text. The\nformat is picked by file extension (`.json` → JSON, anything else → text).\n\n**JSON format** - a flat object mapping name to URL:\n\n```json\n{\n  \"google\":   \"https:\u002F\u002Fgoogle.com\",\n  \"github\":   \"https:\u002F\u002Fgithub.com\",\n  \"rutracker\": \"https:\u002F\u002Frutracker.org\"\n}\n```\n\n**Text format** - one entry per line. Three forms are accepted:\n\n```text\n# bare URL - name auto-derived from the hostname\nhttps:\u002F\u002Fexample.com\n\n# name\u003Cwhitespace>URL\ngithub https:\u002F\u002Fgithub.com\n\n# name=URL\ncustom=https:\u002F\u002Fexample.org\n\n# blank lines and #-comments are skipped\n```\n\nURLs without a scheme get `https:\u002F\u002F` prepended. Duplicate names overwrite\n(with a warning logged); use unique names if both should be probed.\n\n## Install\n\nPython 3.10+.\n\nFrom PyPI:\n\n```bash\npip install rkn-block-checker\n```\n\nFrom source:\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002FMayersScott\u002Frkn-block-checker.git\ncd rkn-block-checker\npip install -e .\n```\n\nFor development (adds pytest and friends):\n\n```bash\npip install -e \".[dev]\"\n```\n\n## Layout\n\n```text\nrkn_checker\u002F\n  __main__.py     # python -m rkn_checker\n  cli.py          # argparse + entry point\n  core.py         # orchestrates DNS -> TCP -> TLS -> HTTP\n  dns.py          # system resolver + Cloudflare DoH (full address sets)\n  network.py      # raw TCP and TLS probes\n  http.py         # HTTP GET, header set, stub-page detection\n  output.py       # colored CLI report\n  lists.py        # parser for user-supplied target files\n  targets.py      # built-in whitelist, blacklist, stub markers\n  models.py       # CheckResult, Verdict, Confidence\ntests\u002F            # 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\nthe same 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\nwhen a `v*` tag is pushed. The workflow uses\n[PyPI Trusted Publishing](https:\u002F\u002Fdocs.pypi.org\u002Ftrusted-publishers\u002F) - no\nAPI token in repo secrets.\n\nTo ship a new version:\n\n```bash\n# bump version in pyproject.toml first, commit\ngit tag v0.3.4\ngit push origin v0.3.4\n```\n\nThe workflow checks that the tag matches `pyproject.toml`'s version,\nbuilds sdist + wheel, runs `twine check --strict`, publishes to PyPI,\nand attaches the artifacts to a GitHub Release with auto-generated\nnotes.\n\n## Caveats\n\n- IPv4 only. Some Russian ISPs treat IPv6 differently (often less\n  filtered) but the v4 path is what users actually experience in\n  practice.\n- The built-in target lists are small (~20 sites per category). That's\n  enough for a verdict but won't catch a block that affects only one\n  specific resource. Use `--url` for ad-hoc checks or `--white-file` \u002F\n  `--black-file` for your own lists.\n- One-shot snapshot, no retries, no longitudinal tracking. If you want\n  to monitor a connection over time, run `rkn-check --json` from cron\n  and store the snapshots.\n- Stub markers are mostly Russian-language phrases narrowed enough to\n  avoid false positives on unrelated articles that happen to mention\n  Roskomnadzor. New patterns get added when reported.\n\n## Acknowledgements\n\nThis project was significantly improved by people who looked at the code\ncritically and reported issues with concrete reproductions. Listed in the\norder their contributions landed:\n\n- [@vladon](https:\u002F\u002Fgithub.com\u002Fvladon) - security holes, misclassifications\n  and edge case fixes ([#1](https:\u002F\u002Fgithub.com\u002FMayersScott\u002Frkn-block-checker\u002Fpull\u002F1)):\n  silent DNS pipeline failures, TLS misclassifications, narrowed stub markers,\n  validation, `--no-self-info` flag, and four new test files.\n- [@easymoney322](https:\u002F\u002Fgithub.com\u002Feasymoney322) - flagged the unique-UA\n  fingerprinting risk\n  ([#2](https:\u002F\u002Fgithub.com\u002FMayersScott\u002Frkn-block-checker\u002Fissues\u002F2)). The\n  threat model around VPN-provider logs was the right one to raise; led to\n  the generic Chrome UA default and the `--identify` opt-in.\n- [@rlobanov](https:\u002F\u002Fgithub.com\u002Frlobanov) - pointed out that ad-hoc URL\n  checking required editing source files\n  ([#4](https:\u002F\u002Fgithub.com\u002FMayersScott\u002Frkn-block-checker\u002Fissues\u002F4)); led to\n  the `--url` flag (repeatable) for one-shot probes without touching the\n  built-in lists.\n- [@AndreyKopeyko](https:\u002F\u002Fgithub.com\u002FAndreyKopeyko) - caught a real false\n  positive in DNS rewriting detection on multi-A-record sites\n  ([#5](https:\u002F\u002Fgithub.com\u002FMayersScott\u002Frkn-block-checker\u002Fissues\u002F5)). The\n  reproduction with `host(1)` made the bug obvious; led to set-based DNS\n  comparison that only flags rewriting when the address sets are completely\n  disjoint.\n- [@tagantank](https:\u002F\u002Fgithub.com\u002Ftagantank) — Docker \u002F Compose support\n  ([#3](https:\u002F\u002Fgithub.com\u002FMayersScott\u002Frkn-block-checker\u002Fpull\u002F3)) so the\n  tool can be run without touching the host's Python environment.\n\nIf you spot something off, open an issue with a reproduction - that's the\nsingle most useful thing you can do.\n\n## License\n\nMIT.","RKN Block Checker 是一个用于诊断互联网连接是否受到RKN\u002FTSPU封锁的命令行工具。它能够逐层分析DNS、TCP、TLS和HTTP协议，以确定具体的封锁类型，如DNS污染、TCP重置、基于SNI的TLS深度包检测或ISP占位页面。该工具使用Python编写，具有简洁直观的操作界面，无需配置即可运行。适用于需要了解网络连接具体受限情况的用户，特别是在面临内容审查或网络限制时，帮助用户更准确地定位问题所在，从而采取相应的解决措施。",2,"2026-06-11 02:50:41","CREATED_QUERY"]