Subject: [PATCH] services: Modernize and test nftables service.

From: Maxim Cournoyer <[email-masked]>
Date: Sat, 10 May 2025 23:33:44 +0900

* doc/guix.texi (Networking Services) <nftables>: Update doc.
* gnu/services/networking.scm (list-of-debug-levels?):
(debug-level?, maybe-list-of-debug-levels?):
(nftables-configuration): Rewrite using `define-configuration'.
[debug-levels]: New field.
(nftables-shepherd-service): Honor it.
* gnu/tests/networking.scm (%inetd-echo-port): Extract to top level.
(run-iptables-test): Adjust accordingly.
(make-nftables-os): New procedure.
(%default-nftables-ruleset-for-tests): New variable.
(%nftables-os): Likewise.
(%test-nftables): New test.

Change-Id: I2889603342ff6d2be6261c3de6e4fddd9a9bbe2d
---

I investigated to also have a validated ruleset file done in a
computed-file:

modified   gnu/services/networking.scm
@@ -2345,6 +2345,16 @@ (define-configuration/no-serialization nftables-configuration
 ruleset rejects all incoming connections except those to TCP port 22, with
 connections from the loopback interface are allowed."))
 
+(define (validated-ruleset nft ruleset)
+  "Check the nftables RULESET.  Return a build error in case RULESET is not
+valid, else a computed-file object of the validated RULESET."
+  (computed-file "nftables.conf"
+                 (with-imported-modules '((guix build utils))
+                   #~(begin
+                       (use-modules (guix build utils))
+                       (invoke #+nft "--check" "--file" #$ruleset)
+                       (copy-file #$ruleset #$output)))))
+
 (define (nftables-shepherd-service config)
   (match-record config <nftables-configuration>
                 (package debug-levels ruleset)
@@ -2359,8 +2369,8 @@ (define (nftables-shepherd-service config)
                                  (list (format #f "--debug=~{~a~^,~}"
                                                debug-levels))
                                  #~())
-                          "--file" #$ruleset)))
+                          "--file" #+(validated-ruleset nft ruleset))))
        (stop #~(lambda _
                  (invoke #$nft "flush" "ruleset")))))))

but 'nft' is not happy to run in the Guix build
environment:

  building /gnu/store/g4czvsmaccx181h395hp4992i0y3rqqx-nftables.conf.drv...
  netlink: Error: cache initialization failed: Operation not permitted

 doc/guix.texi               |  40 +++++++++----
 gnu/services/networking.scm |  49 +++++++++++-----
 gnu/tests/networking.scm    | 113 ++++++++++++++++++++++++++++++++++--
 3 files changed, 172 insertions(+), 30 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 889eab2ab35..2f0cd117a03 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -22606,32 +22606,48 @@ Networking Services
 @end deftp
 
 @cindex nftables
+@cindex firewall, nftables
 @defvar nftables-service-type
-This is the service type to set up a nftables configuration.  nftables is a
-netfilter project that aims to replace the existing iptables, ip6tables,
+This is the service type to set up a nftables configuration.  nftables
+is a netfilter project that aims to replace the iptables, ip6tables,
 arptables and ebtables framework.  It provides a new packet filtering
-framework, a new user-space utility @command{nft}, and a compatibility layer
-for iptables.  This service comes with a default ruleset
-@code{%default-nftables-ruleset} that rejecting all incoming connections
-except those to the ssh port 22.  To use it, simply write:
+framework, a new user-space utility @command{nft}, and a compatibility
+layer for iptables.  This service comes with a default ruleset,
+@code{%default-nftables-ruleset}, that rejects all incoming connections
+except those to the SSH port 22 (TCP).  To use it, simply write:
 
 @lisp
 (service nftables-service-type)
 @end lisp
 @end defvar
 
+@c %start of fragment
+
 @deftp {Data Type} nftables-configuration
-The data type representing the configuration of nftables.
+Available @code{nftables-configuration} fields are:
 
 @table @asis
-@item @code{package} (default: @code{nftables})
-The nftables package that provides @command{nft}.
-@item @code{ruleset} (default: @code{%default-nftables-ruleset})
-The nftables ruleset to use.  This may be any ``file-like'' object
-(@pxref{G-Expressions, file-like objects}).
+@item @code{package} (default: @code{nftables}) (type: file-like)
+The @code{nftables} package to use.
+
+@item @code{debug-levels} (type: maybe-list-of-debug-levels)
+A list of debug levels, for enabling debugging output.  Valid debug
+level values are the @samp{scanner}, @samp{parser}, @samp{eval},
+@samp{netlink}, @samp{mnl}, @samp{proto-ctx}, @samp{segtree} or
+@samp{all} symbols.
+
+@item @code{ruleset} (type: file-like)
+A file-like object containing the complete nftables ruleset.  The
+default ruleset rejects all incoming connections except those to TCP
+port 22, with connections from the loopback interface are allowed.
+
 @end table
+
 @end deftp
 
+
+@c %end of fragment
+
 @cindex NTP (Network Time Protocol), service
 @cindex ntpd, service for the Network Time Protocol daemon
 @cindex real time clock
diff --git a/gnu/services/networking.scm b/gnu/services/networking.scm
index 67653e2cbf5..8b7bf668927 100644
--- a/gnu/services/networking.scm
+++ b/gnu/services/networking.scm
@@ -10,7 +10,7 @@
 ;;; Copyright © 2018 Chris Marusich <[email-masked]>
 ;;; Copyright © 2018 Arun Isaac <[email-masked]>
 ;;; Copyright © 2019 Florian Pelz <[email-masked]>
-;;; Copyright © 2019, 2021, 2024 Maxim Cournoyer <[email-masked]>
+;;; Copyright © 2019, 2021, 2024, 2025 Maxim Cournoyer <[email-masked]>
 ;;; Copyright © 2019 Sou Bunnbu <[email-masked]>
 ;;; Copyright © 2019 Alex Griffin <[email-masked]>
 ;;; Copyright © 2020 Brice Waegeneire <[email-masked]>
@@ -80,6 +80,7 @@ (define-module (gnu services networking)
   #:use-module (srfi srfi-9)
   #:use-module (srfi srfi-26)
   #:use-module (srfi srfi-43)
+  #:use-module (ice-9 format)
   #:use-module (ice-9 match)
   #:use-module (ice-9 string-fun)
   #:use-module (json)
@@ -258,6 +259,7 @@ (define-module (gnu services networking)
             nftables-configuration
             nftables-configuration?
             nftables-configuration-package
+            nftables-configuration-debug-levels
             nftables-configuration-ruleset
             %default-nftables-ruleset
 
@@ -2279,12 +2281,12 @@ (define iptables-service-type
                              (compose list iptables-shepherd-service))))))
 
 ;;;
-;;; nftables
+;;; nftables.
 ;;;
 
 (define %default-nftables-ruleset
-  (plain-file "nftables.conf"
-              "# A simple and safe firewall
+  (plain-file "nftables.conf" "\
+# A simple and safe firewall
 table inet filter {
   chain input {
     type filter hook input priority 0; policy drop;
@@ -2320,25 +2322,44 @@ (define %default-nftables-ruleset
 }
 "))
 
-(define-record-type* <nftables-configuration>
-  nftables-configuration
-  make-nftables-configuration
-  nftables-configuration?
-  (package nftables-configuration-package
-           (default nftables))
-  (ruleset nftables-configuration-ruleset ; file-like object
-           (default %default-nftables-ruleset)))
+(define (debug-level? x)
+  (member x '(scanner parser eval netlink mnl proto-ctx segtree all)))
+
+(define list-of-debug-levels?
+  (list-of debug-level?))
+
+(define-maybe/no-serialization list-of-debug-levels)
+
+(define-configuration/no-serialization nftables-configuration
+  (package
+    (file-like nftables)
+    "The @code{nftables} package to use.")
+  (debug-levels
+   maybe-list-of-debug-levels
+   "A list of debug levels, for enabling debugging output.  Valid debug level values
+are the @samp{scanner}, @samp{parser}, @samp{eval}, @samp{netlink},
+@samp{mnl}, @samp{proto-ctx}, @samp{segtree} or @samp{all} symbols.")
+  (ruleset
+   (file-like %default-nftables-ruleset)
+   "A file-like object containing the complete nftables ruleset.  The default
+ruleset rejects all incoming connections except those to TCP port 22, with
+connections from the loopback interface are allowed."))
 
 (define (nftables-shepherd-service config)
   (match-record config <nftables-configuration>
-    (package ruleset)
+                (package debug-levels ruleset)
     (let ((nft (file-append package "/sbin/nft")))
       (shepherd-service
        (documentation "Packet filtering and classification")
        (actions (list (shepherd-configuration-action ruleset)))
        (provision '(nftables))
        (start #~(lambda _
-                  (invoke #$nft "--file" #$ruleset)))
+                  (invoke #$nft
+                          #$@(if (maybe-value-set? debug-levels)
+                                 (list (format #f "--debug=~{~a~^,~}"
+                                               debug-levels))
+                                 #~())
+                          "--file" #$ruleset)))
        (stop #~(lambda _
                  (invoke #$nft "flush" "ruleset")))))))
 
diff --git a/gnu/tests/networking.scm b/gnu/tests/networking.scm
index 7d54ebba50e..d3966d5f0da 100644
--- a/gnu/tests/networking.scm
+++ b/gnu/tests/networking.scm
@@ -5,6 +5,7 @@
 ;;; Copyright © 2018 Arun Isaac <[email-masked]>
 ;;; Copyright © 2021 Maxime Devos <[email-masked]>
 ;;; Copyright © 2021, 2023-2024 Ludovic Courtès <[email-masked]>
+;;; Copyright © 2025 Maxim Cournoyer <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,6 +29,7 @@ (define-module (gnu tests networking)
   #:use-module (gnu services)
   #:use-module (gnu services base)
   #:use-module (gnu services networking)
+  #:use-module (gnu services ssh)
   #:use-module (guix gexp)
   #:use-module (guix store)
   #:use-module (guix monads)
@@ -48,6 +50,7 @@ (define-module (gnu tests networking)
             %test-dhcpcd
             %test-tor
             %test-iptables
+            %test-nftables
             %test-ipfs))
 
 
@@ -870,6 +873,8 @@ (define %test-tor
    (description "Test a running Tor daemon configuration.")
    (value (run-tor-test))))
 
+(define %inetd-echo-port 7)
+
 (define* (run-iptables-test)
   "Run tests of 'iptables-service-type'."
   (define iptables-rules
@@ -890,8 +895,6 @@ (define* (run-iptables-test)
 COMMIT
 ")
 
-  (define inetd-echo-port 7)
-
   (define os
     (marionette-operating-system
      (simple-operating-system
@@ -967,7 +970,8 @@ (define* (run-iptables-test)
 
           (test-error "iptables firewall blocks access to inetd echo service"
                       'misc-error
-                      (wait-for-tcp-port inetd-echo-port marionette #:timeout 5))
+                      (wait-for-tcp-port #$%inetd-echo-port marionette
+                                         #:timeout 5))
 
           ;; TODO: This test freezes up at the login prompt without any
           ;; relevant messages on the console. Perhaps it is waiting for some
@@ -979,7 +983,7 @@ (define* (run-iptables-test)
           ;;         (use-modules (gnu services herd))
           ;;         (stop-service 'iptables))
           ;;      marionette)
-          ;;     (wait-for-tcp-port inetd-echo-port marionette #:timeout 5)))
+          ;;     (wait-for-tcp-port #$%inetd-echo-port marionette #:timeout 5)))
 
           (test-end))))
 
@@ -991,6 +995,107 @@ (define %test-iptables
    (description "Test a running iptables daemon.")
    (value (run-iptables-test))))
 
+
+;;;
+;;; nftables.
+;;;
+
+(define (make-nftables-os ruleset)
+  (simple-operating-system
+   (service dhcp-client-service-type)
+   (service inetd-service-type
+            (inetd-configuration
+             (entries (list
+                       (inetd-entry
+                        (name "echo")
+                        (socket-type 'stream)
+                        (protocol "tcp")
+                        (wait? #f)
+                        (user "root"))))))
+   (service openssh-service-type)
+   (service nftables-service-type
+            (nftables-configuration
+             (debug-levels '(all))
+             (ruleset ruleset)))))
+
+(define %default-nftables-ruleset-for-tests
+  ;; This is like the %default-nftables-ruleset, but without allowing any
+  ;; connections from the loopback interface.
+  (plain-file "nftables.conf" "\
+table inet filter {
+  chain input {
+    type filter hook input priority 0; policy drop;
+
+    # early drop of invalid connections
+    ct state invalid drop
+
+    # allow established/related connections
+    ct state { established, related } accept
+
+    # allow from loopback
+    # iif lo accept   # COMMENTED OUT FOR TESTS
+    # drop connections to lo not coming from lo
+    iif != lo ip daddr 127.0.0.1/8 drop
+    iif != lo ip6 daddr ::1/128 drop
+
+    # allow icmp
+    ip protocol icmp accept
+    ip6 nexthdr icmpv6 accept
+
+    # allow ssh
+    tcp dport ssh accept
+
+    # reject everything else
+    reject with icmpx type port-unreachable
+  }
+  chain forward {
+    type filter hook forward priority 0; policy drop;
+  }
+  chain output {
+    type filter hook output priority 0; policy accept;
+  }
+}"))
+
+(define %nftables-os
+  (make-nftables-os %default-nftables-ruleset-for-tests))
+
+(define (run-nftables-test)
+  (define os
+    (marionette-operating-system
+     %nftables-os
+     #:imported-modules '((gnu services herd))
+     #:requirements '(inetd nftables ssh)))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (gnu build marionette)
+                       (srfi srfi-64))
+          (define marionette
+            (make-marionette (list #$(virtual-machine os))))
+
+          (test-runner-current (system-test-runner #$output))
+          (test-begin "nftables")
+
+          (test-error "nftables blocks access to inetd echo service"
+                      'misc-error
+                      (wait-for-tcp-port #$%inetd-echo-port marionette
+                                         #:timeout 5))
+
+          (test-assert "nftables allows access to SSH TCP port 22"
+            (wait-for-tcp-port 22 marionette))
+
+          (test-end))))
+
+  (gexp->derivation "nftables-test" test))
+
+(define %test-nftables
+  (system-test
+   (name "nftables")
+   (description "Tests that an error is raised when attempting build an OS
+with an invalid nftables ruleset file.")
+   (value (run-nftables-test))))
+
 
 ;;;
 ;;; IPFS service

base-commit: f348d2be2e019fcda44af8ab81073e2f04697a38
-- 
2.49.0