A Linux service that monitors DNS queries resolved via systemd-resolved and adds IPs matching specific hostnames to nftables sets. This enables policy-based routing — for example, routing traffic to certain domains through a WireGuard VPN.
systemd-resolved ──varlink──▶ resolved-nftset ──netlink──▶ nftables kernel sets
- Connects to
systemd-resolvedvia theio.systemd.Resolve.Monitorvarlink interface. - Listens for all DNS responses in real time.
- Matches each resolved hostname against user-defined rules (FQDN prefix matching).
- On a match, adds the resolved IPv4/IPv6 addresses to the corresponding nftables set.
- Your nftables ruleset marks packets destined for those IPs, and routing policy sends them to your desired interface.
- systemd ≥ 246 (for
io.systemd.Resolve.Monitor) - nftables
cargo build --release
sudo install -m 755 target/release/resolved-nftset /usr/local/bin/sudo useradd -r -s /sbin/nologin resolved-nftset
sudo install -m 644 resolved-nftset.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now resolved-nftsetConfiguration lives under /etc/resolved-nftset/. The directory structure maps to the nftables table and set names:
/etc/resolved-nftset/
└── <table_name>/ # nftables table name
└── <set_name>/ # set name (without _v4, _v6 suffix)
└── rules.conf # hostnames to match, one per line
Suppose you want traffic to google.com and youtube.com to go through a WireGuard interface (wg0).
sudo mkdir -p /etc/resolved-nftset/resolved_route/vpn
printf 'google.com\nyoutube.com\n' | sudo tee /etc/resolved-nftset/resolved_route/vpn/rules.confCreate /etc/nftables/resolved-mark.nft:
table inet resolved_mark {
set vpn_v4 {
type ipv4_addr
flags timeout
timeout 1h
gc-interval 5m
}
set vpn_v6 {
type ipv6_addr
flags timeout
timeout 1h
gc-interval 5m
}
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
ip daddr @vpn_v4 meta mark set 0xFF
ip6 daddr @vpn_v6 meta mark set 0xFF
}
}
Load it:
sudo install -m 640 resolved-mark.nft /etc/nftables/resolved-mark.nft
sudo nft -f /etc/nftables/resolved-mark.nftAdd include "/etc/nftables/resolved-mark.nft" to your main nftables config.
Mark 0xFF (255) needs a routing rule that directs marked packets to the VPN:
# Create a routing table for VPN traffic (table number 100 is arbitrary)
echo "100 vpn" | sudo tee -a /etc/iproute2/rt_tables
# Packets marked 0xFF go through table "vpn"
sudo ip rule add fwmark 0xFF table vpn
# All routes in the "vpn" table go out via wg0
sudo ip route add default dev wg0 table vpnsudo systemctl restart resolved-nftsetNow any DNS resolution for google.com or youtube.com will cause the daemon to add the resolved IPs to the vpn_v4/vpn_v6 sets. Packets to those IPs get marked 0xFF in prerouting, and the policy routing rule sends them through wg0.
You can define multiple rule directories for different routing policies:
/etc/resolved-nftset/
├── resolved_route/
│ ├── vpn/
│ │ └── rules.conf # → vpn_v4, vpn_v6 sets → route via wg0
│ └── tor/
│ └── rules.conf # → tor_v4, tor_v6 sets → route via tor
└── resolved_block/
└── ads/
└── rules.conf # → ads_v4, ads_v6 sets → drop in filter chain
- One hostname per line
- Lines starting with
#are comments - Blank lines are ignored
- Matching is prefix-based (e.g.,
google.commatcheswww.google.com,mail.google.com, etc.)
- The service requires
CAP_NET_ADMINto write to nftables via Netlink. - The systemd unit uses
AmbientCapabilities=CAP_NET_ADMINso it can run as an unprivileged user. - All other systemd sandboxing options (
ProtectSystem=strict,MemoryDenyWriteExecute, etc.) are enabled by default.
MIT OR Apache-2.0