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:

  1. Download the latest release binary from GitHub
  2. Ask for your domain, passphrase, Telegram credentials, channels
  3. Login to Telegram interactively (one-time)
  4. 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

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 :53 directly requires root. It's better to listen on an unprivileged port (:5300) and port-forward 53 to it.

Replace eth0 with your actual network interface name (check with ip 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 5300

To 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

S
Description
Languages
Go 76.2%
HTML 17.9%
Kotlin 1.6%
JavaScript 1.6%
Shell 1.6%
Other 1%