voxpupuli / puppet-nftables

Puppet Module to manage nftables firewall rules.
Apache License 2.0
12 stars 33 forks source link

forwarding rules #236

Open anarcat opened 7 months ago

anarcat commented 7 months ago

Hello!

I've been working on a home router setup, and one of the things i have painfully figured out is the exact incantations to get not only port forwarding right (that's relatively easy), but "reflection" or "hairpinning" which allows users inside the NAT to access services as if they were on the outside.

I think this might be a worthwhile addition here. I'm still in the testing phase of this here, i have a handful of new forwards to deploy soon that I haven't tested yet, but when I did, i'd submit a PR for that... The code, right now, lives in a profile of mine:

# forward a service from the outside to internal services
#
# This forwards an external port to an internal machine using DNAT. It
# also sets up reflection so that internal hosts can access those
# services transparently by using the same external IP address.
#
# Example:
#
#     profile::network::forward {
#       default:
#         lan_cidr => '192.168.0.0/24',
#         wan_if   => 'eth0',
#         wan_cidr => '10.0.0.1/32',
#       'http':
#         target_addr => '192.168.0.10',
#         target_port => '80',
#         wan_port    => '8080',
#       'https':
#         target_addr => '192.168.0.10',
#         target_port => '443',
#         wan_port    => '8443',
#     }
#
# The above forwards ports 8080 and 8443 to the machine 192.168.0.10's
# regular http (80) and (8443) internal ports. The internal network is
# 192.168.0.0/24 and the external, public IP address is 10.0.0.1. The
# external interface is eth0.
#
# @param target_addr IP address to forward to
# @param lan_addr internal address of the router
# @param lan_cidr internal IP address range
# @param target_port port number of the forward target
# @param wan_port port number on the external interface
# @param wan_if external interface, public network
# @param wan_cidr publicly available IP address range
define profile::network::forward(
  Stdlib::IP::Address::V4::Nosubnet $target_addr,
  Stdlib::IP::Address::V4::Nosubnet $lan_addr,
  Stdlib::IP::Address::V4::CIDR $lan_cidr,
  Stdlib::Port $target_port,
  Stdlib::Port $wan_port,
  String[1] $wan_if,
  Stdlib::IP::Address::V4::CIDR $wan_cidr,
) {
  nftables::rule {
    "PREROUTING-dnat_${title}":
      table   => "ip-${nftables::nat_table_name}",
      order   => '41',
      content => "iifname ${wan_if} tcp dport ${wan_port} dnat to ${target_addr}:${target_port}";
    # reflection
    "PREROUTING-dnat_${title}reflect":
      table   => "ip-${nftables::nat_table_name}",
      order   => '40',
      content => "tcp dport ${wan_port} ip saddr ${lan_cidr} ip daddr ${wan_cidr} dnat to ${target_addr}:${target_port}";
    "POSTROUTING-dnat_${title}reflect":
      table   => "ip-${nftables::nat_table_name}",
      order   => '40',
      content => "tcp dport ${target_port} ip saddr ${lan_cidr} ip daddr ${target_addr}/32 snat to ${lan_addr}";
  }
}

How does that look?

anarcat commented 7 months ago

i actually renamed the define and pushed the code here:

https://gitlab.com/anarcat/puppet/-/blob/c9635ec8e8a7f319874ca63c4074de9e731941aa/site-modules/profile/manifests/nftables/rules/forward.pp

may be updated with time, naturally.

also, i just remembered we have dnat4 and snat4 rules in there, but for me those are really opaque and undocumented, and don't do all of the above. maybe the above should be rewritten to use the dnat/snat stuff, but i'm not sure i understand them well enough to tell.

kenyon commented 7 months ago

Unrelated to your code, but it sure would be simpler to use IPv6 rather than port forwarding. Being able to eliminate this kind of legacy IP complexity on my home network is what got me into IPv6 many years ago.

anarcat commented 7 months ago

Thanks, but if I had access to IPv6 on this setup, I wouldn't need this stuff.

Obviously. :p

kenyon commented 7 months ago

Not necessarily obvious, everyone has to learn at some point. Maybe also obvious, but tunnels are often a good workaround for lack of native IPv6, but I guess some ISPs make even that difficult.

anarcat commented 7 months ago

:shrug:

i understand where you're coming from, but i would assume someone that comes in with relatively advanced knowledge in firewall rules and Puppet would know about other ways to do what i'm trying to here. I wouldn't try to teach them the myriad of other ways they could do what they want to do.

Heck, you could have told me about ipsec, tailscale, ngrok and god knows how else I might manage this. I'm just trying to keep things simple for now. :)

anarcat commented 7 months ago

or to frame this another way:

do you fundamentally object to adding such a NAT forwarding rule here, in favor of telling people they should setup IPv6 tunnels instead?

duritong commented 7 months ago

We do have have a simple wrapper that includes hairpinning here: https://code.immerda.ch/immerda/ibox/puppet-modules/-/blob/main/ib_nftables/manifests/simple_dnat.pp?ref_type=heads

It seems to me that we did it slightly different, but I can assure you we have it in use for quite a while (longer than the commits are).

I intended to contribute that wrapper eventually into the nftables module itself, but lacked a bit of time to properly clean it up and write tests.

anarcat commented 7 months ago

interesting! that leads to a 500 error here, but i'd be really curious to read it! i'd be happy to collaborate towards a final version, but i must admit i suck at unit tests, so i'm not sure i could help with that. :p