rtr7 / router7

router7 is a small home internet router completely written in Go. It is implemented as a gokrazy appliance.
https://router7.org
Apache License 2.0
2.69k stars 110 forks source link

Support for Hairpinning #53

Closed CodeZombieCH closed 2 years ago

CodeZombieCH commented 4 years ago

I noticed that I'm unable to connect to a server inside my local network using the router7 public IP and the forwarded port.

Example Local machine: 192.168.0.100 Public IP: 12.34.56.78 Port forwarding rule: :22 -> 192.168.0.100:22

Result: connection refused

Connecting from outside of router7 works perfectly fine.

To ensure we can connect from the local network using the public IP, it would be nice if support for Hairpinning (see Hairpinning and Hairpin-NAT) could be added. This could be achieved by adding additional nftables rules.

(thanks for @stapelberg for helping me investigating this issue)

stapelberg commented 4 years ago

Because it came up recently on twitter, here are a few additional notes:

I suppose the next step would be prototyping some nft rules that do what Dave outlined in the twitter thread.

stapelberg commented 4 years ago

More insight from Dave: https://twitter.com/dave_universetf/status/1298100264927289349

I guess we just wait another few days until he explains how things ought to really work in Linux :)

neingeist commented 2 years ago

This comes up as 2nd hit when googling for nftables hairpinning, so I thought I share my experience. I'm not running router7 though.

For hairpinning NAT of a portforward from e.g. TCP port 16085 on the outer interface $out_if (using the address $out_ip) to local 192.168.122.2:22 on $lan_if (with the $lan_net IP range), you need to add

And, if you want to make local connections from the router to the service work:

The rules might be a bit narrow, but I go down the careful route (pun intended) with this to avoid accidental NATting.

neingeist commented 2 years ago
  • a forward filter rule iif $lan_if oif $lan_if ip daddr 192.168.122.2 tcp dport 22 accept (this allows forwardng the NATted connection)
    • (probably another output filter rule, but I generally allow outgoing connections, so I have this covered)

The forward filter rules can apparently be replaced by one universal ct status dnat counter accept.

Same goes for the postrouting NAT rules - one iifname $lan_if oifname $lan_if ct status dnat counter masquerade suffices.

neingeist commented 2 years ago

Here's a full ruleset, configured as above but reduced to a handful of rules by using a map of port forwards (requires nftables >= 0.9.4).

https://gist.github.com/neingeist/c97b488f2511bb5ca07c8a07213eccbe

stapelberg commented 2 years ago

Thanks for sharing this! I’m pretty busy the next few days, but will take a closer look and try it out next week hopefully :)

neingeist commented 2 years ago

There's no rush ;)

stapelberg commented 2 years ago

Yeah, on router7 indeed only 2 modifications are needed: the prerouting dnat rule needs to be expressed without using iifname "uplink0", and the postrouting needs a masquerade rule for lan0→lan0 traffic:

# nft add rule ip nat prerouting ip daddr 212.51.xx.0/24 tcp dport 8096 dnat to 10.0.0.252:8096
# nft add rule ip nat postrouting iifname "lan0" oifname "lan0" ct status dnat counter masquerade

I think we can express the rule without using hard-coded addresses (which means they would need to change whenever the public IP address changes) by using ip daddr != 127.0.0.0/8 ip daddr != 10.0.0.0/24 fib daddr type local, which would match all locally-configured IP addresses (fib daddr type local) except for the loopback and LAN IP addresses.

Unfortunately, the router7 kernel is built without CONFIG_NFT_FIB_IPV4. I’ll enable that when I get a chance.

stapelberg commented 2 years ago

Yep, as expected, these rules work, too:

# nft add rule ip nat prerouting ip daddr != 127.0.0.0/8 ip daddr != 10.0.0.0/24 fib daddr type local tcp dport 8096 dnat to 10.0.0.252:8096
# nft add rule ip nat postrouting iifname "lan0" oifname "lan0" ct status dnat counter masquerade

Next up is changing the router7 netconfig to apply these rules.

neingeist commented 2 years ago

I think we can express the rule without using hard-coded addresses (which means they would need to change whenever the public IP address changes) by using ip daddr != 127.0.0.0/8 ip daddr != 10.0.0.0/24 fib daddr type local, which would match all locally-configured IP addresses (fib daddr type local) except for the loopback and LAN IP addresses.

Nice, I didn't know about that one!