]> git.ipfire.org Git - thirdparty/libvirt.git/commitdiff
network: introduce Packet Filter firewall backend
authorRoman Bogorodskiy <bogorodskiy@gmail.com>
Tue, 22 Apr 2025 17:11:28 +0000 (19:11 +0200)
committerRoman Bogorodskiy <bogorodskiy@gmail.com>
Tue, 5 Aug 2025 17:28:57 +0000 (19:28 +0200)
Implement NAT networking support based on the Packet Filter (pf)
firewall in FreeBSD. At this point, the implementation is very basic.
It creates:

 - Essential NAT translation rules
 - Basic forwarding rules

Implementation uses pf's anchor feature to group rules. All rules live
in the "libvirt" anchor and every libvirt's network has its own
sub-anchor.

Currently there are some assumptions and limitations:

 - We assume that a user has created the "libvirt" (nat-)anchors. As
   they cannot be created on fly, it's better not to touch global pf
   configuration and let the user do the changes. If the user doesn't
   have these anchors configured, the rules will still be created in
   sub-anchors, but will not be effective until these anchors are
   activated. Should we check if these anchors are not active to
   give some runtime warning?

 - Currently, rule reloading is not smart: it always deletes rules,
   flushes rules and re-creates that. It would be better to do that
   more gracefully.

 - IPv6 configurations are currently not supported

 - For NAT, pf requires explicit IP address or an interface to NAT to.
   We try to obtain that from the network XML definition, and if it's
   not specified, we try to determine interface corresponding to the
   default route.

Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
Signed-off-by: Roman Bogorodskiy <bogorodskiy@gmail.com>
meson.build
po/POTFILES
src/network/bridge_driver_conf.c
src/network/bridge_driver_linux.c
src/network/meson.build
src/network/network_pf.c [new file with mode: 0644]
src/network/network_pf.h [new file with mode: 0644]
src/util/virfirewall.c
src/util/virfirewall.h

index 68b50bf47d2fb199d17f923a9ef71f650b7726a6..707803b202c3836f9a284ccc7f89341e2404a2f5 100644 (file)
@@ -1606,6 +1606,8 @@ if not get_option('driver_network').disabled() and conf.has('WITH_LIBVIRTD')
   if firewall_backend_priority.length() == 0
       if host_machine.system() == 'linux'
           firewall_backend_priority = ['nftables', 'iptables']
+      elif host_machine.system() == 'freebsd'
+          firewall_backend_priority = ['pf']
       else
           # No firewall impl on non-Linux so far, so force 'none'
           # as placeholder
index 9747c3895146393062bd54334cebc1dac8180090..084f60ba00a51df2fa943f6a88cb38f5db686662 100644 (file)
@@ -151,6 +151,7 @@ src/network/bridge_driver_nop.c
 src/network/leaseshelper.c
 src/network/network_iptables.c
 src/network/network_nftables.c
+src/network/network_pf.c
 src/node_device/node_device_driver.c
 src/node_device/node_device_udev.c
 src/nwfilter/nwfilter_dhcpsnoop.c
index 738652390f925349a07f1958b46dc98998f9605a..309d64fa848061eebc92905bbc2f2a812e40336f 100644 (file)
@@ -129,6 +129,10 @@ virNetworkLoadDriverConfig(virNetworkDriverConfig *cfg G_GNUC_UNUSED,
             break;
         }
 
+        case VIR_FIREWALL_BACKEND_PF: {
+            break;
+        }
+
         case VIR_FIREWALL_BACKEND_LAST:
             virReportEnumRangeError(virFirewallBackend, fwBackends[i]);
             return -1;
index 86f6a5915f74bab2882a49136863fcd06ef38eb1..9077178c3ef82f2ada85ae44452ae4f33b663996 100644 (file)
@@ -58,6 +58,7 @@ networkFirewallSetupPrivateChains(virFirewallBackend backend,
     case VIR_FIREWALL_BACKEND_NFTABLES:
         return nftablesSetupPrivateChains(layer);
 
+    case VIR_FIREWALL_BACKEND_PF:
     case VIR_FIREWALL_BACKEND_LAST:
         virReportEnumRangeError(virFirewallBackend, backend);
         return -1;
@@ -437,6 +438,7 @@ networkAddFirewallRules(virNetworkDef *def,
         case VIR_FIREWALL_BACKEND_NFTABLES:
             return nftablesAddFirewallRules(def, fwRemoval);
 
+        case VIR_FIREWALL_BACKEND_PF:
         case VIR_FIREWALL_BACKEND_LAST:
             virReportEnumRangeError(virFirewallBackend, firewallBackend);
             return -1;
index 07cd5cda55b375690437468555255fa048a2f700..51bbf7063d80a2bd7e6201daa9dcc4d751ace691 100644 (file)
@@ -6,6 +6,10 @@ network_driver_sources = [
   'network_nftables.c',
 ]
 
+if host_machine.system() == 'freebsd'
+  network_driver_sources += 'network_pf.c'
+endif
+
 driver_source_files += files(network_driver_sources)
 stateful_driver_source_files += files(network_driver_sources)
 
diff --git a/src/network/network_pf.c b/src/network/network_pf.c
new file mode 100644 (file)
index 0000000..ce4461c
--- /dev/null
@@ -0,0 +1,326 @@
+/*
+ * network_pf.c: pf-based firewall implementation for virtual networks
+ *
+ * Copyright (C) 2025 FreeBSD Foundation
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * pf(4) configuration principles/assumptions.
+ *
+ * All libvirt-managed firewall rule are configured within a pf anchor.
+ * Every libvirt network has a corresponding sub-anchor, like "libvirt/$network_name".
+ * Libvirt does not create the root anchors, so users are expected to specify them in
+ * their firewall configuration. Minimal configuration might look like:
+ *
+ * # cat /etc/pf.conf
+ * scrub all
+ *
+ * nat-anchor "libvirt\*"
+ * anchor "libvirt\*"
+ *
+ * pass all
+ * #
+ *
+ * Users are not expected to add/modify rules in the "libvirt\*" subanchors because
+ * the changes will be lost on restart.
+ *
+ * IPv6 NAT is currently not supported.
+ */
+
+#include <config.h>
+
+#include <stdarg.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/socket.h>
+#ifdef WITH_NET_IF_H
+# include <net/if.h>
+#endif
+#include <sys/sysctl.h>
+#include <net/if_dl.h>
+#include <net/route.h>
+#include <netinet/in.h>
+
+#include "internal.h"
+#include "virfirewalld.h"
+#include "vircommand.h"
+#include "virerror.h"
+#include "virlog.h"
+#include "virhash.h"
+#include "virenum.h"
+#include "virstring.h"
+#include "network_pf.h"
+
+VIR_LOG_INIT("network.pf");
+
+#define VIR_FROM_THIS VIR_FROM_NONE
+
+
+static const char networkLocalMulticastIPv4[] = "224.0.0.0/24";
+static const char networkLocalBroadcast[] = "255.255.255.255/32";
+
+
+static char *
+findDefaultRouteInterface(void)
+{
+    int mib[6] = {CTL_NET, PF_ROUTE, 0, AF_INET, NET_RT_DUMP, 0};
+    size_t needed;
+    g_autofree char *buf = NULL;
+    char *lim, *next;
+    struct rt_msghdr *rtm;
+    struct sockaddr *sa;
+    struct sockaddr_in *sin;
+    struct sockaddr_dl *sdl;
+    char *ifname;
+    size_t ifname_len;
+    size_t i;
+
+    if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0) {
+        virReportSystemError(errno,
+                             "%s",
+                             _("Unable to get default interface name"));
+        return NULL;
+    }
+
+    if (posix_memalign((void **)&buf, 8, needed) != 0) {
+        virReportSystemError(errno,
+                             "%s",
+                             _("Unable to get default interface name"));
+        return NULL;
+    }
+
+    if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0) {
+        virReportSystemError(errno,
+                             "%s",
+                             _("Unable to get default interface name"));
+        return NULL;
+    }
+
+    lim = buf + needed;
+    next = buf;
+
+    while (next < lim) {
+        rtm = (struct rt_msghdr *)(void *)next;
+        if (next + rtm->rtm_msglen > lim)
+            break;
+
+        sin = (struct sockaddr_in *)(rtm + 1);
+
+        if ((rtm->rtm_flags & RTF_GATEWAY) && sin->sin_addr.s_addr == INADDR_ANY) {
+            sdl = NULL;
+            sa = (struct sockaddr *)(sin + 1);
+
+            for (i = 1; i < RTAX_MAX; i++) {
+                if (rtm->rtm_addrs & (1 << i)) {
+                    if (i == RTAX_IFP && sa->sa_family == AF_LINK) {
+                        sdl = (struct sockaddr_dl *)(void *)sa;
+                        ifname_len = (sdl->sdl_nlen >= IFNAMSIZ) ? IFNAMSIZ - 1 : sdl->sdl_nlen;
+                        ifname = g_new(char, ifname_len + 1);
+                        virStrcpy(ifname, sdl->sdl_data, ifname_len + 1);
+                        return ifname;
+                    }
+                    sa = (struct sockaddr *)((char *)sa +
+                         ((sa->sa_len > 0) ? sa->sa_len : sizeof(struct sockaddr)));
+                }
+            }
+        }
+
+        next += rtm->rtm_msglen;
+    }
+
+    return NULL;
+}
+
+static int
+pfAddNatFirewallRules(virNetworkDef *def,
+                      virNetworkIPDef *ipdef)
+{
+    /*
+     * # NAT rules
+     * table <natdst> persist
+     *   { 0.0.0.0/0, ! 192.168.122.0/24, !224.0.0.0/24, !255.255.255.255 }
+     * nat pass log on $ext_if from 192.168.122.0/24 to <natdst>
+     *   -> ($ext_if) port 1024:65535
+     *
+     * # Filtering
+     * pass log quick on virbr0 from 192.168.122.0/24 to 192.168.122.0/24
+     * pass out log quick on virbr0 from 192.168.122.0/24 to 224.0.0.0/24
+     * pass out log quick on virbr0 from 192.168.122.0/24 to 255.255.255.255
+     * block log on virbr0
+     */
+    int prefix = virNetworkIPDefPrefix(ipdef);
+    g_autofree const char *forwardIf = g_strdup(virNetworkDefForwardIf(def, 0));
+    g_auto(virBuffer) pf_rules_buf = VIR_BUFFER_INITIALIZER;
+    g_autoptr(virCommand) cmd = virCommandNew(PFCTL);
+    virPortRange *portRange = &def->forward.port;
+    g_autofree char *portRangeStr = NULL;
+
+    if (prefix < 0) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("Invalid prefix or netmask for '%1$s'"),
+                       def->bridge);
+        return -1;
+    }
+
+    if (portRange->start == 0 && portRange->end == 0) {
+        portRange->start = 1024;
+        portRange->end = 65535;
+    }
+
+    if (portRange->start < portRange->end && portRange->end < 65536) {
+        portRangeStr = g_strdup_printf("%u:%u",
+                                       portRange->start,
+                                       portRange->end);
+    } else {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("Invalid port range '%1$u-%2$u'."),
+                       portRange->start, portRange->end);
+        return -1;
+    }
+
+    if (!forwardIf) {
+        forwardIf = findDefaultRouteInterface();
+        if (!forwardIf) {
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                           "%s",
+                           _("Cannot determine the default interface"));
+            return -1;
+        }
+    }
+
+    virBufferAsprintf(&pf_rules_buf,
+                      "table <natdst> persist { 0.0.0.0/0, ! %s/%d, ! %s, ! %s }\n",
+                      virSocketAddrFormat(&ipdef->address),
+                      prefix,
+                      networkLocalMulticastIPv4,
+                      networkLocalBroadcast);
+    virBufferAsprintf(&pf_rules_buf,
+                      "nat pass on %s from %s/%d to <natdst> -> (%s) port %s\n",
+                      forwardIf,
+                      virSocketAddrFormat(&ipdef->address),
+                      prefix,
+                      forwardIf,
+                      portRangeStr);
+    virBufferAsprintf(&pf_rules_buf,
+                      "pass quick on %s from %s/%d to %s/%d\n",
+                      def->bridge,
+                      virSocketAddrFormat(&ipdef->address),
+                      prefix,
+                      virSocketAddrFormat(&ipdef->address),
+                      prefix);
+    virBufferAsprintf(&pf_rules_buf,
+                      "pass quick on %s from %s/%d to %s\n",
+                      def->bridge,
+                      virSocketAddrFormat(&ipdef->address),
+                      prefix,
+                      networkLocalMulticastIPv4);
+    virBufferAsprintf(&pf_rules_buf,
+                      "pass quick on %s from %s/%d to %s\n",
+                      def->bridge,
+                      virSocketAddrFormat(&ipdef->address),
+                      prefix,
+                      networkLocalBroadcast);
+    virBufferAsprintf(&pf_rules_buf,
+                      "block on %s\n",
+                      def->bridge);
+
+    /*  pfctl -a libvirt/default -F all -f - */
+    virCommandAddArg(cmd, "-a");
+    virCommandAddArgFormat(cmd, "libvirt/%s", def->name);
+    virCommandAddArgList(cmd, "-F", "all", "-f", "-", NULL);
+
+    virCommandSetInputBuffer(cmd, virBufferContentAndReset(&pf_rules_buf));
+
+    if (virCommandRun(cmd, NULL) < 0) {
+        VIR_WARN("Failed to create firewall rules for network %s",
+                 def->name);
+        return -1;
+    }
+    return 0;
+}
+
+
+static int
+pfAddRoutingFirewallRules(virNetworkDef *def,
+                          virNetworkIPDef *ipdef G_GNUC_UNUSED)
+{
+    int prefix = virNetworkIPDefPrefix(ipdef);
+
+    if (prefix < 0) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       _("Invalid prefix or netmask for '%1$s'"),
+                       def->bridge);
+        return -1;
+    }
+
+    /* TODO: routing rules */
+
+    return 0;
+}
+
+
+static int
+pfAddIPSpecificFirewallRules(virNetworkDef *def,
+                             virNetworkIPDef *ipdef)
+{
+    if (def->forward.type == VIR_NETWORK_FORWARD_NAT) {
+        if (VIR_SOCKET_ADDR_IS_FAMILY(&ipdef->address, AF_INET)) {
+            return pfAddNatFirewallRules(def, ipdef);
+        } else {
+            virReportError(VIR_ERR_NO_SUPPORT, "%s",
+                           _("Only IPv4 is supported"));
+            return -1;
+        }
+    } else if (def->forward.type == VIR_NETWORK_FORWARD_ROUTE) {
+        return pfAddRoutingFirewallRules(def, ipdef);
+    }
+    return 0;
+}
+
+
+int
+pfAddFirewallRules(virNetworkDef *def)
+{
+    size_t i;
+    virNetworkIPDef *ipdef;
+
+    for (i = 0;
+         (ipdef = virNetworkDefGetIPByIndex(def, AF_UNSPEC, i));
+         i++) {
+        if (pfAddIPSpecificFirewallRules(def, ipdef) < 0)
+            return -1;
+    }
+
+    return 0;
+}
+
+
+void
+pfRemoveFirewallRules(virNetworkDef *def)
+{
+    /* pfctl -a libvirt/default -F all */
+    g_autoptr(virCommand) cmd = virCommandNew(PFCTL);
+    virCommandAddArg(cmd, "-a");
+    virCommandAddArgFormat(cmd, "libvirt/%s", def->name);
+    virCommandAddArgList(cmd, "-F", "all", NULL);
+
+    if (virCommandRun(cmd, NULL) < 0)
+        VIR_WARN("Failed to remove firewall rules for network %s",
+                 def->name);
+}
diff --git a/src/network/network_pf.h b/src/network/network_pf.h
new file mode 100644 (file)
index 0000000..2cf5a1a
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * network_pf.h: helper APIs for managing pf in network driver
+ *
+ * Copyright (C) 2025 FreeBSD Foundation
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library.  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "network_conf.h"
+
+int pfAddFirewallRules(virNetworkDef *def);
+void pfRemoveFirewallRules(virNetworkDef *def);
index 9389bcf541644a79b4aecad2b102a2d589cd1beb..69521e2b4622c6f5d8110462f681d16656cad84a 100644 (file)
@@ -39,7 +39,8 @@ VIR_ENUM_IMPL(virFirewallBackend,
               VIR_FIREWALL_BACKEND_LAST,
               "none",
               "iptables",
-              "nftables");
+              "nftables",
+              "pf");
 
 VIR_ENUM_DECL(virFirewallLayer);
 VIR_ENUM_IMPL(virFirewallLayer,
@@ -847,6 +848,7 @@ virFirewallApplyCmd(virFirewall *firewall,
             return -1;
         break;
 
+    case VIR_FIREWALL_BACKEND_PF:
     case VIR_FIREWALL_BACKEND_LAST:
     default:
         virReportEnumRangeError(virFirewallBackend,
index 07391bea67b90c9090dcd65066bf2b1ef2e42246..bff7bf44875d638ad903ac671b2fe840b9616d63 100644 (file)
@@ -30,6 +30,7 @@
 #define IPTABLES "iptables"
 #define IP6TABLES "ip6tables"
 #define NFT "nft"
+#define PFCTL "pfctl"
 #define TC "tc"
 
 typedef struct _virFirewall virFirewall;
@@ -49,6 +50,7 @@ typedef enum {
     VIR_FIREWALL_BACKEND_NONE, /* Always fails */
     VIR_FIREWALL_BACKEND_IPTABLES,
     VIR_FIREWALL_BACKEND_NFTABLES,
+    VIR_FIREWALL_BACKEND_PF,
 
     VIR_FIREWALL_BACKEND_LAST,
 } virFirewallBackend;