Answer DNS TXT queries using an LLM (OpenAI or Anthropic). This lets you query LLMs directly via DNS:
dig @127.0.0.1 -p 5353 summarize.interstellar.for.me TXT +short
- How It Works
- Server Architecture
- Notes
- Setup
- Usage Options
- Custom Models
- Response Length Control
- Running on port 53
- Security
- License
This system bridges DNS and LLMs through a clever encoding scheme:
-
DNS Query: You send a DNS TXT query where the domain name encodes your question
dig @127.0.0.1 -p 5353 summarize.interstellar.for.me TXT +short -
Domain Parsing: The DNS server extracts the domain labels
["summarize", "interstellar", "for", "me"] -
Text Reconstruction: Labels are joined with spaces to form the question:
"summarize interstellar for me" -
LLM Processing: The question is sent to your chosen LLM provider (OpenAI or Anthropic)
-
Response Formatting: The LLM response is chunked to fit DNS TXT record limits (≤255 bytes per string)
-
DNS Response: The server returns the answer as DNS TXT records that dig displays
The running server handles DNS-to-LLM translation through several components:
DNS Server Layer (main.py):
- Listens on UDP/TCP ports (default 5353) using
dnslib - Accepts only TXT record queries (returns NOTIMP for other types)
- Spawns threads for concurrent request handling
Request Processing (LLMResolver.resolve()):
- Extracts
qname(domain name) from incoming DNS request - Calls
labels_to_question()to convert DNS labels to natural language - Calls
llm_answer()to get LLM response - Calls
chunk_text_for_txt_record()to split response for DNS TXT format - Constructs DNS reply with TXT records containing the chunked response
Text Processing (utils/text_formatting.py):
labels_to_question(): DNS labels → human-readable question- Handles dots, underscores, URL encoding, base64 encoding
- Joins labels with spaces:
["summarize", "interstellar", "for", "me"]→"summarize interstellar for me"
chunk_text_for_txt_record(): Long text → DNS-compatible chunks- Splits responses to fit 255-byte TXT string limits
- Preserves word boundaries when possible
LLM Integration (utils/ai_providers.py):
generate_with_provider(): Routes to OpenAI or Anthropic APIs- Handles model selection, API key management, error handling
- Truncates responses to ~400 chars to fit UDP packet limits
- Returns plain text responses ready for DNS encoding
Configuration (config.py):
- Loads API keys from environment variables via
python-dotenv - Manages provider-specific settings and defaults
Why DNS?
- Universal protocol - works anywhere with internet access
- No special clients needed - just use
dig - Bypasses many firewalls and proxies
- Demonstrates creative protocol abuse 😉
Encoding Options:
- Dots as spaces:
summarize.interstellar.for.me→"summarize interstellar for me" - Underscores as spaces:
summarize_interstellar_for_me→"summarize interstellar for me" - URL encoding:
summarize%20interstellar%20for%20me→"summarize interstellar for me" - Base64 for complex queries:
b64-encoded_prompt
- Default port is 5353 to avoid root privileges; DNS default is 53.
- Each TXT record string is limited to 255 bytes. The server splits the response into multiple strings; clients will show them contiguously.
- UDP DNS packets are typically limited to ~512 bytes without EDNS0. The server truncates long LLM output to fit (about 400 chars by default).
-
Install uv if you haven't already:
curl -LsSf https://astral.sh/uv/install.sh | sh -
Clone and run (uv handles Python version and dependencies automatically):
git clone <this-repo-url> cd llm-over-dns
-
Set your API key(s) in
.envfile:cp .env.template .env
Set your API key values
-
Run the server (uv automatically manages the environment):
# Basic server uv run main.py --host 0.0.0.0 --port 5353 --provider openai # With API key protection uv run main.py --host 0.0.0.0 --port 5353 --provider openai --require-api-key # Other providers uv run main.py --host 0.0.0.0 --port 5353 --provider anthropic
- Python 3.11+
- Install dependencies:
pip install -e . # or pip install dnslib openai anthropic python-dotenv
- Set API keys (same as above)
- Run the server:
python main.py --host 0.0.0.0 --port 5353 --provider openai --require-api-key
Option 1: Dots as word separators
dig @127.0.0.1 -p 5353 summarize.interstellar.for.me TXT +short
Option 2: Underscores as spaces (more natural)
dig @127.0.0.1 -p 5353 summarize_interstellar_for_me TXT +short
Option 3: URL-encoded spaces
dig @127.0.0.1 -p 5353 summarize%20interstellar%20for%20me TXT +short
Option 4: Base64url-encoded prompt (for complex queries)
PROMPT='Give me three bullet points about DNS over HTTPS vs classic DNS.'
ENC=$(python - <<'PY'
import base64,os
s=os.environ['PROMPT'].encode()
print(base64.urlsafe_b64encode(s).decode().rstrip('='))
PY
)
dig @127.0.0.1 -p 5353 b64-$ENC TXT +short
When --require-api-key is enabled, queries must include the API key as the first DNS label with key- prefix:
Basic authenticated query:
dig @127.0.0.1 -p 5353 key-your_secret_key.summarize.interstellar.for.me TXT +short
With underscores:
dig @127.0.0.1 -p 5353 key-your_secret_key.summarize_interstellar_for_me TXT +short
With base64 encoding:
dig @127.0.0.1 -p 5353 key-your_secret_key.b64-$ENC TXT +short
Unauthorized queries will return REFUSED:
# Without API key (will fail)
dig @127.0.0.1 -p 5353 summarize.interstellar.for.me TXT +short
# With wrong API key (will fail)
dig @127.0.0.1 -p 5353 key-wrong_key.summarize.interstellar.for.me TXT +shortOverride default models:
python main.py --provider openai --model gpt-4o
python main.py --provider anthropic --model claude-3-7-sonnet-20250219
By default, responses are limited to 800 characters for DNS compatibility. You can customize this:
Limited responses (recommended for reliability):
python main.py --host 127.0.0.1 --port 5353 --provider openai --max-chars 800Unlimited responses (use with TCP):
# Start server with unlimited responses
python main.py --host 127.0.0.1 --port 5353 --provider openai --max-chars 0
# Query with TCP to avoid UDP packet size limits
dig @127.0.0.1 -p 5353 +tcp "explain quantum computing in detail" TXT +shortCustom limits:
python main.py --host 127.0.0.1 --port 5353 --provider openai --max-chars 1500Why use TCP for longer responses?
- UDP DNS packets are limited to ~512 bytes traditionally
- TCP supports much larger responses
- Use
+tcpflag in dig for reliable delivery of long responses
Use sudo if you need the standard DNS port:
sudo python main.py --port 53 --provider anthropic
This is a demo. Exposing an LLM-backed DNS to the public Internet can be costly and abused. Consider:
Enable with --require-api-key flag and set DNS_API_KEY in your .env file:
- Only requests with the correct API key will receive responses
- Unauthorized requests get DNS REFUSED response
- API key is embedded in the DNS query itself:
key-your_secret_key.your.question
- Rate limiting per source IP
- Firewall rules to restrict access
- Model output length limits (already implemented)
- Observability/logging
- Regular API key rotation
Apache