Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nftables module: Add new module for nftables firewall settings #18842

Closed
wants to merge 1 commit into from
Closed

nftables module: Add new module for nftables firewall settings #18842

wants to merge 1 commit into from

Conversation

Jookia
Copy link
Contributor

@Jookia Jookia commented Sep 22, 2016

Motivation for this change

This adds a simple service for setting up firewall rules using nftables instead of iptables. It includes a few examples, and it allows you to specify files so they can be generated from scripts.

Things done
  • Tested using sandboxing
    (nix.useSandbox on NixOS,
    or option build-use-sandbox in nix.conf
    on non-NixOS)
  • Built on platform(s)
    • NixOS
    • OS X
    • Linux
  • Tested compilation of all pkgs that depend on this change using nix-shell -p nox --run "nox-review wip"
  • Tested execution of all binary files (usually in ./result/bin/)
  • Fits CONTRIBUTING.md.

@mention-bot
Copy link

@Jookia, thanks for your PR! By analyzing the annotation information on this pull request, we identified @edolstra, @bjornfor and @offlinehacker to be potential reviewers

Copy link
Contributor

@groxxda groxxda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm really unsure about this addition.
I haven't looked into nftables yet but maybe we could provide a better interface and use nftables for networking.firewall eventually.

Just a few comments:
the logic for the service should be wants = [ "network-pre.target" ]; not the other way around.

Adding ruleset and rulesetFile options that are mutually exclusive (rulesetFile overrides ruleset) seems wrong to me. Either one should suffice.

@Jookia
Copy link
Contributor Author

Jookia commented Sep 22, 2016

If you have ideas for a better interface, I'd be happy to listen. I use rulesetFile, but it would be annoying to have to do pkgs.writeText if you're just writing inline rules. The wants change is a good idea.

I use this personally, but if it's not useful for NixOS I'm happy to close the PR

@Jookia
Copy link
Contributor Author

Jookia commented Sep 22, 2016

Actually the wants change isn't a good idea. This module does the same as firewall.

@groxxda
Copy link
Contributor

groxxda commented Sep 22, 2016

Then the firewall module is broken too...
About network-pre.target source:

It's a passive unit: you cannot start it directly and it is not pulled in by the the network management service, but by the service that wants to run before it. [...] Services that want to be run before the network is configured should place Before=network-pre.target and also set Wants=network-pre.target to pull it in.

@Jookia
Copy link
Contributor Author

Jookia commented Sep 22, 2016

Yeah, I guess I'll fix it here then. The nftables module isn't intended to replace the firewall, but it is incompatible with the firewall. nftables itself is probably a little too early to go all out on, but it's nice to have for now as a module with some examples.

@Jookia
Copy link
Contributor Author

Jookia commented Sep 22, 2016

Fixed?

1 similar comment
@Jookia
Copy link
Contributor Author

Jookia commented Sep 22, 2016

Fixed?

@groxxda
Copy link
Contributor

groxxda commented Sep 22, 2016

I think we need some wantedBy.. Maybe basic.target?

@Jookia
Copy link
Contributor Author

Jookia commented Sep 22, 2016

Added wantedBy = multi-user.target, this is how another firewall package does it.

@tavyc
Copy link
Contributor

tavyc commented Sep 23, 2016

My 2¢ as someone who manages many complex Linux firewalls, and in the process of switching some of them to NixOS.

I would like to have an abstraction layer over tables, chains and rules, so I can split my firewall configuration over multiple Nix files, eg. have a vpn.nix define a VPN connection and add some iptables/nftables rules to the relevant chains. Then the NixOS module system would merge all the rules and chains and generate a single text file as input for iptables-restore/nftables, so the ruleset can be loaded atomically.

An example of what I'm thinking about:

networking.firewall = {
  ipRules = {

    filter.INPUT = [
      { inputInterface = "eth2"; chain = "dmz"; }
    ];

    filter.dmz = [
      { proto = "tcp"; destinationPort = [ 80 443 ]; target = "ACCEPT"; } # expands to 2 iptables rules
      { allowedTCPPorts = [ 80 443 ]; }  # shortcut for the rule above
    ];

    nat.POSTROUTING = [
      { source = "192.168.0.1/24"; outputInterface = "eth1"; target = "SNAT"; targetArgs = "--to 192.168.7.1"; }
    ];

    mangle.PREROUTING = [
      { source = "10.0.0.1/24"; inputInterface = "eth0"; mark = "0"; target = "MARK"; targetArgs = "--set-mark 0x20"; }
    ];
  };

  ip6Rules = {
    # ...
  };
};

The proposal in #12940 would be a step in the right direction for iptables IMO.

However, nftables has a much more expressive language than iptables, and abstracting all its features with Nix would be way overkill.
So let's thin the abstraction and generate the rulesets from text fragments; it would be simpler to implement as a module, and still be nice to the firewall builder.
Something like this:

networking.firewall.iptablesRules = {

  filter.INPUT = [
    "-i eth2 -j dmz"
  ];

  filter.dmz = [
    "-p tcp --dport 80 -j ACCEPT"
    "-p tcp --dport 443 -j ACCEPT"
  ];

};

Or, for nftables:

networking.firewall.nftables = {

  sets.private = ''
    { 192.168.0.0/16, 10.0.0.0/8 }
  '';

  ipRules.filter.input = ''
    ip saddr @private log drop
    iif eth2 jump dmz
  '';

  ipRules.filter.dmz = ''
    tcp dport { 80, 443 } accept
  '';

  ip6Rules.filter.input = "...";

};

The existing allowedTCPPorts options would generate rules like these so we can keep backwards compatibility and a simple option for basic firewalls.

Let me know what you think.

@Jookia
Copy link
Contributor Author

Jookia commented Sep 23, 2016

Should this be renamed to something like nftables-raw then?

@aneeshusa
Copy link
Contributor

Something I do with my nftables ruleset files is to put flush ruleset at the top of the file, so I can always just re-run nft on that file and it will automatically flush + reload all rules. I find this nicer than doing it explicitly in ExecReload.

@Jookia
Copy link
Contributor Author

Jookia commented Sep 24, 2016

On Fri, Sep 23, 2016 at 06:15:14PM -0700, Aneesh Agrawal wrote:

Something I do with my nftables ruleset files is to put flush ruleset at the
top of the file, so I can always just re-run nft on that file and it will
automatically flush + reload all rules. I find this nicer than doing it
explicitly in ExecReload.

Having it twice doesn't hurt, and having it in ExecReload allows it to ensure
that things are loaded atomically. However, I'm open to changing this given that
it's hard to use the rules outside of nft reliably.

Would it be better to update documentation to include "flush ruleset" and also
provide a way to avoid the automatic flushing in cases people don't want that?

Perhaps rulesetFile could not include it.

@aneeshusa
Copy link
Contributor

I can't imagine a scenario where you wouldn't want atomic flushing. IMO, always automatically adding flush ruleset to the top of the file is the simplest and thus best solution - no special ExecReload rule, impossible to forget it using nft, don't need to document how to enable it because it's always on.

If there is a good reason not to use atomic flushing, I would like to hear about it.

@Jookia
Copy link
Contributor Author

Jookia commented Sep 24, 2016

On Fri, Sep 23, 2016 at 08:48:13PM -0700, Aneesh Agrawal wrote:

I can't imagine a scenario where you wouldn't want atomic flushing. IMO,
always automatically adding flush ruleset to the top of the file is the
simplest and thus best solution - no special ExecReload rule, impossible to
forget it using nft, don't need to document how to enable it because it's
always on.

What's the difference between adding it at build-time versus runtime?

If there is a good reason not to use atomic flushing, I would like to hear
about it.

Load fast crash hard?

RemainAfterExit = true;
ExecStart = "${pkgs.nftables}/bin/nft --file ${cfg.rulesetFile}";
ExecStop = "${pkgs.nftables}/bin/nft flush ruleset";
ExecReload = pkgs.writeScript "nftables-atomic-reload" ''
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found this version as used to reload nftables in archlinux clever:

https://git.archlinux.org/svntogit/packages.git/tree/trunk/nftables-reload?h=packages/nftables

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the advantage of this over how it works now? It would require adding another intermediate file just for reloading.

Copy link
Member

@Mic92 Mic92 Oct 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, you also generate a script using writeScript to do the same, this could be replaced by a direct call to nft. This way no temporary files are required.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, that sounds good. But why? Are temporarily files a big deal? Or is it just more clever to do it that way?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/tmp/ could be full for example and this version is also easier to read:

 ExecReload = pkgs.writeScript "nftables-atomic-reload" ''
   #! ${pkgs.nft}/bin/nft -f
   flush ruleset
   include "${cfg.rulesetFile}"
 ''

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, yeah. That looks MUCH better. I assumed I had a quick one-liner using shell pipes to fix this, but re-reading my code it looks awful. The github issue didn't show the rest of the code, sorry.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

"ip_tables"
"iptable_nat"
"iptable_mangle"
"iptable_filter"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if these modules are already loaded? (because a user switches from iptables to nftables)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nftables' NAT rules won't have any effect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script will now error if ip_tables is loaded. Fixed.

description =
''
The ruleset file to be used with nftables. Should be in a format that
can be loaded using "/bin/nft -f". The ruleset is updated atomically.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should change these path descriptions somehow, since /bin/nft will not be a thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just 'nft'?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@Mic92
Copy link
Member

Mic92 commented Oct 22, 2016

@aneeshusa even if ExecStart is atomic, one still want to have an ExecReload, because otherwise it will leave the system unprotected for a short amount of time between ExecStop and ExecStart on restart.

@spacekitteh
Copy link
Contributor

@grahamc security

@Mic92
Copy link
Member

Mic92 commented Feb 26, 2017

Thanks!

@primeos
Copy link
Member

primeos commented Feb 26, 2017

@Mic92 Why did you merge this without any regard to #23181? (Don't want to flame her but I'm honestly a bit upset...)

A also see multiple issues with this PR (not that familiar with nftables though, so I might be wrong...). I'll probably post some comments anyway.

Copy link
Member

@primeos primeos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally it looks good but I really don't like some design choices...

default =
''
table inet filter {
# Block all IPv4/IPv6 input traffic except SSH.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ct state {established, related} accept
iifname lo accept

Block all IPv4/IPv6 input traffic except SSH.

Doesn't block all traffic... (only new traffic)
I don't like that port 22 is hard-coded.

reject
}

# Allow anything in.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the output chain?

chain output {
type filter hook output priority 0;
ct state invalid reject
ct state {established, related} accept
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally one would use new here as well (at least with iptables).

ct state invalid reject
ct state {established, related} accept
oifname lo accept
accept
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We accept everything (that hasn't an invalid conntrack state) anyway.

'';
example =
''
# Check out http://wiki.nftables.org/ for better documentation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally prefer HTTPS links.

}
}

# Block all IPv6 traffic.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably very common at the moment but I really think we shouldn't include this in an example.

The IAB expects that the IETF will stop requiring IPv4 compatibility in new or extended protocols. Future IETF protocol work will then optimize for and depend on IPv6.

https://www.iab.org/2016/11/07/iab-statement-on-ipv6/

can be loaded using "/bin/nft -f". The ruleset is updated atomically.
'';
};
networking.nftables.rulesetFile = mkOption {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great 😄

@Mic92
Copy link
Member

Mic92 commented Feb 26, 2017

@primeos I saw your rfc, but I thought this pull request orthogonal. A generic firewall can still apply its rules using this module. Also my guess was that it would also take some time, while this module is already useful. I address your concerns regarding the default rule set.

@Mic92
Copy link
Member

Mic92 commented Feb 26, 2017

How about leaving it ruleset null at the moment by default and moving default to example? Then user can set rules as they like.

@primeos
Copy link
Member

primeos commented Feb 26, 2017

Also my guess was that it would also take some time, while this module is already useful.

Agreed, I just hope we don't have to make any changes later that could break many setups.

How about leaving it ruleset null at the moment by default and moving default to example? Then user can set rules as they like.

Would probably be a good idea. Generally that obviously wouldn't be desirable but it would hopefully prevent SSH breakage (e.g. with services.openssh.ports).

But we should probably at least keep "# Check out https://wiki.nftables.org/ for better documentation." and fix the "# Allow anything in." comment.

I saw your rfc, but I thought this pull request orthogonal.

It probably is (I initially feared this would cause too many problems later but I guess I was wrong since the module is pretty minimal and we can probably easily extend it).

@primeos
Copy link
Member

primeos commented Feb 26, 2017

Suggestion for the example:

          # Check out https://wiki.nftables.org/ for better documentation.

          # Table for both IPv4 and IPv6.
          table inet filter {
            # Block all IPv4/IPv6 input traffic except SSH.
            chain input {
              type filter hook input priority 0;

              # accept any localhost traffic
              iifname lo accept

              # accept traffic originated from us
              ct state {established, related} accept

              # accept neighbour discovery otherwise IPv6 connectivity breaks
              ip6 nexthdr icmpv6 icmpv6 type { nd-neighbor-solicit, echo-request, nd-router-advert, nd-neighbor-advert } accept

              # accept SSH connections (required for a server)
              tcp dport 22 accept
              
              # count and drop any other traffic
              counter drop
            }
            
            # Allow anything out.
            chain output {
              type filter hook output priority 0;

              oifname lo accept

              accept
            }
            
            chain forward {
              type filter hook forward priority 0;

              accept
            }
          }

By far not a perfect example (I mainly copied the one from https://wiki.nftables.org/wiki-nftables/index.php/Simple_ruleset_for_a_workstation) but it should work (requires testing, unfortunately I can't do that ATM) but it should e.g. work with IPv6. But note that this can potentially still cause a lot of problems (it probably doesn't accept any ICMP packets -> outgoing ping, traceroute, etc. won't work).

@primeos
Copy link
Member

primeos commented Feb 26, 2017

A potential fix (from the Arch-Wiki) for the ICMP packets (but they might already be covered by the conntrack state related):

# ICMP
# routers may also want: mld-listener-query, nd-router-solicit
ip6 nexthdr icmpv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
ip protocol icmp icmp type { destination-unreachable, router-advertisement, time-exceeded, parameter-problem } accept

I'd probably add fragmentation-required for ICMP(v4) as well (just to be extra save - but since it's only an example it probably doesn't really matter anyway). And sorry for the delayed update... :o (doesn't really matter if it's too late).

@Mic92
Copy link
Member

Mic92 commented Feb 26, 2017

changed in 6c36d9f

@primeos
Copy link
Member

primeos commented Feb 26, 2017

@Mic92 Great, thanks 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet