For anyone looking to do this using nftables (the official replacement for iptables), here's what I came up with:
define dnat_targets = {
http : 10.0.10.1 . http,
https : 10.0.10.1 . https,
32400 : 10.0.10.4 . 32400,
25565 : 10.0.10.8 . 25565,
}
define dnat_allowed = {
10.0.10.1 . http,
10.0.10.1 . https,
10.0.10.4 . 32400,
}
table inet nat {
map dnat_destinations {
type inet_service : ipv4_addr . inet_service
elements = $dnat_targets
}
set dnat_masq {
type ipv4_addr . inet_service
elements = $dnat_allowed
}
chain prerouting {
ip daddr != 10.0.0.0/8 fib daddr type local dnat ip addr . port to tcp dport map @dnat_destinations
}
chain postrouting {
ip saddr 10.0.0.0/8 ip daddr . tcp dport @dnat_masq masquerade
}
}
table inet filter {
set dnat_allowed {
type ipv4_addr . inet_service
elements = $dnat_allowed
}
chain forward {
ip daddr . tcp dport @dnat_allowed accept
}
}
Note that this is a snippet from my firewall definition, you need to have the chains created before using this. It also doesn't match the original post situation exactly, the ip address and RFC1918 ranges need to be adapted.
To add DNAT targets, you only need to add them to the dnat_targets
definition and then allow the destination in the dnat_allowed
set. As far as I can tell you can't do it with only the dnat_targets
set, but one advantage of doing it this way is that you can toggle the dnat rules on and off by removing them from the dnat_allowed
set while still keeping the mapping around in case you want it later.
One really nice thing about this is that the maps are first class constructs in nftables, in other words, no matter how many DNAT targets you have (1,10,100,1000) the packets only need to traverse these three rules which use the maps and sets (similar to how ipset works).
Another thing I added was to make this working using an unknown public IP address, so you can safely use this when the wan interface is using DHCP, and you can still run a LAN accessible website, or anything else for that matter, on the router box.
Finally, this does not show the masquerade rule for the wan connection which is required to be fully functional.
Going through the rules one at a time:
ip daddr != 10.0.0.0/8 fib daddr type local dnat ip addr . port to tcp dport map @dnat_destinations
This is the main DNAT rule, if the destination address is not in 10.0.0.0/8 and it is a local address (defined as any address assigned to an interface on the local machine) then we're going to do DNAT on it. This is what makes sure that you match only packets intended for an unknown WAN address. Note that if you skip daddr != 10.0.0.0/8
then the dnat rule will match request to any interface on the router, so an admin web UI would be dnated instead of shown when access via 10.0.0.1:80 for example, and if you skip fib daddr type local
then any request for anything using any of the ports in the map (any website for example) will be dnated (e.g. if you try to go to google.com on one of your clients you'd end up at the site you're trying to host instead).
My router has several lan addresses, if yours only has one you can simplify the rule further. The DNAT is defined using the map lookup on the destination port.
ip saddr 10.0.0.0/8 ip daddr . tcp dport @dnat_masq masquerade
This is analogous to the masquerade rule in the iptables answer, if the source is a lan address and the destination address and port are in the dnat_allowed
set, then masquerade. The reason this makes it work, by the way, is that if you don't alter the source address, then when the server goes to respond to the web request, it will see a LAN address, then arp "who has" that LAN address, your machine will respond to that arp request, and then it will directly send the packet to your local machine. Your local machine, however, made the request to the routers public IP, so when it gets a response from the server directly it doesn't know what to do with it since it wasnt expecting a packet from that address, so it flags it as invalid and drops it. By masquerading here, you force the web server to reply to the router and the router sends it back to your machine using conntrack.
ip daddr . tcp dport @dnat_allowed accept
Finally, the forward filter rule, if the destination address and port are in the dnat_allowed
set, then accept the packet.