A lightweight mock DNS server for testing. Manage DNS records via a REST API and serve them over UDP + TCP. Built for testing DNS-dependent user flows (domain verification, email routing, etc.) in the MinuteMail testing platform.
π Built by MinuteMail.co β ephemeral mailboxes, a mock OAuth IdP, and a REST API for testing email and auth flows. If you're mocking DNS to test email delivery, check it out: minutemail.co
Why? When testing email delivery or domain verification flows, you need to control what DNS records resolve to β without managing real domains. BifrostDNS lets you spin up a DNS server, add records via API, and point your test suite at it.
curlscratch-based)β οΈ Security: The HTTP API has no authentication. Do not expose port 15353 to untrusted networks. See SECURITY.md for details.
# Run with default ports (requires port 53 privileges on host)
docker run -p 53:53/udp -p 53:53 -p 15353:15353 \
ghcr.io/minutemailco/bifrost-dns:latest
# With DNS fallback enabled (forward unknown queries to real DNS)
docker run -p 53:53/udp -p 53:53 -p 15353:15353 \
-e FALLBACK_DNS=1.1.1.1:53,8.8.8.8:53 \
ghcr.io/minutemailco/bifrost-dns:latest
# Or run unprivileged on alternate ports
docker run -e DNS_PORT=8053 -p 8053:8053/udp -p 8053:8053 -p 15353:15353 \
ghcr.io/minutemailco/bifrost-dns:latest
git clone https://github.com/minutemailco/bifrost-dns.git
cd bifrost-dns
# Install binary + systemd service (fallback enabled by default)
sudo ./install.sh
sudo systemctl enable --now bifrost-dns
# Install and also set BifrostDNS as the system DNS resolver
sudo ./install.sh --set-dns
# Install binary only, no systemd service
sudo ./install.sh --no-service
The install script will:
/usr/local/bin/bifrost-dns127.0.0.1 β auto-detects systemd-resolved, NetworkManager, or /etc/resolv.confIf you don't pass --set-dns, the script will prompt you at the end. Either way, it backs up the old config and prints revert instructions.
cargo run --release
All configuration is via environment variables:
| Env Var | Default | Description |
|---|---|---|
DNS_PORT | 53 | DNS server port (UDP + TCP) |
API_PORT | 15353 | HTTP API port |
LOG_LEVEL | info | Log level: error, warn, info, debug, trace |
FALLBACK_DNS | (unset) | Comma-separated upstream DNS servers for fallback forwarding (e.g. 1.1.1.1:53,8.8.8.8:53). You can omit the port for bare IPs (1.1.1.1,8.8.8.8). When unset, unknown queries return NXDOMAIN. |
CACHE_TTL | 300 | Maximum fallback cache duration in seconds. Per-entry TTL is the minimum of this and the record's actual TTL. |
BifrostDNS has two independent data layers:
Mock store β records you explicitly create via bifrost-dns add or the API. These persist until you delete them or restart the server. They always take priority over everything else.
Fallback cache β responses from upstream DNS servers, cached to avoid repeated round-trips. Auto-populated, auto-expiring (TTL-based), flushable on demand.
When a DNS query arrives:
FALLBACK_DNS server, cache the response, return it.Note: The
--ttlon mock records controls how long clients (browsers,dig, etc.) should cache the DNS answer. The record itself stays in BifrostDNS until explicitly deleted.
When FALLBACK_DNS is set, BifrostDNS acts as both a mock server and a forwarding resolver:
This is essential when running BifrostDNS as a system DNS resolver (outside Docker), where blocking all non-mocked domains would break internet access.
Example:
# Enable fallback to Cloudflare and Google DNS
FALLBACK_DNS=1.1.1.1,8.8.8.8 ./bifrost-dns
# Now mock records work alongside real DNS:
dig @localhost test.example.com A # β mock record from store
dig @localhost google.com A # β real IP forwarded from 1.1.1.1
Running BifrostDNS directly on a Linux machine is straightforward. The install.sh script handles everything, but here's what happens under the hood.
cargo build --release
# Binary is at target/release/bifrost-dns
# Minimal β mock only, NXDOMAIN for everything else
./target/release/bifrost-dns
# With fallback β mock records + real DNS for everything else
FALLBACK_DNS=1.1.1.1,8.8.8.8 ./target/release/bifrost-dns
# Custom ports
DNS_PORT=8053 API_PORT=8088 FALLBACK_DNS=1.1.1.1 ./target/release/bifrost-dns
The included install.sh script:
/usr/local/bin/bifrost-dns/etc/systemd/system/bifrost-dns.service with DNS fallback enabledsudo ./install.sh
sudo systemctl enable --now bifrost-dns
# Check status
sudo systemctl status bifrost-dns
# View logs
sudo journalctl -u bifrost-dns -f
# Override configuration (without editing the unit file)
sudo systemctl edit bifrost-dns
# In the drop-in, add:
# [Service]
# Environment=FALLBACK_DNS=9.9.9.9
# Environment=LOG_LEVEL=debug
Once BifrostDNS is running on 127.0.0.1:53, point your system at it:
systemd-resolved:
# Set the global DNS resolver
sudo resolvectl dns global 127.0.0.1
# Or for a specific interface
sudo resolvectl dns eth0 127.0.0.1
/etc/resolv.conf (traditional):
# Back up the existing file
sudo cp /etc/resolv.conf /etc/resolv.conf.bak
# Point to BifrostDNS
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
# Restore later with:
# sudo cp /etc/resolv.conf.bak /etc/resolv.conf
NetworkManager:
nmcli connection modify --active <connection-name> ipv4.dns 127.0.0.1
nmcli connection modify --active <connection-name> ipv4.ignore-auto-dns yes
nmcli connection up <connection-name>
Important: When BifrostDNS is your system DNS resolver,
FALLBACK_DNSmust be set β otherwise every non-mocked domain returns NXDOMAIN and your machine loses internet access. Theinstall.shscript enables fallback by default.
Base path: /api/v1
curl -X POST http://localhost:15353/api/v1/records \
-H "Content-Type: application/json" \
-d '{"name":"example.com","type":"A","ttl":3600,"data":"192.168.1.1"}'
Response (201 Created):
{
"id": "a1b2c3d4-...",
"name": "example.com.",
"type": "A",
"ttl": 3600,
"data": "192.168.1.1"
}
# All records
curl http://localhost:15353/api/v1/records
# Filter by name and/or type
curl "http://localhost:15353/api/v1/records?name=example.com.&type=A"
curl http://localhost:15353/api/v1/records/{id}
curl -X DELETE http://localhost:15353/api/v1/records/{id}
# Delete all
curl -X DELETE http://localhost:15353/api/v1/records
# Delete filtered
curl -X DELETE "http://localhost:15353/api/v1/records?name=example.com.&type=A"
curl http://localhost:15353/health
# {"status":"ok","version":"1.0.0"}
The bifrost-dns binary includes a CLI for managing records and cache without curl. It connects to a running BifrostDNS server.
# Add a record (--ttl controls how long clients cache the DNS answer,
# not how long the record lives in BifrostDNS β records persist until deleted)
bifrost-dns add test.example.com A 192.168.1.1 --ttl 3600
bifrost-dns add test.example.com MX "10 mail.example.com."
bifrost-dns add test.example.com TXT "v=spf1 -all"
# List records (with optional filters)
bifrost-dns list
bifrost-dns list --name test.example.com
bifrost-dns list --type A
# Delete by ID
bifrost-dns delete a1b2c3d4-...
# Delete by filter
bifrost-dns delete --name test.example.com
bifrost-dns delete --name test.example.com --type A
# Check server health
bifrost-dns health
# Flush the fallback DNS cache (all entries)
bifrost-dns flush
# Flush cache for a specific domain only
bifrost-dns flush google.com
The CLI reads BIFROST_HOST (default 127.0.0.1) and BIFROST_PORT (default 15353) to find the server:
BIFROST_PORT=15353 bifrost-dns list
| Type | Data Format | Example |
|---|---|---|
| A | IPv4 address | 192.168.1.1 |
| AAAA | IPv6 address | ::1 |
| CNAME | Target hostname | target.example.com. |
| NS | Nameserver hostname | ns1.example.com. |
| MX | <priority> <host> | 10 mail.example.com. |
| TXT | Arbitrary text | v=spf1 include:_spf.example.com ~all |
| SRV | <priority> <weight> <port> <target> | 10 5 5060 sip.example.com. |
# Query a mock record
dig @localhost test.example.com A
dig @localhost test.example.com MX
dig @localhost test.example.com TXT
# With fallback enabled, real domains work too
dig @localhost google.com A
# Using nslookup
nslookup test.example.com localhost
# Run tests
cargo test
# Lint
cargo clippy -- -D warnings
cargo fmt --check
# Build release binary
cargo build --release
# Build Docker image
docker build -t bifrost-dns .
This project uses Semantic Versioning. Git tags (v0.1.0, v1.0.0, etc.) trigger automated Docker image builds.
MIT β Β© MinuteMail.co
10 commits
1 commits
Rust
78.2%
Shell
21.3%
A lightweight mock DNS server for testing. Manage DNS records via a REST API and serve them over UDP + TCP. Built for testing DNS-dependent user flows (domain verification, email routing, etc.) in the MinuteMail testing platform.
π Built by MinuteMail.co β ephemeral mailboxes, a mock OAuth IdP, and a REST API for testing email and auth flows. If you're mocking DNS to test email delivery, check it out: minutemail.co
Why? When testing email delivery or domain verification flows, you need to control what DNS records resolve to β without managing real domains. BifrostDNS lets you spin up a DNS server, add records via API, and point your test suite at it.
curlscratch-based)β οΈ Security: The HTTP API has no authentication. Do not expose port 15353 to untrusted networks. See SECURITY.md for details.
# Run with default ports (requires port 53 privileges on host)
docker run -p 53:53/udp -p 53:53 -p 15353:15353 \
ghcr.io/minutemailco/bifrost-dns:latest
# With DNS fallback enabled (forward unknown queries to real DNS)
docker run -p 53:53/udp -p 53:53 -p 15353:15353 \
-e FALLBACK_DNS=1.1.1.1:53,8.8.8.8:53 \
ghcr.io/minutemailco/bifrost-dns:latest
# Or run unprivileged on alternate ports
docker run -e DNS_PORT=8053 -p 8053:8053/udp -p 8053:8053 -p 15353:15353 \
ghcr.io/minutemailco/bifrost-dns:latest
git clone https://github.com/minutemailco/bifrost-dns.git
cd bifrost-dns
# Install binary + systemd service (fallback enabled by default)
sudo ./install.sh
sudo systemctl enable --now bifrost-dns
# Install and also set BifrostDNS as the system DNS resolver
sudo ./install.sh --set-dns
# Install binary only, no systemd service
sudo ./install.sh --no-service
The install script will:
/usr/local/bin/bifrost-dns127.0.0.1 β auto-detects systemd-resolved, NetworkManager, or /etc/resolv.confIf you don't pass --set-dns, the script will prompt you at the end. Either way, it backs up the old config and prints revert instructions.
cargo run --release
All configuration is via environment variables:
| Env Var | Default | Description |
|---|---|---|
DNS_PORT | 53 | DNS server port (UDP + TCP) |
API_PORT | 15353 | HTTP API port |
LOG_LEVEL | info | Log level: error, warn, info, debug, trace |
FALLBACK_DNS | (unset) | Comma-separated upstream DNS servers for fallback forwarding (e.g. 1.1.1.1:53,8.8.8.8:53). You can omit the port for bare IPs (1.1.1.1,8.8.8.8). When unset, unknown queries return NXDOMAIN. |
CACHE_TTL | 300 | Maximum fallback cache duration in seconds. Per-entry TTL is the minimum of this and the record's actual TTL. |
BifrostDNS has two independent data layers:
Mock store β records you explicitly create via bifrost-dns add or the API. These persist until you delete them or restart the server. They always take priority over everything else.
Fallback cache β responses from upstream DNS servers, cached to avoid repeated round-trips. Auto-populated, auto-expiring (TTL-based), flushable on demand.
When a DNS query arrives:
FALLBACK_DNS server, cache the response, return it.Note: The
--ttlon mock records controls how long clients (browsers,dig, etc.) should cache the DNS answer. The record itself stays in BifrostDNS until explicitly deleted.
When FALLBACK_DNS is set, BifrostDNS acts as both a mock server and a forwarding resolver:
This is essential when running BifrostDNS as a system DNS resolver (outside Docker), where blocking all non-mocked domains would break internet access.
Example:
# Enable fallback to Cloudflare and Google DNS
FALLBACK_DNS=1.1.1.1,8.8.8.8 ./bifrost-dns
# Now mock records work alongside real DNS:
dig @localhost test.example.com A # β mock record from store
dig @localhost google.com A # β real IP forwarded from 1.1.1.1
Running BifrostDNS directly on a Linux machine is straightforward. The install.sh script handles everything, but here's what happens under the hood.
cargo build --release
# Binary is at target/release/bifrost-dns
# Minimal β mock only, NXDOMAIN for everything else
./target/release/bifrost-dns
# With fallback β mock records + real DNS for everything else
FALLBACK_DNS=1.1.1.1,8.8.8.8 ./target/release/bifrost-dns
# Custom ports
DNS_PORT=8053 API_PORT=8088 FALLBACK_DNS=1.1.1.1 ./target/release/bifrost-dns
The included install.sh script:
/usr/local/bin/bifrost-dns/etc/systemd/system/bifrost-dns.service with DNS fallback enabledsudo ./install.sh
sudo systemctl enable --now bifrost-dns
# Check status
sudo systemctl status bifrost-dns
# View logs
sudo journalctl -u bifrost-dns -f
# Override configuration (without editing the unit file)
sudo systemctl edit bifrost-dns
# In the drop-in, add:
# [Service]
# Environment=FALLBACK_DNS=9.9.9.9
# Environment=LOG_LEVEL=debug
Once BifrostDNS is running on 127.0.0.1:53, point your system at it:
systemd-resolved:
# Set the global DNS resolver
sudo resolvectl dns global 127.0.0.1
# Or for a specific interface
sudo resolvectl dns eth0 127.0.0.1
/etc/resolv.conf (traditional):
# Back up the existing file
sudo cp /etc/resolv.conf /etc/resolv.conf.bak
# Point to BifrostDNS
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
# Restore later with:
# sudo cp /etc/resolv.conf.bak /etc/resolv.conf
NetworkManager:
nmcli connection modify --active <connection-name> ipv4.dns 127.0.0.1
nmcli connection modify --active <connection-name> ipv4.ignore-auto-dns yes
nmcli connection up <connection-name>
Important: When BifrostDNS is your system DNS resolver,
FALLBACK_DNSmust be set β otherwise every non-mocked domain returns NXDOMAIN and your machine loses internet access. Theinstall.shscript enables fallback by default.
Base path: /api/v1
curl -X POST http://localhost:15353/api/v1/records \
-H "Content-Type: application/json" \
-d '{"name":"example.com","type":"A","ttl":3600,"data":"192.168.1.1"}'
Response (201 Created):
{
"id": "a1b2c3d4-...",
"name": "example.com.",
"type": "A",
"ttl": 3600,
"data": "192.168.1.1"
}
# All records
curl http://localhost:15353/api/v1/records
# Filter by name and/or type
curl "http://localhost:15353/api/v1/records?name=example.com.&type=A"
curl http://localhost:15353/api/v1/records/{id}
curl -X DELETE http://localhost:15353/api/v1/records/{id}
# Delete all
curl -X DELETE http://localhost:15353/api/v1/records
# Delete filtered
curl -X DELETE "http://localhost:15353/api/v1/records?name=example.com.&type=A"
curl http://localhost:15353/health
# {"status":"ok","version":"1.0.0"}
The bifrost-dns binary includes a CLI for managing records and cache without curl. It connects to a running BifrostDNS server.
# Add a record (--ttl controls how long clients cache the DNS answer,
# not how long the record lives in BifrostDNS β records persist until deleted)
bifrost-dns add test.example.com A 192.168.1.1 --ttl 3600
bifrost-dns add test.example.com MX "10 mail.example.com."
bifrost-dns add test.example.com TXT "v=spf1 -all"
# List records (with optional filters)
bifrost-dns list
bifrost-dns list --name test.example.com
bifrost-dns list --type A
# Delete by ID
bifrost-dns delete a1b2c3d4-...
# Delete by filter
bifrost-dns delete --name test.example.com
bifrost-dns delete --name test.example.com --type A
# Check server health
bifrost-dns health
# Flush the fallback DNS cache (all entries)
bifrost-dns flush
# Flush cache for a specific domain only
bifrost-dns flush google.com
The CLI reads BIFROST_HOST (default 127.0.0.1) and BIFROST_PORT (default 15353) to find the server:
BIFROST_PORT=15353 bifrost-dns list
| Type | Data Format | Example |
|---|---|---|
| A | IPv4 address | 192.168.1.1 |
| AAAA | IPv6 address | ::1 |
| CNAME | Target hostname | target.example.com. |
| NS | Nameserver hostname | ns1.example.com. |
| MX | <priority> <host> | 10 mail.example.com. |
| TXT | Arbitrary text | v=spf1 include:_spf.example.com ~all |
| SRV | <priority> <weight> <port> <target> | 10 5 5060 sip.example.com. |
# Query a mock record
dig @localhost test.example.com A
dig @localhost test.example.com MX
dig @localhost test.example.com TXT
# With fallback enabled, real domains work too
dig @localhost google.com A
# Using nslookup
nslookup test.example.com localhost
# Run tests
cargo test
# Lint
cargo clippy -- -D warnings
cargo fmt --check
# Build release binary
cargo build --release
# Build Docker image
docker build -t bifrost-dns .
This project uses Semantic Versioning. Git tags (v0.1.0, v1.0.0, etc.) trigger automated Docker image builds.
MIT β Β© MinuteMail.co
10 commits
1 commits
Rust
78.2%
Shell
21.3%