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.
197 lines
6.7 KiB
Markdown
197 lines
6.7 KiB
Markdown
# DNS-TProxy
|
|
|
|
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.
|