587 lines
21 KiB
Markdown
587 lines
21 KiB
Markdown
> Scroll to the bottom for an **IMPORTANT** message.
|
|
# VibeDNS
|
|
The AI generated DNS server designed to solve an idiots neiche problems
|
|
|
|
|
|
```
|
|
An authoritative DNS server, recursive resolver and network-wide filtering
|
|
appliance in a single Go binary, with a Bootstrap 5 management interface.
|
|
|
|
It is meant to be the DNS infrastructure for a home lab, a small office or a
|
|
lab network: the thing you point your DHCP server at. It answers
|
|
authoritatively for your internal zones, resolves everything else through
|
|
upstream resolvers, caches the results, and applies per-subnet blocklists so
|
|
the guest Wi-Fi and the server VLAN can have different rules.
|
|
|
|
Everything ships in one binary — HTML templates, CSS, JavaScript, the icon
|
|
font, and the schema migrations are all embedded. There is no Node.js, no build
|
|
step, and no CDN. The management interface works on a network with no Internet
|
|
access, which matters because a DNS server's UI should not need working DNS to
|
|
render.
|
|
```
|
|
---
|
|
|
|
## Features
|
|
|
|
**Authoritative DNS**
|
|
- Forward, reverse IPv4 (`in-addr.arpa`) and reverse IPv6 (`ip6.arpa`) zones
|
|
- Reverse zones created from a subnet — enter `192.168.1.0/24`, not `1.168.192.in-addr.arpa`
|
|
- Automatic SOA management with automatic serial increments, and a manual
|
|
override for migrations from another server
|
|
- Wildcards with correct closest-encloser semantics, CNAME chasing, delegation
|
|
referrals with glue, and empty non-terminals answering NODATA rather than NXDOMAIN
|
|
- BIND-compatible zone file import and export
|
|
- Zone cloning, enable/disable, bulk record editing, cross-zone record search
|
|
|
|
**Recursive resolver**
|
|
- Forwarding to configurable upstreams with per-server health tracking
|
|
- Selection strategies: fastest, sequential, round robin, random
|
|
- Configurable timeout, retries and concurrency ceiling
|
|
- EDNS(0), DNSSEC record pass-through, automatic TCP fallback on truncation
|
|
- **Closed by default**: recursion is restricted to an explicit network list
|
|
|
|
**Cache**
|
|
- Sharded, LRU-bounded, entirely in memory
|
|
- TTL decay so clients never see a TTL that stands still
|
|
- Negative caching per RFC 2308, serve-stale per RFC 8767, background prefetch
|
|
- Browse, search and evict individual entries from the UI
|
|
|
|
**Filtering**
|
|
- Per-subnet policies: guest Wi-Fi and a trusted LAN can use different blocklists
|
|
- Reusable blacklists and allowlists shared across policies
|
|
- Allowlist matches always override blacklist matches
|
|
- Exact, subdomain and wildcard matching — blocking `example.com` covers
|
|
`a.b.example.com` without storing a single extra row
|
|
- Block actions: NXDOMAIN (default), REFUSED, or sinkhole to a configurable address
|
|
- Bulk import from plain lists, hosts files and Adblock-style rules, in one
|
|
transaction; a 300,000 line list imports in seconds
|
|
|
|
**Operations**
|
|
- Query log with full filtering, retention limits and automatic cleanup
|
|
- Audit log of every administrative change, from the UI, the API and the CLI
|
|
- `/healthz`, `/readyz` and Prometheus `/metrics`
|
|
- Per-client DNS rate limiting with exemptions for trusted infrastructure
|
|
- Automatic SQLite backups using `VACUUM INTO`, with a safe restore workflow
|
|
- Versioned configuration export and import
|
|
- REST API under `/api/v1` with revocable API tokens
|
|
|
|
---
|
|
|
|
## Build
|
|
|
|
Go 1.26 or newer. No other toolchain is required.
|
|
|
|
```bash
|
|
git clone https://github.com/owen/vibedns.git
|
|
cd vibedns
|
|
go build -o vibedns ./cmd/vibedns
|
|
```
|
|
|
|
For a release build with version information:
|
|
|
|
```bash
|
|
go build -trimpath \
|
|
-ldflags="-s -w -X github.com/owen/vibedns/internal/version.Version=1.0.0" \
|
|
-o vibedns ./cmd/vibedns
|
|
```
|
|
|
|
`CGO_ENABLED=0` is the default and works: the SQLite driver is pure Go, so the
|
|
binary is static and cross-compiles without a C toolchain.
|
|
|
|
```bash
|
|
GOOS=linux GOARCH=arm64 go build -o vibedns-arm64 ./cmd/vibedns
|
|
```
|
|
|
|
Run the tests:
|
|
|
|
```bash
|
|
go test ./...
|
|
```
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
### Native
|
|
|
|
```bash
|
|
sudo useradd --system --home-dir /var/lib/vibedns --shell /usr/sbin/nologin vibedns
|
|
sudo install -m 0755 vibedns /usr/local/bin/vibedns
|
|
sudo install -d -o vibedns -g vibedns -m 0750 /var/lib/vibedns
|
|
|
|
# Bind port 53 without running as root
|
|
sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/vibedns
|
|
|
|
sudo install -m 0644 deploy/vibedns.service /etc/systemd/system/
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now vibedns
|
|
|
|
# The generated administrator password is printed once, to the journal
|
|
sudo journalctl -u vibedns -n 40 --no-pager
|
|
```
|
|
|
|
Most distributions run `systemd-resolved` on port 53. Disable it first:
|
|
|
|
```bash
|
|
sudo systemctl disable --now systemd-resolved
|
|
sudo rm -f /etc/resolv.conf
|
|
echo 'nameserver 127.0.0.1' | sudo tee /etc/resolv.conf
|
|
```
|
|
|
|
### Docker
|
|
|
|
```bash
|
|
docker compose up -d
|
|
docker compose logs vibedns # the generated password is printed once
|
|
```
|
|
|
|
---
|
|
|
|
## Initial setup
|
|
|
|
On first start the server creates the database, applies migrations, generates a
|
|
strong administrator password and prints it **once**:
|
|
|
|
```
|
|
VibeDNS 0.1.0 starting
|
|
|
|
Database: /var/lib/vibedns/dns.db
|
|
DNS UDP: 0.0.0.0:53
|
|
DNS TCP: 0.0.0.0:53
|
|
Management: http://127.0.0.1:8080
|
|
Zones: 0 (0 records)
|
|
Filtering: 6 networks, 0 blocked domains
|
|
Recursion: enabled for 8 network(s), 3 upstream(s)
|
|
|
|
Initial administrator:
|
|
Username: admin
|
|
Password: <generated-password>
|
|
|
|
This password will not be displayed again.
|
|
Change it at http://127.0.0.1:8080/account
|
|
```
|
|
|
|
The interface shows a banner until you replace that password. To supply your
|
|
own instead, set `VIBEDNS_ADMIN_PASSWORD` before the first start.
|
|
|
|
Lost the password? The database file is the credential:
|
|
|
|
```bash
|
|
sudo -u vibedns vibedns admin reset-password --db /var/lib/vibedns/dns.db
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
Two layers, deliberately separated.
|
|
|
|
**Startup settings** — needed before the database is open. CLI flags and
|
|
environment variables only.
|
|
|
|
| Flag | Environment variable | Default | Purpose |
|
|
|---|---|---|---|
|
|
| `--db` | `VIBEDNS_DB_PATH` | `./data/dns.db` | SQLite database file |
|
|
| `--http` | `VIBEDNS_HTTP_ADDR` | `127.0.0.1:8080` | Management interface |
|
|
| `--dns` | `VIBEDNS_DNS_ADDR` | `0.0.0.0:53` | DNS listeners (UDP and TCP) |
|
|
| `--log-level` | `VIBEDNS_LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
|
|
| `--log-format` | `VIBEDNS_LOG_FORMAT` | `text` | `text` or `json` |
|
|
| `--admin-username` | `VIBEDNS_ADMIN_USERNAME` | `admin` | Initial administrator |
|
|
| — | `VIBEDNS_ADMIN_PASSWORD` | generated | Initial password |
|
|
|
|
An address given on the command line is written back to the database, so the
|
|
running process and the stored configuration always agree.
|
|
|
|
**Everything else** lives in SQLite and is edited from *Settings* in the UI or
|
|
through `/api/v1/settings`. Changes to zones, records, policies, lists,
|
|
upstreams and cache behaviour take effect immediately — no restart. Only the
|
|
listen addresses need one, and the UI says so where that applies.
|
|
|
|
Inspect the effective configuration without starting the server:
|
|
|
|
```bash
|
|
vibedns config check # validate, with an explicit open-resolver check
|
|
vibedns config show # print every effective setting
|
|
```
|
|
|
|
---
|
|
|
|
## DNS configuration examples
|
|
|
|
### A forward zone
|
|
|
|
```bash
|
|
# UI: Zones → Forward Zones → Add Zone
|
|
# Then add records from the zone page.
|
|
```
|
|
|
|
A typical internal zone:
|
|
|
|
| Name | Type | Value | TTL |
|
|
|---|---|---|---|
|
|
| `@` | A | `192.0.2.10` | 3600 |
|
|
| `www` | CNAME | `example.com.` | 3600 |
|
|
| `mail` | A | `192.0.2.20` | 3600 |
|
|
| `@` | MX | `10 mail.example.com.` | 3600 |
|
|
| `@` | TXT | `v=spf1 mx -all` | 3600 |
|
|
| `*` | A | `192.0.2.99` | 300 |
|
|
|
|
Names are relative to the zone apex; `@` is the apex itself and `*` is a
|
|
wildcard. Quoting and 255-character chunking for TXT records is handled for you.
|
|
|
|
### Reverse DNS
|
|
|
|
The point of the reverse zone form is that you never calculate a zone name.
|
|
Enter the subnet and the apex is derived:
|
|
|
|
| You enter | Zone created |
|
|
|---|---|
|
|
| `192.168.1.0/24` | `1.168.192.in-addr.arpa.` |
|
|
| `10.0.0.0/8` | `10.in-addr.arpa.` |
|
|
| `172.16.0.0/16` | `16.172.in-addr.arpa.` |
|
|
| `2001:db8::/32` | `8.b.d.0.1.0.0.2.ip6.arpa.` |
|
|
|
|
Reverse delegation only happens on octet boundaries for IPv4 and nibble
|
|
boundaries for IPv6. A `/25` is rounded up to the enclosing `/24`, and the UI
|
|
tells you it did.
|
|
|
|
Add PTR records using the last octet as the name:
|
|
|
|
| Name | Type | Value |
|
|
|---|---|---|
|
|
| `10` | PTR | `host.example.com.` |
|
|
| `20` | PTR | `mail.example.com.` |
|
|
|
|
That answers `10.1.168.192.in-addr.arpa → host.example.com`.
|
|
|
|
### Importing an existing zone
|
|
|
|
```bash
|
|
curl -H "Authorization: Bearer $VIBEDNS_TOKEN" \
|
|
--data-binary @example.com.zone \
|
|
"http://127.0.0.1:8080/api/v1/zones/1/import?mode=replace"
|
|
```
|
|
|
|
The whole file is validated before anything is written, so a syntax error on
|
|
line 400 never leaves the zone half-imported.
|
|
|
|
---
|
|
|
|
## Blacklist examples
|
|
|
|
Create a list under *Policies → Blacklists*, then import into it. Three formats
|
|
are understood, and may be mixed in one file:
|
|
|
|
```text
|
|
# plain list
|
|
example.com
|
|
tracker.example.net
|
|
|
|
# hosts file
|
|
0.0.0.0 ads.example.com
|
|
127.0.0.1 telemetry.example.net
|
|
:: bad.example
|
|
|
|
# Adblock-style host rules
|
|
||analytics.example.org^
|
|
```
|
|
|
|
Comments, blank lines, IP-only lines, `localhost` entries and duplicates are
|
|
skipped, and the import summary reports exactly what happened:
|
|
|
|
> Imported hosts.txt: 184,291 lines processed, 172,004 domains added,
|
|
> 12,201 duplicates skipped, 6 invalid entries, 80 comments or blank lines ignored.
|
|
|
|
With *match subdomains* on (the default), blocking `example.com` also covers
|
|
`www.example.com` and `a.b.example.com` — matching walks the name's suffixes
|
|
rather than storing every possible subdomain.
|
|
|
|
### Per-subnet policies
|
|
|
|
The arrangement from the brief:
|
|
|
|
1. **Blacklists**: Adult Content, Gambling, Malware
|
|
2. **Policy "Guest Filtering"**: all three blacklists, action NXDOMAIN
|
|
3. **Policy "Malware Only"**: Malware alone
|
|
4. **Network "Guest Wi-Fi"** `100.64.30.0/24` → Guest Filtering
|
|
5. **Network "SecureLAN"** `100.64.10.0/24` → Malware Only
|
|
|
|
A client is matched against the *most specific* network containing its address.
|
|
To let one domain through everywhere, add it to an allowlist attached to the
|
|
same policy — an allowlist match always beats a blacklist match, so you never
|
|
have to edit an imported list.
|
|
|
|
Check what any name would do, as any client, under *Tools*.
|
|
|
|
---
|
|
|
|
## API examples
|
|
|
|
Create a token under *Settings → API*. It is shown once.
|
|
|
|
```bash
|
|
export VIBEDNS_TOKEN=vibedns_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
|
export VIBEDNS=http://127.0.0.1:8080
|
|
AUTH="Authorization: Bearer $VIBEDNS_TOKEN"
|
|
```
|
|
|
|
```bash
|
|
# List zones
|
|
curl -H "$AUTH" $VIBEDNS/api/v1/zones
|
|
|
|
# Create a zone
|
|
curl -H "$AUTH" -H 'Content-Type: application/json' \
|
|
-d '{"name":"internal.example","admin_email":"hostmaster@internal.example"}' \
|
|
$VIBEDNS/api/v1/zones
|
|
|
|
# Create a reverse zone from a subnet
|
|
curl -H "$AUTH" -H 'Content-Type: application/json' \
|
|
-d '{"cidr":"192.168.1.0/24","kind":"reverse4"}' \
|
|
$VIBEDNS/api/v1/zones
|
|
|
|
# Add a record
|
|
curl -H "$AUTH" -H 'Content-Type: application/json' \
|
|
-d '{"name":"www","type":"A","data":"192.0.2.10","ttl":3600}' \
|
|
$VIBEDNS/api/v1/zones/1/records
|
|
|
|
# Search records across every zone
|
|
curl -H "$AUTH" "$VIBEDNS/api/v1/records?search=192.0.2&type=A"
|
|
|
|
# Import a blocklist (streamed, no size limit beyond the configured one)
|
|
curl -H "$AUTH" --data-binary @hosts.txt \
|
|
$VIBEDNS/api/v1/blacklists/1/import
|
|
|
|
# Statistics
|
|
curl -H "$AUTH" $VIBEDNS/api/v1/stats
|
|
|
|
# Cache
|
|
curl -H "$AUTH" $VIBEDNS/api/v1/cache
|
|
curl -H "$AUTH" -X DELETE $VIBEDNS/api/v1/cache
|
|
curl -H "$AUTH" -X DELETE "$VIBEDNS/api/v1/cache?name=example.com"
|
|
|
|
# What would this name do?
|
|
curl -H "$AUTH" "$VIBEDNS/api/v1/tools/lookup?name=example.com&type=A&client=100.64.30.5"
|
|
curl -H "$AUTH" "$VIBEDNS/api/v1/tools/domain-check?domain=ads.example.com"
|
|
|
|
# Configuration export
|
|
curl -H "$AUTH" $VIBEDNS/api/v1/config/export > vibedns-config.json
|
|
```
|
|
|
|
Errors are JSON with an actionable message:
|
|
|
|
```json
|
|
{
|
|
"error": {
|
|
"status": 400,
|
|
"message": "an A record needs a valid IPv4 address, for example 192.0.2.10 (got \"not-an-ip\")",
|
|
"code": "invalid_request"
|
|
}
|
|
}
|
|
```
|
|
|
|
The administrator's Basic credentials also work, which is convenient for
|
|
interactive `curl`. Automation should use a token: tokens are individually
|
|
revocable and cannot change the administrator's credentials.
|
|
|
|
---
|
|
|
|
## Backup and restore
|
|
|
|
Backups use SQLite's `VACUUM INTO`, which writes a transactionally consistent
|
|
copy while the database is in use. Copying the `.db` file with `cp` would
|
|
capture a torn snapshot whose committed data still lives in the write-ahead log.
|
|
|
|
```bash
|
|
vibedns database backup --db /var/lib/vibedns/dns.db --output /var/backups/vibedns
|
|
vibedns database stats --db /var/lib/vibedns/dns.db
|
|
vibedns database vacuum --db /var/lib/vibedns/dns.db
|
|
```
|
|
|
|
Enable scheduled backups under *Settings → Database*, with an interval and a
|
|
retention count. Backups can be downloaded from the UI or the API.
|
|
|
|
**Restore is deliberately two-step.** Overwriting the database underneath a
|
|
running process would leave open connections reading a file that no longer
|
|
exists, so a restore is *staged* and applied on the next start:
|
|
|
|
```bash
|
|
vibedns database restore --file /var/backups/vibedns/vibedns-20250101-030000.db
|
|
sudo systemctl restart vibedns
|
|
```
|
|
|
|
The database being replaced is preserved next to it as
|
|
`dns.db.pre-restore-<timestamp>`, so a restore that turns out to be the wrong
|
|
choice is still recoverable. In the UI you must retype the backup's file name
|
|
to confirm.
|
|
|
|
---
|
|
|
|
## Security considerations
|
|
|
|
**This server is not an open resolver, and takes work to become one.**
|
|
Recursion is restricted to an explicit allow list, seeded with RFC1918 and ULA
|
|
ranges. `vibedns config check` fails loudly if that list ever contains
|
|
`0.0.0.0/0` or `::/0`. Authoritative answers remain available to clients that
|
|
are not allowed to recurse, so tightening the ACL does not break your own zones.
|
|
|
|
**Credentials.** The administrator password is stored as an Argon2id hash
|
|
(64 MiB, 3 passes). Because HTTP Basic replays credentials on every request,
|
|
successful verifications are cached briefly in memory, keyed by a MAC of the
|
|
password — otherwise every page load would cost 64 MiB and tens of milliseconds.
|
|
Changing the password clears that cache immediately. Repeated failures from one
|
|
address are locked out.
|
|
|
|
API tokens are 256 bits from the system CSPRNG, stored as a SHA-256 hash with a
|
|
short clear-text prefix for lookup. A fast hash is correct here precisely
|
|
because a token has no low-entropy guess space — and it is verified on every
|
|
API request.
|
|
|
|
**CSRF.** Basic authentication does not protect state-changing requests, so the
|
|
UI carries a signed, account-bound, double-submitted token on every form. The
|
|
API applies the same check only where it is meaningful: bearer tokens are never
|
|
sent automatically by a browser, and a JSON body cannot be produced by a
|
|
cross-origin form without a preflight.
|
|
|
|
**Headers.** A strict Content-Security-Policy (`script-src 'self'`, no inline
|
|
scripts, no `eval`), `X-Frame-Options: DENY`, `nosniff`, and HSTS when served
|
|
over TLS. Page data reaches JavaScript through `data-` attributes rather than
|
|
inline `<script>` blocks, so the policy needs no exceptions.
|
|
|
|
**Everything else.** All SQL is parameterised. The database and its backups are
|
|
created `0600`. Templates escape by default. Uploads and request bodies are
|
|
size-limited. Rate limiting applies to both DNS and the management interface.
|
|
Passwords and token secrets never appear in logs, audit entries or the
|
|
configuration export.
|
|
|
|
**Recommended deployment:** bind the management interface to `127.0.0.1` and
|
|
reach it over SSH or a VPN. If it must be exposed, put it behind a reverse proxy
|
|
with TLS and set the trusted-proxy list so `X-Forwarded-For` is honoured only
|
|
from that proxy — trusting it unconditionally would let any client forge its
|
|
address and slip past the sign-in rate limiter.
|
|
|
|
---
|
|
|
|
## CLI
|
|
|
|
```
|
|
vibedns start the server (serve is the default)
|
|
vibedns serve start explicitly
|
|
vibedns version version and build information
|
|
|
|
vibedns config check validate configuration, exit non-zero on problems
|
|
vibedns config show print every effective setting
|
|
|
|
vibedns admin reset-password generate or set a new administrator password
|
|
vibedns admin show show the administrator account
|
|
|
|
vibedns database migrate apply pending schema migrations
|
|
vibedns database backup write a consistent backup
|
|
vibedns database restore --file stage a restore for the next start
|
|
vibedns database vacuum reclaim space after large deletions
|
|
vibedns database stats size and row counts
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
cmd/vibedns/ entry point
|
|
internal/
|
|
api/ REST API (/api/v1)
|
|
app/ service layer: validation, auditing, invalidation
|
|
auditlog/ administrative change log
|
|
auth/ Argon2id, API tokens, CSRF, middleware
|
|
authoritative/ in-memory zone index and answer engine
|
|
backup/ VACUUM INTO backups and staged restore
|
|
blacklist/ domain matcher and bulk-import parsers
|
|
cache/ sharded resolver cache
|
|
cli/ command line interface
|
|
config/ bootstrap config and DB-backed settings
|
|
database/ SQLite access, schema, migrations
|
|
dnsengine/ UDP/TCP listeners and the query pipeline
|
|
metrics/ counters and Prometheus exposition
|
|
models/ shared data types
|
|
netutil/ address helpers
|
|
policy/ CIDR-indexed policy evaluation
|
|
querylog/ buffered query logging
|
|
ratelimit/ per-client token buckets
|
|
resolver/ upstream forwarding and recursion ACL
|
|
runtimecfg/ immutable configuration snapshots
|
|
validate/ DNS name and record validation
|
|
web/ HTML handlers, templates, middleware
|
|
zonefile/ BIND zone file import and export
|
|
web/
|
|
templates/ embedded HTML
|
|
static/ embedded CSS, JS, fonts
|
|
```
|
|
|
|
**The DNS data path never touches SQLite.** Settings, the compiled zone index,
|
|
the compiled policy index and the recursion ACL live in one immutable
|
|
`Snapshot` behind an atomic pointer. A query reads that pointer once and works
|
|
entirely from immutable data — no locks, no database, nothing a writer can
|
|
block. Configuration changes build a fresh snapshot and swap it in; queries
|
|
already in flight finish against the old one. Reloads are debounced, so
|
|
importing a list one API call at a time still results in a bounded number of
|
|
rebuilds.
|
|
|
|
Blocklists are shared by pointer between policies, so a 200,000 domain list
|
|
used by five policies is held in memory exactly once.
|
|
|
|
Resolution order for each query:
|
|
|
|
1. Client policy — blocklists apply even to names a local zone would answer
|
|
2. Authoritative zones — these always win over recursion
|
|
3. Recursion ACL — refuse if the client may not recurse
|
|
4. Cache
|
|
5. Upstream resolvers
|
|
6. Cache the result
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
GNU General Public License v3.0.
|
|
|
|
|
|
|
|
# TL;DR
|
|
|
|
This is entirely vibe-coded software I made because I couldn't find a local homelab DNS server that did what I wanted without being either way too complicated or missing half the stuff I actually care about.
|
|
|
|
I built it for myself, my homelab, and the way I want to manage DNS. I put it online because somebody else might find it useful too.
|
|
|
|
That does not mean I am trying to turn this into some giant community-driven DNS project.
|
|
|
|
1. Yes, it is vibe-coded.
|
|
2. Yes, some of the code is probably bad.
|
|
3. Yes, there are probably much better ways to do some of this.
|
|
4. No, I probably will not implement the feature you want.
|
|
5. No, I am not going to rewrite half the project because you think I picked the wrong database, framework, DNS library, or whatever else.
|
|
|
|
And no, I am not going to be upset if you think this software sucks.
|
|
|
|
If you like it, use it.
|
|
|
|
If you don't like it, don't use it.
|
|
|
|
If you want to fork it and make it better, please do. That is literally what forks are for.
|
|
|
|
This is not trying to replace BIND, PowerDNS, Technitium, AdGuard Home, Pi-hole, or whatever enterprise DNS platform you are about to compare it to. I just wanted something simple that I could run locally, manage from a decent web UI, and not spend an entire afternoon configuring.
|
|
|
|
I couldn't find exactly what I wanted, so naturally I made the extremely reasonable decision to have AI help me write a DNS server.
|
|
|
|
There will be bugs. There will probably be questionable code. There may be parts of this project that make experienced software engineers stare at their screen for a minute.
|
|
|
|
That is okay.
|
|
|
|
Please do not open an issue just to tell me the entire architecture is wrong. Please do not write me an essay explaining why this should have been written in Rust. Please do not @ me because you disagree with how I implemented DNS caching.
|
|
|
|
Useful bug reports are fine. PRs are fine. Forks are fine.
|
|
|
|
But this is ultimately software I made to solve my problem.
|
|
|
|
Use it, fork it, laugh at it, improve it, or don't use it.
|
|
|
|
That is all. Please don't @ me.
|