thefeed
DNS-based feed reader for Telegram channels. Designed for environments where only DNS queries work.
How It Works
┌──────────────┐ DNS TXT Query ┌──────────────┐ MTProto ┌──────────┐
│ Client │ ──────────────────────▸ │ Server │ ──────────────▸ │ Telegram │
│ (Web UI) │ ◂────────────────────── │ (DNS auth) │ ◂────────────── │ API │
└──────────────┘ Encrypted TXT └──────────────┘ └──────────┘
Server (runs outside censored network):
- Connects to Telegram, reads messages from configured channels
- Serves feed data as encrypted DNS TXT responses
- Random padding on responses to vary size (anti-DPI)
- Session persistence — login once, run forever
- All data stored in a single directory
Client (runs inside censored network):
- Browser-based web UI with RTL/Farsi support (VazirMatn font)
- Configure via the web UI — no CLI flags needed
- Sends encrypted DNS TXT queries via available resolvers
- Single-label base32 encoding (stealthier) or double-label hex
- Rate limiting to respect resolver limits
- Live DNS query log in the browser
- All data (config, cache) stored next to the binary
Anti-DPI Features
- Variable response size: Random padding (0-32 bytes) on each DNS response prevents fingerprinting by fixed packet size
- Single-label queries: Base32 encoded subdomain in one DNS label (
abc123def.t.example.com) instead of the more detectable two-label hex pattern - Resolver shuffling: Queries are distributed across resolvers randomly
- Rate limiting: Configurable query rate to blend with normal DNS traffic
- Concurrency limiting: Max 3 concurrent block fetches to avoid DNS bursts
- Random query padding: 4 random bytes in each query payload
Protocol
Block size: 180 bytes payload (fits in 512-byte UDP DNS with padding + encryption overhead)
Query format (single-label, default): [base32_encrypted].t.example.com
Query format (double-label): [hex_part1].[hex_part2].t.example.com
- Payload: 4 random bytes + 2 channel + 2 block = 8 bytes, AES-256-GCM encrypted
Response: [2-byte length][data][random padding] → AES-256-GCM encrypted → Base64
Encryption: AES-256-GCM with HKDF-derived keys from shared passphrase
Quick Install (Server)
One-line install (downloads latest release from GitHub)
bash <(curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/main/scripts/install.sh)
Or manually:
# On your server (Linux with systemd)
curl -Ls https://raw.githubusercontent.com/sartoopjj/thefeed/main/scripts/install.sh -o install.sh
sudo bash install.sh
The script will:
- Download the latest release binary from GitHub
- Ask for your domain, passphrase, Telegram credentials, channels
- Login to Telegram interactively (one-time)
- Set up a systemd service
Update: sudo bash install.sh (detects existing config, only updates binary)
Re-login: sudo bash install.sh --login
Uninstall: sudo bash install.sh --uninstall
Manual Setup
Prerequisites
- Go 1.26+
- Telegram API credentials from https://my.telegram.org
- A domain with NS records pointing to your server
Server
# Build
make build-server
# First run: login to Telegram and save session
./build/thefeed-server \
--login-only \
--data-dir ./data \
--domain t.example.com \
--key "your-secret-passphrase" \
--api-id 12345 \
--api-hash "your-api-hash" \
--phone "+1234567890"
# Normal run (uses saved session from data directory)
./build/thefeed-server \
--data-dir ./data \
--domain t.example.com \
--key "your-secret-passphrase" \
--api-id 12345 \
--api-hash "your-api-hash" \
--phone "+1234567890" \
--listen ":5300"
All data files (session, channels) are stored in the --data-dir directory (default: ./data).
Environment variables: THEFEED_DOMAIN, THEFEED_KEY, TELEGRAM_API_ID, TELEGRAM_API_HASH, TELEGRAM_PHONE, TELEGRAM_PASSWORD
Server Flags
| Flag | Default | Description |
|---|---|---|
--data-dir |
./data |
Data directory for channels, session, config |
--domain |
DNS domain (required) | |
--key |
Encryption passphrase (required) | |
--channels |
{data-dir}/channels.txt |
Path to channels file |
--api-id |
Telegram API ID (required) | |
--api-hash |
Telegram API Hash (required) | |
--phone |
Telegram phone number (required) | |
--session |
{data-dir}/session.json |
Path to Telegram session file |
--login-only |
false |
Authenticate to Telegram, save session, exit |
--listen |
:5300 |
DNS listen address |
--padding |
32 |
Max random padding bytes (0=disabled) |
--version |
Show version and exit |
Client
# Build
make build-client
# Run (opens web UI in browser)
./build/thefeed-client
# Custom data directory and port
./build/thefeed-client --data-dir ./mydata --port 9090
On first run, the client creates a ./thefeeddata/ directory next to where you run it. Open http://127.0.0.1:8080 in your browser and configure your domain, passphrase, and resolvers through the Settings page.
All configuration, cache, and data files are stored in the data directory.
Client Flags
| Flag | Default | Description |
|---|---|---|
--data-dir |
./thefeeddata |
Data directory for config, cache |
--port |
8080 |
Web UI port |
--version |
Show version and exit |
Web UI
The browser-based UI has:
- Channels sidebar (left): channel list with selection
- Messages panel (right): messages with native RTL/Farsi rendering (VazirMatn font)
- Log panel (bottom): live DNS query log
- Settings modal: configure domain, passphrase, resolvers, query mode, rate limit
Development
make test # Run tests
make build # Build both binaries
make build-all # Cross-compile all platforms
make vet # Go vet
make fmt # Format code
make clean # Remove build artifacts
DNS Records Setup
You need two DNS records on your domain. Suppose your server IP is 203.0.113.10 and you want to use example.com:
1. A Record for the NS server
| Type | Name | Value |
|---|---|---|
| A | ns.example.com |
203.0.113.10 |
This points a hostname to your server IP.
2. NS Record for the tunnel subdomain
| Type | Name | Value |
|---|---|---|
| NS | t.example.com |
ns.example.com |
This delegates all DNS queries for t.example.com (and its subdomains) to your server.
Note: The server needs to receive packets on external port 53. Running on
:53directly requires root. It's better to listen on an unprivileged port (:5300) and port-forward 53 to it.Replace
eth0with your actual network interface name (check withip a):sudo iptables -I INPUT -p udp --dport 5300 -j ACCEPT sudo iptables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300 sudo ip6tables -I INPUT -p udp --dport 5300 -j ACCEPT sudo ip6tables -t nat -I PREROUTING -i eth0 -p udp --dport 53 -j REDIRECT --to-ports 5300To make these rules persistent across reboots:
sudo apt install iptables-persistent # Debian/Ubuntu sudo netfilter-persistent save
channels.txt Format
# Comments start with #
@VahidOnline
Security
- All queries and responses are encrypted with AES-256-GCM
- Separate HKDF-derived keys for queries and responses
- Random padding in queries prevents caching and replay
- Random padding in responses prevents DPI size fingerprinting
- No session state — each query is independent
- Pre-shared passphrase required for both client and server
- Telegram 2FA password is prompted interactively (not stored in CLI args)
- Session file stored with 0600 permissions
Service Management
# After install.sh
systemctl status thefeed-server
systemctl restart thefeed-server
journalctl -u thefeed-server -f
# Update channels
sudo vi /opt/thefeed/data/channels.txt
sudo systemctl restart thefeed-server
# Update binary
sudo bash scripts/install.sh
License
MIT