Problem: Internet censorship in Iran uses Deep Packet Inspection (DPI) at the ISP level to detect and block proxy protocols in real time. A server that worked yesterday may be blocked today. This project continuously harvests V2Ray/XRAY proxy configurations from public Telegram channels and GitHub mirrors, tests each one under real Iranian network conditions, and serves only the verified working configs as a subscription URL compatible with V2Ray/XRAY clients.
┌─────────────────────────────────────────────────────────────┐
│ Sources │
│ Telegram Channels GitHub / Public HTTP Mirrors │
└─────────────┬──────────────────────────┬────────────────────┘
│ │
└──────────┬───────────────┘
▼
scanner/sources.py
(raw text → proxy URI strings)
│
▼
scanner/parser.py
(URI strings → ParsedConfig objects)
│
▼
TCP Probe ≤ 1050 ms?
scanner/probe.py
/ \
PASS FAIL → discard
│
▼
xray-core test
curl 100 KB through SOCKS5
│
/─────────\
PASS FAIL → discard
│
▼
ProbeResult(is_working=True, speed_kbps=N)
│
▼
scanner/engine.py → SQLite (Django ORM)
│
▼
GET /api/confs/
plain text, one link per line
(V2Ray / XRAY client compatible)
-
Fetch (
scanner/sources.py) Telethon reads the last 500 messages from each configured Telegram channel (today + yesterday). HTTP mirrors are fetched withrequests. All text is scanned with regex patterns forvless://,vmess://,trojan://,ss://URIs. -
Parse (
scanner/parser.py) Each raw URI is decoded into a frozenParsedConfigdataclass — host, port, user_id, protocol, remark, and method (ss only). vmess uses base64 + JSON. ss handles both encoded and plain-auth forms. Malformed links returnNoneand are silently dropped. -
TCP Probe (
scanner/probe.py) A fast TCP connect (2 s timeout) to(host, port)filters dead nodes. Latency ≥ 1050 ms counts as failure. This eliminates the majority of candidates before the expensive xray test. -
Deep Test (
scanner/probe.py) xray-core is launched with the config on a random local SOCKS5 port.curldownloads 100 KB through Cloudflare's speed endpoint (edge-cached, resistant to country-level filtering). Speed =100 KB / seconds. The xray process is killed via process-group signal and the temp config file deleted in afinallyblock — guaranteed regardless of outcome. -
Rank & Persist (
scanner/engine.py) WorkingProbeResultobjects are filtered and ranked byfilter_and_rank(). Nodes are bulk-upserted to SQLite. Existing nodes are re-tested on every run; failures are pruned. -
Serve (
scanner/views.py)GET /api/confs/returns allis_working=Truenodes as newline-separated links, ready to paste into V2RayNG, Clash, or any XRAY-compatible client.
One command installs everything from scratch — supports Linux, macOS, and Windows (Git Bash). Requires sudo on Linux (apt + systemd) and Homebrew on macOS.
bash <(curl -Ls https://raw.githubusercontent.com/sinachaichi/scanner/main/install.sh)Note: use
bash <(curl ...)— notcurl ... | bash. The pipe form breaks the interactive Telegram credential prompts.
The script will:
- Install system packages and Redis — apt on Linux, Homebrew on macOS, or verify requirements on Windows (Git Bash)
- Clone the repo to
~/scanner_project - Create a virtualenv and install Python dependencies
- Prompt for Telegram API credentials (press Enter or type
noto skip and use HTTP mirrors only; the API hash is read with hidden input) - Generate a fresh
SECRET_KEYandFIELD_ENCRYPTION_KEY, write.env(chmod600) - Run migrations and create an admin superuser (password set via
DJANGO_SUPERUSER_PASSWORD, never exposed in the process list) - Linux: register three systemd services (Gunicorn, Celery worker, Celery beat) and install global helper commands
- macOS: register three launchd LaunchAgents (no sudo needed; auto-start at login, restart on crash)
- Windows: start services in the background with
nohup - Print the admin panel URL and generated password
Prerequisites: Python 3.12, Redis, curl, Git.
git clone https://github.com/sinachaichi/scanner.git
cd scanner1. Virtualenv and dependencies
python3.12 -m venv .venv
source .venv/bin/activate # Windows Git Bash: source .venv/Scripts/activate
pip install -r requirements.txt2. xray binary
The xray binary is not included in the repo — download the correct build for your platform from the xray-core releases and place it in the project root. Quick one-liner per platform:
# macOS (Apple Silicon)
curl -fsSL -o /tmp/xray.zip https://github.com/XTLS/Xray-core/releases/latest/download/Xray-macos-arm64-v8a.zip && unzip -o /tmp/xray.zip xray
# macOS (Intel)
curl -fsSL -o /tmp/xray.zip https://github.com/XTLS/Xray-core/releases/latest/download/Xray-macos-64.zip && unzip -o /tmp/xray.zip xray
# Linux x86_64
curl -fsSL -o /tmp/xray.zip https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip && unzip -o /tmp/xray.zip xray
chmod +x xrayOr set XRAY_PATH in .env to point to an existing xray binary elsewhere on your system.
3. Environment
cp .env.example .envEdit .env and fill in at minimum SECRET_KEY and FIELD_ENCRYPTION_KEY (see Configuration below). The app will refuse to start without these two.
4. Database
python manage.py migrate
python manage.py createsuperuser5. Start services
redis-server &
# Redis uses RDB persistence by default; disable it since Celery data is ephemeral
redis-cli CONFIG SET save "" && redis-cli CONFIG SET stop-writes-on-bgsave-error no
celery -A config worker -Q scanner_queue --loglevel=info &
celery -A config beat --loglevel=info &
python manage.py runserver 0.0.0.0:8000After installation (either method):
- Open the admin panel at
http://<server-ip>:<port>/admin/and log in. - Under Mirrors, add one or more public config mirror URLs and mark them active.
(Optional) Under Channels, add Telegram channel usernames (e.g.
@someproxychannel) if you configured Telegram credentials. - Trigger a scan in one of two ways:
- Admin action: select mirrors or channels → choose "Scan and update nodes" from the action dropdown.
- Management command:
python manage.py run_full_scan(orscanner-manage run_full_scanon Linux after the automated install). Scans take 10–60 minutes depending on how many nodes are found.
- Once the scan completes, working nodes appear under Nodes.
- Copy the subscription URL:
http://<server-ip>:<port>/api/confs/ - Paste it into your V2Ray/XRAY client (V2RayNG, Nekoray, Clash Meta, etc.) as a subscription link.
Copy .env.example to .env and fill in all values.
Never commit .env to version control.
| Variable | Required | Description |
|---|---|---|
SECRET_KEY |
Yes | Django secret key. Generate: openssl rand -hex 32 |
FIELD_ENCRYPTION_KEY |
Yes | Fernet key for encrypting GitHub tokens at rest. Generate: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" |
TELEGRAM_API_ID |
No | From my.telegram.org → API development tools. Omit to use HTTP mirrors only. |
TELEGRAM_API_HASH |
No | Same source as API ID. Treat like a password — never share. |
DEBUG |
No | False in production (default). |
ALLOWED_HOSTS |
No | Comma-separated hostnames. Default: localhost. |
XRAY_PATH |
No | Path to xray binary. Default: ./xray. |
CELERY_BROKER_URL |
No | Redis URL. Default: redis://redis:6379/0. |
RANDOM_PORT |
No | Port for Gunicorn / CSRF origin. Default: 8000. |
LOG_LEVEL |
No | DEBUG, INFO, WARNING. Default: INFO. |
| Component | Choice | Why |
|---|---|---|
| Web framework | Django 5 | Admin UI manages channels/mirrors/nodes without custom frontend; ORM handles SQLite cleanly |
| Task queue | Celery + Redis | Scans take 20–60 min; Celery handles scheduling, retries, and result storage |
| Database | SQLite | Single-server deployment with no concurrent writers — zero ops overhead |
| Proxy engine | xray-core | Supports VLESS/VMess/Trojan/SS including REALITY and XTLS extensions |
| Telegram client | Telethon | Async MTProto — reads public channels without bot API rate limits |
| Field encryption | django-encrypted-model-fields | GitHub tokens stored encrypted at rest (Fernet symmetric encryption) |
Credentials are environment-only. Telegram api_id/api_hash and Django SECRET_KEY are loaded exclusively from environment variables. The application refuses to start if SECRET_KEY or FIELD_ENCRYPTION_KEY are absent. There are no hardcoded fallback values.
GitHub tokens are encrypted at rest. Subscription.token uses EncryptedCharField — the value stored in SQLite is Fernet-encrypted. The encryption key lives in .env and must never be committed.
Temp files use the OS temp directory. xray config files are written with tempfile.mkstemp() and deleted in a finally block, not left in the working directory.
Files that must never be committed:
.env— credentialsdb.sqlite3— may contain decrypted node datasession_name.session— Telegram session file (equivalent to a logged-in account)
pip install -r requirements-dev.txt
pytestTests mock all network calls and subprocess invocations — no real xray binary, Telegram connection, or internet access required.
tests/
test_parser.py — ParsedConfig parsing, edge cases, all four protocol decoders
test_probe.py — TcpProbe.ping/is_reachable, build_xray_config, XraySpeedTester.test
test_scanner.py — end-to-end pipeline orchestration and node re-testing
test_filter.py — filter_and_rank ordering and filtering logic
test_sources.py — MirrorSource fetch/parse + error handling, Telegram guard branches
pytest runs with coverage enabled and fails below 80% (see pyproject.toml).
The gate is scoped to the modules that hold real algorithmic logic — parsing,
probing, ranking, the scan pipeline, and source fetching. The thin Django
integration layers (ORM persistence, the GitHub HTTP push, views, Celery task
wrappers, the composition root) are excluded from the gate: their behaviour is
the framework's, not ours, so they're better covered by integration tests than
by unit-coverage on glue.
The Django admin at /admin/ provides:
- Channels — add/remove Telegram channel usernames, trigger per-channel scans
- Mirrors — add/remove HTTP mirror URLs, trigger per-mirror scans
- Nodes — view working configs with speed/latency columns, filter by protocol
- Subscriptions — configure GitHub push targets (token stored encrypted)
scanner-logs [web|celery|beat] tail service logs
scanner-restart restart all three services
scanner-stop stop all three services
scanner-manage <cmd> python manage.py <cmd>
scanner-celery <args> celery <args>
scanner-celery-task interactively pick and trigger a task