On Sat, Apr 26, 2025 at 09:42:36AM +0200, Roman Bogorodskiy wrote:
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?
So IIUC a PF 'anchor' is essentially a top level namespace
under which all libvirt rules will live, so they are isolated
from other system rules ?
This sounds similar to how we deal with the new 'nft' firewall
on Linux, where we have to create top level tables to hold
all our rules
That has thrown up a tricky problem for us though around the
prioritization. A packet has to be accepted by *every* top
level table, for it to be allowed. So even if a libvirt rule
allows it, potentiallly other system rules can still block
it. This is basically unsolvable in the general case. We do
a special hack in the firewalld case to tell it to put the
bridge device into a separate libvirt "zone", which in
turn means none of the other firewalld rules will ever get
applied to those packets.
Back to PF though. You're saying we can't create anchors on
the fly - so that's saying the only way to create the anchors
is to edit the /etc/ config file, and those get applied on
system boot ?
We should probably document this in docs/drvnetwork.rst...
which I see does not actually exist :-( Probably good to
start a skeleton of a page and put the FreeBSD specific
notes there, and we can fll out Linux notes later
- 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.
That's not especially worse than what we do on Linux either
- IPv6 configurations are currently not supported
Probably worth reporting a fatal error if you see IPv6
config in the XML input, so attempts to use IPV6 are
not silently ignored.
- 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.
On Linux the defaults should work based on routing, so that's not
too bad as a default behaviour. The difference is that FreeBSD
would only work based on the single route, while the Linux behaviour
could use all available routes. Not the end of the world, since
most machines only have a single default route. The common exception
being people using VPN clients.
diff --git a/src/network/network_pf.c b/src/network/network_pf.c
new file mode 100644
index 0000000000..b12ef86089
--- /dev/null
+++ b/src/network/network_pf.c
@@ -0,0 +1,327 @@
+/*
+ * 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
+#if defined(__FreeBSD__)
Do you need these __FreeBSD__ conditions here ? Is 'pf' intended to be
used on anything other than FreeBSD ? If not, we should ensure that
it simply never built on any other platform at the meson level.
+# include <sys/sysctl.h>
+# include <net/if_dl.h>
+#endif
+#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)
+{
+#if defined(__FreeBSD__)
+ 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;
+ }
+#endif
+
+ 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
+ return -1;
Should report an error message here about lack of IPV6
+ } else if (def->forward.type == VIR_NETWORK_FORWARD_ROUTE) {
+ return pfAddRoutingFirewallRules(def, ipdef);
Should this check for IPv6 too ? The linux impl doesn't seem to
conditionalize based on IP family though.
+ }
+ 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/util/virfirewall.h b/src/util/virfirewall.h
index d42e60884b..030cb2c0a4 100644
--- a/src/util/virfirewall.h
+++ b/src/util/virfirewall.h
@@ -30,6 +30,7 @@
#define IPTABLES "iptables"
#define IP6TABLES "ip6tables"
#define NFT "nft"
+#define PFCTL "pfctl"
typedef struct _virFirewall virFirewall;
@@ -48,6 +49,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;
I'd suggest adding the new backend to virfirewall as a standalone patch
behand, so this patch focuses exclusively on the freebsd pf impl in the
network driver.
With regards,
Daniel
--
|:
https://berrange.com -o-
https://www.flickr.com/photos/dberrange :|
|:
https://libvirt.org -o-
https://fstop138.berrange.com :|
|:
https://entangle-photo.org -o-
https://www.instagram.com/dberrange :|