[dns] Add dual-stack fake IP rewriting with nftables maps

Support A and AAAA DNS answer rewriting, CNAME alias handling, and temporary IPv4/IPv6 NAT mappings backed by nftables sets/maps.

Add example nftables rules and expand the README with usage, behavior, and setup notes.
This commit is contained in:
2026-05-19 13:43:17 +08:00
parent 63c471be8e
commit b066e36770
3 changed files with 680 additions and 239 deletions

197
README.md
View File

@@ -1,3 +1,196 @@
# dotp
# DNS-TProxy
Domain Transport Proxy with DNS and Netfilter
DNS-based transparent proxy using fake IPs and nftables to redirect and NAT selected domain traffic.
`dotp` could stand for Domain Over Transparent Proxy.
## Overview
`dotp` is a small DNS proxy that selectively rewrites A and AAAA records for
specified domains to "fake" IP addresses and maintains a temporary
one-to-one mapping between fake and real client addresses using nftables. This
allows traffic to be transparently redirected and NAT-ed while keeping
per-connection state in userspace and in kernel nftables sets.
The program:
- Listens for DNS queries on a UDP socket.
- Forwards queries to an upstream DNS server.
- Inspects the DNS responses and, for configured domains, replaces the
returned IPv4/IPv6 addresses with fake addresses from configured prefixes.
- Programs nftables sets to map fake addresses back to the corresponding
real addresses for a short TTL.
## How it works
### DNS path
1. A client sends a DNS query (UDP/53) to `dotp`.
2. `dotp` receives the packet, allocates a per-request `client_ctx`, and opens
a temporary UDP socket to the upstream DNS server.
3. The upstream response is read and parsed:
- DNS header and question section are copied as-is.
- Answer section is scanned record by record.
- For each name that matches a configured domain:
- CNAME records are copied over, preserving compression and structure.
- A/AAAA records are intercepted: their RDATA (IP address) is replaced
with a fake IP allocated from the configured IPv4/IPv6 prefix pools.
4. For every substituted address:
- A NAT entry is created or looked up in an in-memory hash table.
- A libev timer is armed for 120 seconds (`NAT_TTL`) for the mapping.
- nftables commands are issued (via libnftables) to:
- Add the real address to `inet <table> nat_addr` / `nat_addr6` sets.
- Add a mapping from fake to real in `inet <table> nat_map` /
`nat_map6` sets.
5. The modified DNS response is sent back to the client.
6. When the NAT timer expires, `dotp` removes the corresponding entries from
the nftables sets and frees the mapping.
If the incoming DNS packet is larger than the internal buffer
(`MAX_MESSAGE_SIZE`), the response is truncated and the TC (truncation) flag is
set in the DNS header.
### Domain matching
Domains are supplied via the `-d` option and stored in a tree of labels. During
DNS parsing, domain names in questions and answers are decoded, including
compression pointers, and matched against this tree:
- Only names under one of the configured domains are subject to rewriting.
- CNAME chains are followed so that subsequent A/AAAA answers for the aliased
name are also rewritten.
### NAT pool
`dotp` manages two address pools (IPv4 and IPv6) defined by prefixes passed on
the command line. It uses a simple hash (`city_hash_mix`) to allocate unique
fake addresses within the prefix ranges:
- For each real address, a `struct ip_nat` is created containing:
- real address and fake address
- family (AF_INET / AF_INET6)
- links for two separate hash chains (by real and by fake address)
- a libev timer (`expire`) that removes the mapping after `NAT_TTL` seconds.
- Lookups by real address refresh the timer.
## nftables integration
The code assumes an existing nftables table named `inet dotp` with appropriate
sets/rules. It manipulates the following sets:
- `nat_addr` / `nat_addr6`: containers for real client addresses.
- `nat_map` / `nat_map6`: maps from fake to real addresses.
It uses commands like:
- `add element inet dotp nat_addr { REAL }`
- `add element inet dotp nat_map { FAKE:REAL }`
- `add element inet dotp nat_addr6 { [REAL6] }`
- `add element inet dotp nat_map6 { [FAKE6]:[REAL6] }`
and their corresponding `delete element` variants on expiry.
The nftables rules themselves (e.g. for DNAT/SNAT using these sets) are not
set up by the program; an example configuration is provided in `src/rules.nft`.
## Command-line usage
From the `main` function, the expected usage is:
```text
Usage: dotp -H LISTEN_HOST -p LISTEN_PORT
-4 FAKE_IP_PREFIX -6 FAKE_IP6_PREFIX
--upstream-host UPSTREAM_HOST
--upstream-port UPSTREAM_PORT
[ -d DOMAIN ]
[ --daemonize ]
```
### Options
- `-H, --host LISTEN_HOST`
IP address to bind the local UDP DNS listener to (IPv4 only in current
implementation). Required.
- `-p, --port LISTEN_PORT`
Local UDP port to listen on. Defaults to 53.
- `-d, --domain DOMAIN`
Domain to subject to address rewriting (may be specified multiple times).
Domain syntax is validated (alphanumeric plus `-`, no leading/trailing `-`,
each label up to 63 chars).
- `-4, --ipv4-prefix FAKE_IP_PREFIX`
IPv4 prefix (e.g. `100.64.0.0/24`) from which fake IPv4 addresses will be
allocated. Prefix length must be ≤ 30. Required.
- `-6, --ipv6-prefix FAKE_IP6_PREFIX`
IPv6 prefix (e.g. `fd00::/64`) from which fake IPv6 addresses will be
allocated. Prefix length must be ≤ 64. Required.
- `--upstream-host UPSTREAM_HOST`
IPv4 address of the upstream DNS server to which queries are forwarded.
Required.
- `--upstream-port UPSTREAM_PORT`
UDP port of the upstream DNS server. Defaults to 53.
- `--daemonize`
Run the process in the background using `daemon(3)`.
`dotp` exits with error status if required options are missing or invalid.
## Runtime behavior
- Uses `libev` for event-driven I/O:
- One main `server_ctx` for the listening socket.
- A short-lived `client_ctx` for each in-flight upstream query with a
5-second timeout.
- On `SIGINT` or `SIGTERM`, the event loop is broken and the server exits
cleanly, freeing domain/NAT structures and nftables context.
## Dependencies
Build-time and runtime dependencies inferred from `src/main.c`:
- POSIX sockets (`AF_INET`, `SOCK_DGRAM`, `recvfrom`, `sendto`, `bind`)
- `libev`
- `libnftables`
On Debian/Ubuntu-like systems, packages may be named:
- `libev-dev`
- `libnftables-dev`
## Limitations
- Only UDP DNS is supported.
- Listener and upstream are currently IPv4-only.
- DNS payload size is limited to `MAX_MESSAGE_SIZE` (0x200 bytes).
- nftables table/sets must be created externally (see `src/rules.nft`).
## Example
Assuming:
- You have an nftables table `inet dotp` set up according to `src/rules.nft`.
- You want to redirect traffic for `example.com` and `foo.example.com`.
- You have fake address ranges:
- IPv4: `100.64.0.0/24`
- IPv6: `fd00::/64`
- Your upstream resolver is `1.1.1.1`.
You might run:
```sh
./dotp \
-H 0.0.0.0 -p 53 \
-4 100.64.0.0/24 \
-6 fd00::/64 \
--upstream-host 1.1.1.1 \
--upstream-port 53 \
-d example.com
```
Point your clients' DNS to the host running `dotp`. For matching domains,
clients will see fake IPs, while nftables rules will map those fake addresses
back to the real ones for the lifetime of the mapping.