weakboson b066e36770 [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.
2026-05-19 13:43:17 +08:00
2025-11-27 23:27:49 +08:00
2025-03-06 09:23:21 +00:00

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:

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:

./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.

Description
No description provided
Readme 62 KiB
Languages
C 98.8%
Makefile 1.2%