Subject: [bug#32465] Add iptables service

From: Arun Isaac <[email-masked]>
Date: Fri, 14 Sep 2018 16:29:09 +0530

Hi,

I generalized this service to work for both iptables and ip6tables. I
added system tests, and made the other corrections that were
suggested. Some questions follow.

- Is the example I added for the iptables.rules sufficient? I couldn't
  find upstream documentation for the iptables.rules format. I suspect
  it doesn't exist. Do you know of any upstream documentation that can
  be referred to here?
- In the attached patch, the fourth test ("inetd echo service is
  accessible after iptables firewall is stopped") doesn't work. In that
  service, I am trying to stop the iptables service, but I'm not able
  to. How do I programmatically stop the iptables service? Is what I
  have done correct?

Any other feedback is also welcome.

Thank you.

>From dcfdd0bd981aa9da4835f322173490e239048e65 Mon Sep 17 00:00:00 2001
From: Arun Isaac <[email-masked]>
Date: Fri, 17 Aug 2018 16:39:07 +0530
Subject: [PATCH] gnu: services: Add iptables service.

* gnu/services/networking.scm (<iptables-configuration>): New record type.
(iptables-service-type): New variable.
* gnu/tests/networking.scm (run-iptables-test): New procedure.
(%test-iptables): New variable.
* doc/guix.texi (Networking Services): Document it.
---
 doc/guix.texi               |  48 ++++++++++++++
 gnu/services/networking.scm |  56 ++++++++++++++++-
 gnu/tests/networking.scm    | 122 +++++++++++++++++++++++++++++++++++-
 3 files changed, 224 insertions(+), 2 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index cccf166d0..7dd7f8fc9 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -11575,6 +11575,54 @@ Thus, it can be instantiated like this:
 @end lisp
 @end defvr
 
+@cindex iptables
+@defvr {Scheme Variable} iptables-service-type
+This is the service type to set up an iptables configuration. iptables is a
+packet filtering framework supported by the Linux kernel.  This service
+supports configuring iptables for both IPv4 and IPv6.  A simple example
+configuration rejecting all incoming connections except those to the ssh port
+22 is shown below.
+
+@lisp
+(service iptables-service-type
+         (iptables-configuration
+          (ipv4-rules (plain-file "iptables.rules" "*filter
+:INPUT ACCEPT
+:FORWARD ACCEPT
+:OUTPUT ACCEPT
+-A INPUT -p tcp --dport 22 -j ACCEPT
+-A INPUT -j REJECT --reject-with icmp-port-unreachable
+COMMIT
+"))
+          (ipv6-rules (plain-file "ip6tables.rules" "*filter
+:INPUT ACCEPT
+:FORWARD ACCEPT
+:OUTPUT ACCEPT
+-A INPUT -p tcp --dport 22 -j ACCEPT
+-A INPUT -j REJECT --reject-with icmp6-port-unreachable
+COMMIT
+"))))
+@end lisp
+@end defvr
+
+@deftp {Data Type} iptables-configuration
+The data type representing the configuration of iptables.
+
+@table @asis
+@item @code{iptables} (default: @code{iptables})
+The iptables package that provides @code{iptables-restore} and
+@code{ip6tables-restore}.
+@item @code{ipv4-rules} (default: @code{%iptables-accept-all-rules})
+The iptables rules to use.  It will be passed to @code{iptables-restore}.
+This may be any ``file-like'' object (@pxref{G-Expressions, file-like
+objects}).
+@item @code{ipv6-rules} (default: @code{%iptables-accept-all-rules})
+The ip6tables rules to use.  It will be passed to @code{ip6tables-restore}.
+This may be any ``file-like'' object (@pxref{G-Expressions, file-like
+objects}).
+@end table
+@end deftp
+
 @cindex NTP
 @cindex real time clock
 @deffn {Scheme Procedure} ntp-service [#:ntp @var{ntp}] @
diff --git a/gnu/services/networking.scm b/gnu/services/networking.scm
index b6b5ee3fe..bd1d5a270 100644
--- a/gnu/services/networking.scm
+++ b/gnu/services/networking.scm
@@ -8,6 +8,7 @@
 ;;; Copyright © 2017 Marius Bakke <[email-masked]>
 ;;; Copyright © 2018 Tobias Geerinckx-Rice <[email-masked]>
 ;;; Copyright © 2018 Chris Marusich <[email-masked]>
+;;; Copyright © 2018 Arun Isaac <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -103,7 +104,14 @@
             wpa-supplicant-service-type
 
             openvswitch-service-type
-            openvswitch-configuration))
+            openvswitch-configuration
+
+            iptables-configuration
+            iptables-configuration?
+            iptables-configuration-iptables
+            iptables-configuration-ipv4-rules
+            iptables-configuration-ipv6-rules
+            iptables-service-type))
 
 ;;; Commentary:
 ;;;
@@ -1108,4 +1116,50 @@ networking."))))
 switch designed to enable massive network automation through programmatic
 extension.")))
 
+;;;
+;;; iptables
+;;;
+
+(define %iptables-accept-all-rules
+  (plain-file "iptables-accept-all.rules"
+              "*filter
+:INPUT ACCEPT
+:FORWARD ACCEPT
+:OUTPUT ACCEPT
+COMMIT
+"))
+
+(define-record-type* <iptables-configuration>
+  iptables-configuration make-iptables-configuration iptables-configuration?
+  (iptables iptables-configuration-iptables
+            (default iptables))
+  (ipv4-rules iptables-configuration-ipv4-rules
+              (default %iptables-accept-all-rules))
+  (ipv6-rules iptables-configuration-ipv6-rules
+              (default %iptables-accept-all-rules)))
+
+(define iptables-shepherd-service
+  (match-lambda
+    (($ <iptables-configuration> iptables ipv4-rules ipv6-rules)
+     (let ((iptables-restore (file-append iptables "/sbin/iptables-restore"))
+           (ip6tables-restore (file-append iptables "/sbin/ip6tables-restore")))
+       (shepherd-service
+        (documentation "Packet filtering framework")
+        (provision '(iptables))
+        (start #~(lambda _
+                   (invoke #$iptables-restore #$ipv4-rules)
+                   (invoke #$ip6tables-restore #$ipv6-rules)))
+        (stop #~(lambda _
+                  (invoke #$iptables-restore #$%iptables-accept-all-rules)
+                  (invoke #$ip6tables-restore #$%iptables-accept-all-rules))))))))
+
+(define iptables-service-type
+  (service-type
+   (name 'iptables)
+   (description
+    "Run @command{iptables-restore}, setting up the specified rules.")
+   (extensions
+    (list (service-extension shepherd-root-service-type
+                             (compose list iptables-shepherd-service))))))
+
 ;;; networking.scm ends here
diff --git a/gnu/tests/networking.scm b/gnu/tests/networking.scm
index 381c5caf1..58fe61a49 100644
--- a/gnu/tests/networking.scm
+++ b/gnu/tests/networking.scm
@@ -2,6 +2,7 @@
 ;;; Copyright © 2017 Thomas Danckaert <[email-masked]>
 ;;; Copyright © 2017 Marius Bakke <[email-masked]>
 ;;; Copyright © 2018 Chris Marusich <[email-masked]>
+;;; Copyright © 2018 Arun Isaac <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -29,9 +30,11 @@
   #:use-module (guix store)
   #:use-module (guix monads)
   #:use-module (gnu packages bash)
+  #:use-module (gnu packages linux)
   #:use-module (gnu packages networking)
   #:use-module (gnu services shepherd)
-  #:export (%test-inetd %test-openvswitch %test-dhcpd %test-tor))
+  #:use-module (ice-9 match)
+  #:export (%test-inetd %test-openvswitch %test-dhcpd %test-tor %test-iptables))
 
 (define %inetd-os
   ;; Operating system with 2 inetd services.
@@ -434,3 +437,120 @@ subnet 192.168.1.0 netmask 255.255.255.0 {
    (name "tor")
    (description "Test a running Tor daemon configuration.")
    (value (run-tor-test))))
+
+(define* (run-iptables-test)
+  "Run tests of 'iptables-service-type'."
+  (define iptables-rules
+    "*filter
+:INPUT ACCEPT
+:FORWARD ACCEPT
+:OUTPUT ACCEPT
+-A INPUT -p tcp -m tcp --dport 7 -j REJECT --reject-with icmp-port-unreachable
+COMMIT
+")
+
+  (define ip6tables-rules
+    "*filter
+:INPUT ACCEPT
+:FORWARD ACCEPT
+:OUTPUT ACCEPT
+-A INPUT -p tcp -m tcp --dport 7 -j REJECT --reject-with icmp6-port-unreachable
+COMMIT
+")
+
+  (define inetd-echo-port 7)
+
+  (define os
+    (marionette-operating-system
+     (simple-operating-system
+      (dhcp-client-service)
+      (service inetd-service-type
+               (inetd-configuration
+                (entries (list
+                          (inetd-entry
+                           (name "echo")
+                           (socket-type 'stream)
+                           (protocol "tcp")
+                           (wait? #f)
+                           (user "root"))))))
+      (service iptables-service-type
+               (iptables-configuration
+                (ipv4-rules (plain-file "iptables.rules" iptables-rules))
+                (ipv6-rules (plain-file "ip6tables.rules" ip6tables-rules)))))
+     #:imported-modules '((gnu services herd))
+     #:requirements '(inetd iptables)))
+
+  (define test
+    (with-imported-modules '((gnu build marionette))
+      #~(begin
+          (use-modules (srfi srfi-64)
+                       (gnu build marionette))
+          (define marionette
+            (make-marionette (list #$(virtual-machine os))))
+
+          (define (dump-iptables iptables-save marionette)
+            (marionette-eval
+             `(begin
+                (use-modules (ice-9 popen)
+                             (ice-9 rdelim)
+                             (ice-9 regex))
+                (call-with-output-string
+                  (lambda (out)
+                    (call-with-port
+                     (open-pipe* OPEN_READ ,iptables-save)
+                     (lambda (in)
+                       (let loop ((line (read-line in)))
+                         ;; iptables-save does not output rules in the exact
+                         ;; same format we loaded using iptables-restore. It
+                         ;; adds comments, packet counters, etc. We remove
+                         ;; these additions.
+                         (unless (eof-object? line)
+                           (cond
+                            ;; Remove comments
+                            ((string-match "^#" line) #t)
+                            ;; Remove packet counters
+                            ((string-match "^:([A-Z]*) ([A-Z]*) .*" line)
+                             => (lambda (match-record)
+                                  (format out ":~a ~a~%"
+                                          (match:substring match-record 1)
+                                          (match:substring match-record 2))))
+                            ;; Pass other lines without modification
+                            (else (display line out)
+                                  (newline out)))
+                           (loop (read-line in)))))))))
+             marionette))
+
+          (mkdir #$output)
+          (chdir #$output)
+
+          (test-begin "iptables")
+
+          (test-equal "iptables-save dumps the same rules that were loaded"
+            (dump-iptables #$(file-append iptables "/sbin/iptables-save")
+                           marionette)
+            #$iptables-rules)
+
+          (test-equal "ip6tables-save dumps the same rules that were loaded"
+            (dump-iptables #$(file-append iptables "/sbin/ip6tables-save")
+                           marionette)
+            #$ip6tables-rules)
+
+          (test-error "iptables firewall blocks access to inetd echo service"
+                      'misc-error
+                      (wait-for-tcp-port inetd-echo-port marionette #:timeout 5))
+
+          (test-assert "inetd echo service is accessible after iptables firewall is stopped"
+            (begin
+              (marionette-eval '(stop-service 'iptables) marionette)
+              (wait-for-tcp-port inetd-echo-port marionette #:timeout 5)))
+
+          (test-end)
+          (exit (= (test-runner-fail-count (test-runner-current)) 0)))))
+
+  (gexp->derivation "iptables" test))
+
+(define %test-iptables
+  (system-test
+   (name "iptables")
+   (description "Test a running iptables daemon.")
+   (value (run-iptables-test))))
-- 
2.18.0