]> git.proxmox.com Git - pve-firewall.git/commitdiff
bump version to 5.0.4 master
authorThomas Lamprecht <t.lamprecht@proxmox.com>
Fri, 19 Apr 2024 18:04:25 +0000 (20:04 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Fri, 19 Apr 2024 18:04:25 +0000 (20:04 +0200)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
30 files changed:
.gitignore [new file with mode: 0644]
Makefile
debian/changelog
debian/compat [deleted file]
debian/control
debian/docs
debian/pvefw-logger.service
debian/rules
src/Makefile
src/PVE/API2/Firewall/Aliases.pm
src/PVE/API2/Firewall/Cluster.pm
src/PVE/API2/Firewall/Groups.pm
src/PVE/API2/Firewall/Host.pm
src/PVE/API2/Firewall/IPSet.pm
src/PVE/API2/Firewall/Makefile
src/PVE/API2/Firewall/Rules.pm
src/PVE/API2/Firewall/VM.pm
src/PVE/API2/Makefile
src/PVE/Firewall.pm
src/PVE/Firewall/Helpers.pm [new file with mode: 0644]
src/PVE/Firewall/Makefile [new file with mode: 0644]
src/PVE/FirewallSimulator.pm
src/PVE/Makefile
src/PVE/Service/Makefile
src/PVE/Service/pve_firewall.pm
src/pvefw-logger.c
test/README
test/fwtester.pl
test/test-basic1/100.fw
test/test-basic1/tests

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..1df91c5
--- /dev/null
@@ -0,0 +1,7 @@
+/*.build
+/*.buildinfo
+/*.changes
+/*.deb
+/*.dsc
+/*.tar*
+/pve-firewall-*/
index c98886d1de8d78524df55c4e70f0f49a5e57b92b..74be3787801f8291111e7a7770dae7784d3a78d4 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -3,12 +3,12 @@ include /usr/share/dpkg/architecture.mk
 
 PACKAGE=pve-firewall
 
-BUILDDIR ?= ${PACKAGE}-${DEB_VERSION_UPSTREAM}
+BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION)
 GITVERSION:=$(shell git rev-parse HEAD)
 
-DEB=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}_${DEB_BUILD_ARCH}.deb
-DSC=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}.dsc
-DEB2=${PACKAGE}-dbgsym_${DEB_VERSION_UPSTREAM_REVISION}_${DEB_BUILD_ARCH}.deb
+DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
+DSC=$(PACKAGE)_$(DEB_VERSION).dsc
+DEB2=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb
 DEBS=$(DEB) $(DEB2)
 
 all: $(DEBS)
@@ -17,23 +17,29 @@ all: $(DEBS)
 dinstall: deb
        dpkg -i $(DEBS)
 
-${BUILDDIR}:
-       rm -rf ${BUILDDIR}
-       rsync -a  src/ debian ${BUILDDIR}
-       echo "git clone git://git.proxmox.com/git/pve-firewall.git\\ngit checkout ${GITVERSION}" > ${BUILDDIR}/debian/SOURCE
+$(BUILDDIR):
+       rm -rf $(BUILDDIR)
+       rsync -a  src/ debian $(BUILDDIR)
+       echo "git clone git://git.proxmox.com/git/pve-firewall.git\\ngit checkout $(GITVERSION)" > $(BUILDDIR)/debian/SOURCE
 
 .PHONY: deb
 deb: $(DEBS)
 $(DEB2): $(DEB)
-$(DEB): ${BUILDDIR} check
-       cd ${BUILDDIR}; dpkg-buildpackage -b -us -uc
-       lintian ${DEBS}
+$(DEB): $(BUILDDIR) check
+       cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
+       lintian $(DEBS)
 
 .PHONY: dsc
-dsc: ${DSC}
-${DSC}: ${BUILDDIR}
-       cd ${BUILDDIR}; dpkg-buildpackage -S -us -uc -d
-       lintian ${DSC}
+dsc:
+       rm -rf $(DSC) $(BUILDDIR)
+       $(MAKE) $(DSC)
+       lintian $(DSC)
+
+$(DSC): $(BUILDDIR)
+       cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
+
+sbuild: $(DSC)
+       sbuild $(DSC)
 
 .PHONY: check
 check:
@@ -44,8 +50,9 @@ distclean: clean
 clean:
        make -C src clean
        make -C test clean
-       rm -rf *~ debian/*~ example/*~ *.deb *.changes *.buildinfo ${BUILDDIR} ${PACKAGE}*.tar.gz *.dsc
+       rm -rf *.deb *.dsc *.changes *.build *.buildinfo $(PACKAGE)-[0-9]*/ $(PACKAGE)*.tar*
 
 .PHONY: upload
+upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
 upload: $(DEBS)
-       tar cf - $(DEBS) | ssh repoman@repo.proxmox.com -- upload --product pve --dist buster --arch ${DEB_BUILD_ARCH}
+       tar cf - $(DEBS) | ssh repoman@repo.proxmox.com -- upload --product pve --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH)
index 24378efe357ed2ee52d5dccb2ec2f6a903820add..0128bc0c1e9d3bc942cf76ccb5e75036ae10e3c0 100644 (file)
@@ -1,3 +1,186 @@
+pve-firewall (5.0.4) bookworm; urgency=medium
+
+  * fix #5335: stable sorting in cluster.fw
+
+  * add configuration option for new nftables firewall tech-preview
+
+ -- Proxmox Support Team <support@proxmox.com>  Fri, 19 Apr 2024 20:04:09 +0200
+
+pve-firewall (5.0.3) bookworm; urgency=medium
+
+  * fix resolution of scoped aliases in ipsets
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 17 Jul 2023 10:39:28 +0200
+
+pve-firewall (5.0.2) bookworm; urgency=medium
+
+  * fix #4556: api: return scoped IPSets and aliases
+
+ -- Proxmox Support Team <support@proxmox.com>  Wed, 21 Jun 2023 19:17:19 +0200
+
+pve-firewall (5.0.1) bookworm; urgency=medium
+
+  * fix #4556: support 'dc/' and 'guest/' prefix for aliases and ipsets
+
+ -- Proxmox Support Team <support@proxmox.com>  Wed, 07 Jun 2023 16:06:10 +0200
+
+pve-firewall (5.0.0) bookworm; urgency=medium
+
+  * switch to native versioning scheme
+
+  * build for Proxmox VE 8 / Debian 12 Bookworm
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 22 May 2023 14:43:58 +0200
+
+pve-firewall (4.3-2) bullseye; urgency=medium
+
+  * fix variables declared in conditional statement
+
+  * fix #4730: add safeguards to prevent ICMP type misuse
+
+ -- Proxmox Support Team <support@proxmox.com>  Tue, 16 May 2023 11:17:58 +0200
+
+pve-firewall (4.3-1) bullseye; urgency=medium
+
+  * allow entering IP address with the host bits (those inside the mask) not
+    being all zero non-zero, like 192.168.1.155/24 for example.
+
+  * api: firewall logger: add optional parameters `since` and `until` for
+    time-range filtering
+
+  * fix #4550: host options: add nf_conntrack_helpers to compensate that
+    kernel 6.1 and newer have removed the auto helpers
+
+ -- Proxmox Support Team <support@proxmox.com>  Fri, 17 Mar 2023 15:24:56 +0100
+
+pve-firewall (4.2-7) bullseye; urgency=medium
+
+  * fix #4018: add firewall macro for SPICE proxy
+
+  * fix #4204: automatically update each usage of a group to the new ID when
+    it is renamed
+
+  * fix #4268: add 'force' parameter to delete IPSet with members
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 17 Nov 2022 19:53:04 +0100
+
+pve-firewall (4.2-6) bullseye; urgency=medium
+
+  * config defaults: document that the mac filter defaults to on
+
+  * fix #4175: ignore non-filter ebtables tables
+
+  * fix enabling ebtables if VM firewall config is invalid
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 29 Aug 2022 09:43:53 +0200
+
+pve-firewall (4.2-5) bullseye; urgency=medium
+
+  * fix #3677 ipset get chains: handle newer ipset output for actual
+    change detection
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 04 Nov 2021 16:37:13 +0100
+
+pve-firewall (4.2-4) bullseye; urgency=medium
+
+  * re-build to avoid issues stemming from semi-broken systemd-debhelper version
+
+ -- Proxmox Support Team <support@proxmox.com>  Tue, 12 Oct 2021 10:39:05 +0200
+
+pve-firewall (4.2-3) bullseye; urgency=medium
+
+  * fix #2721: remove the (nowadays) bogus reject for TCP port 43 from the
+    default drop and reject actions
+
+ -- Proxmox Support Team <support@proxmox.com>  Fri, 10 Sep 2021 13:00:07 +0200
+
+pve-firewall (4.2-2) bullseye; urgency=medium
+
+  * re-set relevant sysctls on every apply round
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 21 Jun 2021 11:31:42 +0200
+
+pve-firewall (4.2-1) bullseye; urgency=medium
+
+  * fix #967: source: dest: limit length
+
+  * re-build for Debian 11 Bullseye based releases (Proxmox VE 7)
+
+  * fix #2358: allow --<opt> in firewall rule config files
+
+ -- Proxmox Support Team <support@proxmox.com>  Wed, 12 May 2021 20:32:30 +0200
+
+pve-firewall (4.1-3) pve; urgency=medium
+
+  * fix #2773: ebtables: keep policy of custom chains
+
+  * introduce new icmp-type parameter
+
+ -- Proxmox Support Team <support@proxmox.com>  Fri, 18 Sep 2020 16:51:27 +0200
+
+pve-firewall (4.1-2) pve; urgency=medium
+
+  * revert: rules: verify referenced security group exists
+
+ -- Proxmox Support Team <support@proxmox.com>  Wed, 06 May 2020 17:41:36 +0200
+
+pve-firewall (4.1-1) pve; urgency=medium
+
+  * logging: add missing log message for inbound rules
+
+  * fix #2686: avoid adding 'arp-ip-src' IP filter if guests uses DHCP
+
+  * IPSets: parse the CIDR before checking for duplicates
+
+  * verify that a referenced security group exists
+
+  * ICMP: fix iptables-restore failing if ICMP-type values bigger than '255'
+
+  * ICMP: allow one to specify the 'echo-reply' (0) type also as integer
+
+  * improve handling concurrent (parallel) access and modifications to rules
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 04 May 2020 15:01:57 +0200
+
+pve-firewall (4.0-10) pve; urgency=medium
+
+  * macros: add macro for Proxmox Mail Gateway web interface
+
+  * api node: always pass cluster conf to node FW parser to fix false positive
+    error message about non existing aliases, or IP sets, when querying the
+    node FW options GET API call.
+
+  * grammar fix: s/does not exists/does not exist/g
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 27 Jan 2020 19:25:49 +0100
+
+pve-firewall (4.0-9) pve; urgency=medium
+
+  * ensure port range used for offline storage migration and insecure migration
+    traffic is allowed by default rule set.
+
+ -- Proxmox Support Team <support@proxmox.com>  Tue, 03 Dec 2019 08:12:20 +0100
+
+pve-firewall (4.0-8) pve; urgency=medium
+
+  * increase default nf_conntrack_max to the kernel's default
+
+  * fix some "use of uninitialized value" warnings when updating CIDRs
+
+  * update schema documentation
+
+  * add explicit dependency on libpve-cluster-perl
+
+  * add support for "raw" tables
+
+  * add options for synflood protection for host firewall:
+    - nf_conntrack_tcp_timeout_syn_recv
+    - protection_synflood: boolean
+    - protection_synflood_rate: SYN rate limit (default 200 per second)
+    - protection_synflood_burst: SYN burst limit (default 1000)
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 18 Nov 2019 13:48:20 +0100
+
 pve-firewall (4.0-7) pve; urgency=medium
 
   * only add VM chains and rules if VM firewall is enabled
diff --git a/debian/compat b/debian/compat
deleted file mode 100644 (file)
index f599e28..0000000
+++ /dev/null
@@ -1 +0,0 @@
-10
index b119159536ce100f50343ba05d18fc1a55708371..6edf0b33ca6b804aad88f5b329b2a110d30d760c 100644 (file)
@@ -2,15 +2,17 @@ Source: pve-firewall
 Section: admin
 Priority: optional
 Maintainer: Proxmox Support Team <support@proxmox.com>
-Build-Depends: debhelper (>= 10),
+Build-Depends: debhelper-compat (= 13),
+               libanyevent-perl,
                libglib2.0-dev,
                libnetfilter-conntrack-dev,
                libnetfilter-log-dev,
-               libpve-common-perl,
                libpve-access-control,
+               libpve-cluster-perl,
+               libpve-common-perl (>= 7.3-2),
                pve-cluster (>= 6.0-4),
                pve-doc-generator (>= 5.3-3),
-Standards-Version: 3.9.8
+Standards-Version: 4.6.2
 
 Package: pve-firewall
 Architecture: any
@@ -19,9 +21,9 @@ Depends: ebtables,
          ipset,
          iptables,
          libpve-access-control,
-         libpve-common-perl,
-         lsb-base,
-         pve-cluster (>= 6.0-4),
+         libpve-cluster-perl,
+         libpve-common-perl (>= 7.3-2),
+         pve-cluster (>= 6.1-6),
          ${misc:Depends},
          ${perl:Depends},
          ${shlibs:Depends},
index ef95fb080dc337ce932e07c8b2f53591b71ab840..a24793f929a0d59612df83b11819de60a53b9d96 100644 (file)
@@ -1,3 +1,3 @@
+debian/README
 debian/SOURCE
 debian/example/
-debian/README
index 05dd0dfefca218f72612451e33a57176dfba5d86..53d9056a92841f43b16cb3245d4199abcae440d1 100644 (file)
@@ -12,3 +12,5 @@ PIDFile=/run/pvefw-logger.pid
 TimeoutStopSec=5
 Type=forking
 
+[Install]
+WantedBy=multi-user.target
index 0b46d6291bb2a1b0840629d3fae26ef62588f482..9bd53bc37964b6cb91bd43de82555218b9217741 100755 (executable)
@@ -4,14 +4,10 @@
 #export DH_VERBOSE=1
 
 %:
-       dh $@ --with systemd
+       dh $@
 
-override_dh_systemd_enable:
-       dh_systemd_enable --name pvefw-logger pvefw-logger.service
-       dh_systemd_enable --name pve-firewall pve-firewall.service
-
-override_dh_systemd_start:
-       dh_systemd_start --restart-after-upgrade pvefw-logger.service
-       # pve-firewall start/try-reload-restart manually in postinst
+override_dh_installsystemd:
+       dh_installsystemd --name pvefw-logger pvefw-logger.service
+       dh_installsystemd --name pve-firewall --no-stop-on-upgrade --no-restart-after-upgrade pve-firewall.service
 
 override_dh_installinit:
index 7cea6b56e4fb69c13c801e4d9bed24a95b3efd5e..aaece5adbd336926a7ed9c1700de140782e66f0a 100644 (file)
@@ -1,16 +1,15 @@
 DESTDIR=
 PREFIX= /usr
-BINDIR=${DESTDIR}/${PREFIX}/bin
-SBINDIR=${DESTDIR}/${PREFIX}/sbin
-MANDIR=${DESTDIR}/${PREFIX}/share/man
-DOCDIR=${DESTDIR}/${PREFIX}/share/doc/pve-firewall
-MAN1DIR=${MANDIR}/man1/
-MAN8DIR=${MANDIR}/man8/
-BASHCOMPLDIR=${DESTDIR}/${PREFIX}/share/bash-completion/completions
-ZSHCOMPLDIR=${DESTDIR}/${PREFIX}/share/zsh/vendor-completions
-
-export NOVIEW=1
-include /usr/share/pve-doc-generator/pve-doc-generator.mk
+BINDIR=$(DESTDIR)/$(PREFIX)/bin
+SBINDIR=$(DESTDIR)/$(PREFIX)/sbin
+MANDIR=$(DESTDIR)/$(PREFIX)/share/man
+DOCDIR=$(DESTDIR)/$(PREFIX)/share/doc/pve-firewall
+MAN1DIR=$(MANDIR)/man1/
+MAN8DIR=$(MANDIR)/man8/
+BASHCOMPLDIR=$(DESTDIR)/$(PREFIX)/share/bash-completion/completions
+ZSHCOMPLDIR=$(DESTDIR)/$(PREFIX)/share/zsh/vendor-completions
+
+-include /usr/share/pve-doc-generator/pve-doc-generator.mk
 
 all: pve-firewall.8 pvefw-logger
 
@@ -22,34 +21,31 @@ pve-firewall.zsh-completion: PVE/Service/pve_firewall.pm
        perl -I. -T -e "use PVE::Service::pve_firewall; PVE::Service::pve_firewall->generate_zsh_completions();" >$@.tmp
        mv $@.tmp $@
 
-CPPFLAGS:=$(shell dpkg-buildflags --get CPPFLAGS)
 CFLAGS:=$(shell dpkg-buildflags --get CFLAGS)
+CFLAGS+=$(shell pkg-config libnetfilter_log libnetfilter_conntrack glib-2.0 --libs --cflags)
 LDFLAGS:=$(shell dpkg-buildflags --get LDFLAGS)
 
 pvefw-logger: pvefw-logger.c
-       gcc -Wall -Werror pvefw-logger.c -o pvefw-logger -std=gnu99 \
-       $(CPPFLAGS) $(CFLAGS) $(LDFLAGS) \
-       $(shell pkg-config libnetfilter_log libnetfilter_conntrack glib-2.0 --libs --cflags)
+       gcc -Wall -Werror pvefw-logger.c -o pvefw-logger -std=gnu99 $(CFLAGS) $(LDFLAGS)
 
 .PHONY: install
 install: pve-firewall pve-firewall.8 pve-firewall.bash-completion pve-firewall.zsh-completion pvefw-logger
        make -C PVE install
-       install -d -m 0755 ${SBINDIR}
-       install -m 0755 pve-firewall ${SBINDIR}
-       install -m 0755 pvefw-logger ${SBINDIR}
-       install -d ${MAN8DIR}
-       install -m 0644 pve-firewall.8 ${MAN8DIR}
-       install -m 0644 -D pve-firewall.bash-completion ${BASHCOMPLDIR}/pve-firewall
-       install -m 0644 -D pve-firewall.zsh-completion ${ZSHCOMPLDIR}/_pve-firewall
-       install -d -m 0755 ${DESTDIR}/usr/lib/sysctl.d/
-       install -m 0644 pve-firewall-sysctl.conf ${DESTDIR}/usr/lib/sysctl.d/pve-firewall.conf
+       install -d -m 0755 $(SBINDIR)
+       install -m 0755 pve-firewall $(SBINDIR)
+       install -m 0755 pvefw-logger $(SBINDIR)
+       install -d $(MAN8DIR)
+       install -m 0644 pve-firewall.8 $(MAN8DIR)
+       install -m 0644 -D pve-firewall.bash-completion $(BASHCOMPLDIR)/pve-firewall
+       install -m 0644 -D pve-firewall.zsh-completion $(ZSHCOMPLDIR)/_pve-firewall
+       install -d -m 0755 $(DESTDIR)/usr/lib/sysctl.d/
+       install -m 0644 pve-firewall-sysctl.conf $(DESTDIR)/usr/lib/sysctl.d/pve-firewall.conf
 
 .PHONY: clean
 clean:         
        make -C PVE clean
-       make cleanup-docgen
-       rm -rf pvefw-logger 
-       find . -name '*~' -exec rm {} ';'
+       rm -f *.xml.tmp *.1 *.5 *.8 *{synopsis,opts}.adoc docinfo.xml *~
+       rm -rf pvefw-logger
 
 
 .PHONY: distclean
index 6f421fbe271ff8c75e09fe5533f95b71bd94f0c3..33ac669d6381f76c6f5c8ce46156925bb22ca1d7 100644 (file)
@@ -9,7 +9,7 @@ use PVE::Firewall;
 
 use base qw(PVE::RESTHandler);
 
-my $api_properties = { 
+my $api_properties = {
     cidr => {
        description => "Network/IP specification in CIDR format.",
        type => 'string', format => 'IPorCIDR',
@@ -25,6 +25,12 @@ my $api_properties = {
     },
 };
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    die "implement this in subclass";
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -41,7 +47,7 @@ sub save_aliases {
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     die "implement this in subclass";
 }
 
@@ -97,7 +103,7 @@ sub register_get_aliases {
                        type => 'string',
                        optional => 1,
                    },
-                   digest => get_standard_option('pve-config-digest', { optional => 0} ),      
+                   digest => get_standard_option('pve-config-digest', { optional => 0} ),
                },
            },
            links => [ { rel => 'child', href => "{name}" } ],
@@ -137,19 +143,23 @@ sub register_create_alias {
        code => sub {
            my ($param) = @_;
 
-           my ($fw_conf, $aliases) = $class->load_config($param);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
 
-           my $name = lc($param->{name});
-           
-           raise_param_exc({ name => "alias '$param->{name}' already exists" }) 
-               if defined($aliases->{$name});
-           
-           my $data = { name => $param->{name}, cidr => $param->{cidr} };
-           $data->{comment} = $param->{comment} if $param->{comment};
+               my ($fw_conf, $aliases) = $class->load_config($param);
+
+               my $name = lc($param->{name});
+
+               raise_param_exc({ name => "alias '$param->{name}' already exists" })
+                   if defined($aliases->{$name});
 
-           $aliases->{$name} = $data;
+               my $data = { name => $param->{name}, cidr => $param->{cidr} };
+               $data->{comment} = $param->{comment} if $param->{comment};
 
-           $class->save_aliases($param, $fw_conf, $aliases);
+               $aliases->{$name} = $data;
+
+               $class->save_aliases($param, $fw_conf, $aliases);
+           });
 
            return undef;
        }});
@@ -161,7 +171,7 @@ sub register_read_alias {
     my $properties = $class->additional_parameters();
 
     $properties->{name} = $api_properties->{name};
-    
+
     $class->register_method({
        name => 'read_alias',
        path => '{name}',
@@ -213,34 +223,39 @@ sub register_update_alias {
        code => sub {
            my ($param) = @_;
 
-           my ($fw_conf, $aliases) = $class->load_config($param);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
 
-           my $list = &$aliases_to_list($aliases);
+               my ($fw_conf, $aliases) = $class->load_config($param);
 
-           my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list);
+               my $list = &$aliases_to_list($aliases);
 
-           PVE::Tools::assert_if_modified($digest, $param->{digest});
+               my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list);
 
-           my $name = lc($param->{name});
+               PVE::Tools::assert_if_modified($digest, $param->{digest});
 
-           raise_param_exc({ name => "no such alias" }) if !$aliases->{$name};
+               my $name = lc($param->{name});
 
-           my $data = { name => $param->{name}, cidr => $param->{cidr} };
-           $data->{comment} = $param->{comment} if $param->{comment};
+               raise_param_exc({ name => "no such alias" }) if !$aliases->{$name};
 
-           $aliases->{$name} = $data;
+               my $data = { name => $param->{name}, cidr => $param->{cidr} };
+               $data->{comment} = $param->{comment} if $param->{comment};
 
-           my $rename = lc($param->{rename});
+               $aliases->{$name} = $data;
 
-           if ($rename && ($name ne $rename)) {
-               raise_param_exc({ name => "alias '$param->{rename}' already exists" }) 
-                   if defined($aliases->{$rename});
-               $aliases->{$name}->{name} = $param->{rename};
-               $aliases->{$rename} = $aliases->{$name};
-               delete $aliases->{$name};
-           }
+               my $rename = $param->{rename};
+               $rename = lc($rename) if $rename;
 
-           $class->save_aliases($param, $fw_conf, $aliases);
+               if ($rename && ($name ne $rename)) {
+                   raise_param_exc({ name => "alias '$param->{rename}' already exists" })
+                       if defined($aliases->{$rename});
+                   $aliases->{$name}->{name} = $param->{rename};
+                   $aliases->{$rename} = $aliases->{$name};
+                   delete $aliases->{$name};
+               }
+
+               $class->save_aliases($param, $fw_conf, $aliases);
+           });
 
            return undef;
        }});
@@ -269,17 +284,21 @@ sub register_delete_alias {
        code => sub {
            my ($param) = @_;
 
-           my ($fw_conf, $aliases) = $class->load_config($param);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
 
-           my $list = &$aliases_to_list($aliases);
-           my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list);
-           PVE::Tools::assert_if_modified($digest, $param->{digest});
+               my ($fw_conf, $aliases) = $class->load_config($param);
 
-           my $name = lc($param->{name});
-           delete $aliases->{$name};
+               my $list = &$aliases_to_list($aliases);
+               my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list);
+               PVE::Tools::assert_if_modified($digest, $param->{digest});
+
+               my $name = lc($param->{name});
+               delete $aliases->{$name};
+
+               $class->save_aliases($param, $fw_conf, $aliases);
+           });
 
-           $class->save_aliases($param, $fw_conf, $aliases);
-           
            return undef;
        }});
 }
@@ -303,10 +322,16 @@ use base qw(PVE::API2::Firewall::AliasesBase);
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'cluster';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -335,15 +360,21 @@ use base qw(PVE::API2::Firewall::AliasesBase);
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'vm';
 }
 
-__PACKAGE__->additional_parameters({ 
+__PACKAGE__->additional_parameters({
     node => get_standard_option('pve-node'),
-    vmid => get_standard_option('pve-vmid'),                              
+    vmid => get_standard_option('pve-vmid'),
 });
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -373,15 +404,21 @@ use base qw(PVE::API2::Firewall::AliasesBase);
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'ct';
 }
 
-__PACKAGE__->additional_parameters({ 
+__PACKAGE__->additional_parameters({
     node => get_standard_option('pve-node'),
-    vmid => get_standard_option('pve-vmid'),                              
+    vmid => get_standard_option('pve-vmid'),
 });
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
index 0bf8285f6db86b423dac612dacde921ccade8caa..48ad90d40b2a6ebc8646678d9d0c917f84d08ecb 100644 (file)
@@ -132,29 +132,31 @@ __PACKAGE__->register_method({
     code => sub {
        my ($param) = @_;
 
-       my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+       PVE::Firewall::lock_clusterfw_conf(10, sub {
+           my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
 
-       my (undef, $digest) = PVE::Firewall::copy_opject_with_digest($cluster_conf->{options});
-       PVE::Tools::assert_if_modified($digest, $param->{digest});
+           my (undef, $digest) = PVE::Firewall::copy_opject_with_digest($cluster_conf->{options});
+           PVE::Tools::assert_if_modified($digest, $param->{digest});
 
-       if ($param->{delete}) {
-           foreach my $opt (PVE::Tools::split_list($param->{delete})) {
-               raise_param_exc({ delete => "no such option '$opt'" })
-                   if !$option_properties->{$opt};
-               delete $cluster_conf->{options}->{$opt};
+           if ($param->{delete}) {
+               foreach my $opt (PVE::Tools::split_list($param->{delete})) {
+                   raise_param_exc({ delete => "no such option '$opt'" })
+                       if !$option_properties->{$opt};
+                   delete $cluster_conf->{options}->{$opt};
+               }
            }
-       }
 
-       if (defined($param->{enable}) && ($param->{enable} > 1)) {
-           $param->{enable} = time();
-       }
+           if (defined($param->{enable}) && ($param->{enable} > 1)) {
+               $param->{enable} = time();
+           }
 
-       foreach my $k (keys %$option_properties) {
-           next if !defined($param->{$k});
-           $cluster_conf->{options}->{$k} = $param->{$k};
-       }
+           foreach my $k (keys %$option_properties) {
+               next if !defined($param->{$k});
+               $cluster_conf->{options}->{$k} = $param->{$k};
+           }
 
-       PVE::Firewall::save_clusterfw_conf($cluster_conf);
+           PVE::Firewall::save_clusterfw_conf($cluster_conf);
+       });
 
        # instant firewall update when using double (anti-lockout) API call
        # -> not waiting for a firewall update at the first (timestamp enable) set
@@ -238,6 +240,9 @@ __PACKAGE__->register_method({
                ref => {
                    type => 'string',
                },
+               scope => {
+                   type => 'string',
+               },
                comment => {
                    type => 'string',
                    optional => 1,
@@ -250,36 +255,7 @@ __PACKAGE__->register_method({
 
        my $conf = PVE::Firewall::load_clusterfw_conf();
 
-       my $res = [];
-
-       if (!$param->{type} || $param->{type} eq 'ipset') {
-           foreach my $name (keys %{$conf->{ipset}}) {
-               my $data = {
-                   type => 'ipset',
-                   name => $name,
-                   ref => "+$name",
-               };
-               if (my $comment = $conf->{ipset_comments}->{$name}) {
-                   $data->{comment} = $comment;
-               }
-               push @$res, $data;
-           }
-       }
-
-       if (!$param->{type} || $param->{type} eq 'alias') {
-           foreach my $name (keys %{$conf->{aliases}}) {
-               my $e = $conf->{aliases}->{$name};
-               my $data = {
-                   type => 'alias',
-                   name => $name,
-                   ref => $name,
-               };
-               $data->{comment} = $e->{comment} if $e->{comment};
-               push @$res, $data;
-           }
-       }
-
-       return $res;
+       return PVE::Firewall::Helpers::collect_refs($conf, $param->{type}, "dc");
     }});
 
 1;
index 8f94b6df72f58bc41bffb9da4e2ea9068cee22fb..ffdc45c1889e3d2af1906e412bca2601601c595f 100644 (file)
@@ -16,7 +16,7 @@ my $get_security_group_list = sub {
 
     my $res = [];
     foreach my $group (sort keys %{$cluster_conf->{groups}}) {
-       my $data = { 
+       my $data = {
            group => $group,
        };
        if (my $comment = $cluster_conf->{group_comments}->{$group}) {
@@ -30,6 +30,15 @@ my $get_security_group_list = sub {
     return wantarray ? ($list, $digest) : $list;
 };
 
+my $rename_fw_rules = sub {
+    my ($old, $new, $rules) = @_;
+
+    for my $rule (@{$rules}) {
+       next if ($rule->{type} ne "group" || $rule->{action} ne $old);
+       $rule->{action} = $new;
+    }
+};
+
 __PACKAGE__->register_method({
     name => 'list_security_groups',
     path => '',
@@ -44,10 +53,10 @@ __PACKAGE__->register_method({
        type => 'array',
        items => {
            type => "object",
-           properties => { 
+           properties => {
                group => get_standard_option('pve-security-group-name'),
                digest => get_standard_option('pve-config-digest', { optional => 0} ),
-               comment => { 
+               comment => {
                    type => 'string',
                    optional => 1,
                }
@@ -74,7 +83,7 @@ __PACKAGE__->register_method({
     },
     parameters => {
        additionalProperties => 0,
-       properties => { 
+       properties => {
            group => get_standard_option('pve-security-group-name'),
            comment => {
                type => 'string',
@@ -91,43 +100,100 @@ __PACKAGE__->register_method({
     code => sub {
        my ($param) = @_;
 
-       my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+       my $group = $param->{group};
+       my $rename = $param->{rename};
+       my $comment = $param->{comment};
 
-       if ($param->{rename}) {
-           my (undef, $digest) = &$get_security_group_list($cluster_conf);
-           PVE::Tools::assert_if_modified($digest, $param->{digest});
+       PVE::Firewall::lock_clusterfw_conf(10, sub {
+           my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
 
-           raise_param_exc({ group => "Security group '$param->{rename}' does not exists" }) 
-               if !$cluster_conf->{groups}->{$param->{rename}};
+           if ($rename) {
+               my (undef, $digest) = &$get_security_group_list($cluster_conf);
+               PVE::Tools::assert_if_modified($digest, $param->{digest});
 
-           # prevent overwriting an existing group
-           raise_param_exc({ group => "Security group '$param->{group}' does already exist" })
-               if $cluster_conf->{groups}->{$param->{group}} &&
-               $param->{group} ne $param->{rename};
+               raise_param_exc({ group => "Security group '$rename' does not exist" })
+                   if !$cluster_conf->{groups}->{$rename};
 
-           my $data = delete $cluster_conf->{groups}->{$param->{rename}};
-           $cluster_conf->{groups}->{$param->{group}} = $data;
-           if (my $comment = delete $cluster_conf->{group_comments}->{$param->{rename}}) {
-               $cluster_conf->{group_comments}->{$param->{group}} = $comment;
-           }
-           $cluster_conf->{group_comments}->{$param->{group}} = $param->{comment} if defined($param->{comment});
-       } else {
-           foreach my $name (keys %{$cluster_conf->{groups}}) {
-               raise_param_exc({ group => "Security group '$name' already exists" }) 
-                   if $name eq $param->{group};
+               # prevent overwriting an existing group
+               raise_param_exc({ group => "Security group '$group' does already exist" })
+                   if $cluster_conf->{groups}->{$group} &&
+                   $group ne $rename;
+
+               if ($rename eq $group) {
+                   $cluster_conf->{group_comments}->{$rename} = $comment if defined($comment);
+                   PVE::Firewall::save_clusterfw_conf($cluster_conf);
+                   return;
+               }
+
+               # Create an exact copy of the old security group
+               $cluster_conf->{groups}->{$group} = $cluster_conf->{groups}->{$rename};
+               $cluster_conf->{group_comments}->{$group} = $cluster_conf->{group_comments}->{$rename};
+
+               # Update comment if provided
+               $cluster_conf->{group_comments}->{$group} = $comment if defined($comment);
+
+               # Write the copy to the cluster config, so that if something fails inbetween, the new firewall
+               # rules won't be broken when the new name is referenced
+               PVE::Firewall::save_clusterfw_conf($cluster_conf);
+
+               # Update all the host configs to the new copy
+               my $hosts = PVE::Cluster::get_nodelist();
+               foreach my $host (@$hosts) {
+                   PVE::Firewall::lock_hostfw_conf($host, 10, sub {
+                       my $host_conf_path = "/etc/pve/nodes/$host/host.fw";
+                       my $host_conf = PVE::Firewall::load_hostfw_conf($cluster_conf, $host_conf_path);
+
+                       if (defined($host_conf)) {
+                           &$rename_fw_rules($rename,
+                               $group,
+                               $host_conf->{rules});
+                           PVE::Firewall::save_hostfw_conf($host_conf, $host_conf_path);
+                       }
+                   });
+               }
+
+               # Update all the VM configs
+               my $vms = PVE::Cluster::get_vmlist();
+               foreach my $vm (keys %{$vms->{ids}}) {
+                   PVE::Firewall::lock_vmfw_conf($vm, 10, sub {
+                       my $vm_type = $vms->{ids}->{$vm}->{type} eq "lxc" ? "ct" : "vm";
+                       my $vm_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, $vm_type, $vm, "/etc/pve/firewall");
+
+                       if (defined($vm_conf)) {
+                           &$rename_fw_rules($rename,
+                               $group,
+                               $vm_conf->{rules});
+                           PVE::Firewall::save_vmfw_conf($vm, $vm_conf);
+                       }
+                   });
+               }
+
+               # And also update the cluster itself
+               &$rename_fw_rules($rename,
+                   $group,
+                   $cluster_conf->{rules});
+
+               # Now that everything has been updated, the old rule can be deleted
+               delete $cluster_conf->{groups}->{$rename};
+               delete $cluster_conf->{group_comments}->{$rename};
+           } else {
+               foreach my $name (keys %{$cluster_conf->{groups}}) {
+                   raise_param_exc({ group => "Security group '$name' already exists" })
+                       if $name eq $group;
+               }
+
+               $cluster_conf->{groups}->{$group} = [];
+               $cluster_conf->{group_comments}->{$group} = $comment if defined($comment);
            }
 
-           $cluster_conf->{groups}->{$param->{group}} = [];
-           $cluster_conf->{group_comments}->{$param->{group}} = $param->{comment} if defined($param->{comment});
-       }
+           PVE::Firewall::save_clusterfw_conf($cluster_conf);
+       });
 
-       PVE::Firewall::save_clusterfw_conf($cluster_conf);
-       
        return undef;
     }});
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::GroupRules",  
+    subclass => "PVE::API2::Firewall::GroupRules",
     path => '{group}',
 });
 
index 764dab14e348a942b9c13b544139e1bda7cfe5d7..0432de2224b7713ef787ff52ac996beebbaa2837 100644 (file)
@@ -14,7 +14,7 @@ use PVE::API2::Firewall::Rules;
 use base qw(PVE::RESTHandler);
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::HostRules",  
+    subclass => "PVE::API2::Firewall::HostRules",
     path => 'rules',
 });
 
@@ -58,7 +58,7 @@ my $add_option_properties = sub {
     foreach my $k (keys %$option_properties) {
        $properties->{$k} = $option_properties->{$k};
     }
-    
+
     return $properties;
 };
 
@@ -86,7 +86,8 @@ __PACKAGE__->register_method({
     code => sub {
        my ($param) = @_;
 
-       my $hostfw_conf = PVE::Firewall::load_hostfw_conf();
+       my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+       my $hostfw_conf = PVE::Firewall::load_hostfw_conf($cluster_conf);
 
        return PVE::Firewall::copy_opject_with_digest($hostfw_conf->{options});
     }});
@@ -117,36 +118,39 @@ __PACKAGE__->register_method({
     code => sub {
        my ($param) = @_;
 
-       my $hostfw_conf = PVE::Firewall::load_hostfw_conf();
+       PVE::Firewall::lock_hostfw_conf(undef, 10, sub {
+           my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+           my $hostfw_conf = PVE::Firewall::load_hostfw_conf($cluster_conf);
 
-       my (undef, $digest) = PVE::Firewall::copy_opject_with_digest($hostfw_conf->{options});
-       PVE::Tools::assert_if_modified($digest, $param->{digest});
+           my (undef, $digest) = PVE::Firewall::copy_opject_with_digest($hostfw_conf->{options});
+           PVE::Tools::assert_if_modified($digest, $param->{digest});
 
-       if ($param->{delete}) {
-           foreach my $opt (PVE::Tools::split_list($param->{delete})) {
-               raise_param_exc({ delete => "no such option '$opt'" }) 
-                   if !$option_properties->{$opt};
-               delete $hostfw_conf->{options}->{$opt};
+           if ($param->{delete}) {
+               foreach my $opt (PVE::Tools::split_list($param->{delete})) {
+                   raise_param_exc({ delete => "no such option '$opt'" })
+                       if !$option_properties->{$opt};
+                   delete $hostfw_conf->{options}->{$opt};
+               }
            }
-       }
 
-       if (defined($param->{enable})) {
-           $param->{enable} = $param->{enable} ? 1 : 0;
-       }
+           if (defined($param->{enable})) {
+               $param->{enable} = $param->{enable} ? 1 : 0;
+           }
 
-       foreach my $k (keys %$option_properties) {
-           next if !defined($param->{$k});
-           $hostfw_conf->{options}->{$k} = $param->{$k}; 
-       }
+           foreach my $k (keys %$option_properties) {
+               next if !defined($param->{$k});
+               $hostfw_conf->{options}->{$k} = $param->{$k};
+           }
 
-       PVE::Firewall::save_hostfw_conf($hostfw_conf);
+           PVE::Firewall::save_hostfw_conf($hostfw_conf);
+       });
 
        return undef;
     }});
 
 __PACKAGE__->register_method({
-    name => 'log', 
-    path => 'log', 
+    name => 'log',
+    path => 'log',
     method => 'GET',
     description => "Read firewall log",
     proxyto => 'node',
@@ -168,11 +172,23 @@ __PACKAGE__->register_method({
                minimum => 0,
                optional => 1,
            },
+           since => {
+               type => 'integer',
+               minimum => 0,
+               description => "Display log since this UNIX epoch.",
+               optional => 1,
+           },
+           until => {
+               type => 'integer',
+               minimum => 0,
+               description => "Display log until this UNIX epoch.",
+               optional => 1,
+           },
        },
     },
     returns => {
        type => 'array',
-       items => { 
+       items => {
            type => "object",
            properties => {
                n => {
@@ -192,12 +208,14 @@ __PACKAGE__->register_method({
        my $rpcenv = PVE::RPCEnvironment::get();
        my $user = $rpcenv->get_user();
        my $node = $param->{node};
+       my $filename = "/var/log/pve-firewall.log";
 
-       my ($count, $lines) = PVE::Tools::dump_logfile("/var/log/pve-firewall.log", $param->{start}, $param->{limit});
+       my ($count, $lines) = PVE::Firewall::Helpers::dump_fw_logfile(
+           $filename, $param, undef);
 
        $rpcenv->set_result_attrib('total', $count);
-           
-       return $lines; 
+
+       return $lines;
     }});
 
 1;
index 068c1a41ca1821bb11a7f71cab0b8f4718d5bacd..ed92d877ee0685726f69fc5f807c3a316a925dbe 100644 (file)
@@ -9,7 +9,7 @@ use PVE::Firewall;
 
 use base qw(PVE::RESTHandler);
 
-my $api_properties = { 
+my $api_properties = {
     cidr => {
        description => "Network/IP specification in CIDR format.",
        type => 'string', format => 'IPorCIDRorAlias',
@@ -25,6 +25,12 @@ my $api_properties = {
     },
 };
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    die "implement this in subclass";
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -41,7 +47,7 @@ sub save_config {
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     die "implement this in subclass";
 }
 
@@ -106,7 +112,7 @@ sub register_get_ipset {
                        type => 'boolean',
                        optional => 1,
                    },
-                   digest => get_standard_option('pve-config-digest', { optional => 0} ),      
+                   digest => get_standard_option('pve-config-digest', { optional => 0} ),
                },
            },
            links => [ { rel => 'child', href => "{cidr}" } ],
@@ -126,6 +132,11 @@ sub register_delete_ipset {
     my $properties = $class->additional_parameters();
 
     $properties->{name} = get_standard_option('ipset-name');
+    $properties->{force} = {
+       type => 'boolean',
+       optional => 1,
+       description => 'Delete all members of the IPSet, if there are any.',
+    };
 
     $class->register_method({
        name => 'delete_ipset',
@@ -141,13 +152,18 @@ sub register_delete_ipset {
        returns => { type => 'null' },
        code => sub {
            my ($param) = @_;
-           
-           my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
 
-           die "IPSet '$param->{name}' is not empty\n" 
-               if scalar(@$ipset);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
+
+               my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
 
-           $class->save_ipset($param, $fw_conf, undef);
+               die "IPSet '$param->{name}' is not empty\n"
+                   if scalar(@$ipset) && !$param->{force};
+
+               $class->save_ipset($param, $fw_conf, undef);
+
+           });
 
            return undef;
        }});
@@ -178,30 +194,42 @@ sub register_create_ip {
        code => sub {
            my ($param) = @_;
 
-           my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
+
+               my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
+
+               my $cidr = $param->{cidr};
+               if ($cidr =~ m@^(dc/|guest/)?(${PVE::Firewall::ip_alias_pattern})$@) {
+                   my $scope = $1 // "";
+                   my $alias = $2;
+                   # make sure alias exists (if $cidr is an alias)
+                   PVE::Firewall::resolve_alias($cluster_conf, $fw_conf, $alias, $scope);
+               } else {
+                   $cidr = PVE::Firewall::clean_cidr($cidr);
+                   # normalize like config parser, otherwise duplicates might slip through
+                   $cidr = PVE::Firewall::parse_ip_or_cidr($cidr);
+               }
 
-           my $cidr = $param->{cidr};
-           
-           foreach my $entry (@$ipset) {
-               raise_param_exc({ cidr => "address '$cidr' already exists" }) 
-                   if $entry->{cidr} eq $cidr;
-           }
+               foreach my $entry (@$ipset) {
+                   raise_param_exc({ cidr => "address '$cidr' already exists" })
+                       if $entry->{cidr} eq $cidr;
+               }
+
+               raise_param_exc({ cidr => "a zero prefix is not allowed in ipset entries" })
+                   if $cidr =~ m!/0+$!;
 
-           raise_param_exc({ cidr => "a zero prefix is not allowed in ipset entries" })
-               if $cidr =~ m!/0+$!;
 
-           # make sure alias exists (if $cidr is an alias)
-           PVE::Firewall::resolve_alias($cluster_conf, $fw_conf, $cidr)
-               if $cidr =~ m/^${PVE::Firewall::ip_alias_pattern}$/;
+               my $data = { cidr => $cidr };
 
-           my $data = { cidr => $cidr };
+               $data->{nomatch} = 1 if $param->{nomatch};
+               $data->{comment} = $param->{comment} if $param->{comment};
 
-           $data->{nomatch} = 1 if $param->{nomatch};
-           $data->{comment} = $param->{comment} if $param->{comment};
+               unshift @$ipset, $data;
 
-           unshift @$ipset, $data;
+               $class->save_ipset($param, $fw_conf, $ipset);
 
-           $class->save_ipset($param, $fw_conf, $ipset);
+           });
 
            return undef;
        }});
@@ -214,7 +242,7 @@ sub register_read_ip {
 
     $properties->{name} = $api_properties->{name};
     $properties->{cidr} = $api_properties->{cidr};
-    
+
     $class->register_method({
        name => 'read_ip',
        path => '{cidr}',
@@ -270,19 +298,27 @@ sub register_update_ip {
        code => sub {
            my ($param) = @_;
 
-           my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
+           my $found = $class->lock_config($param, sub {
+               my ($param) = @_;
 
-           my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
-           PVE::Tools::assert_if_modified($digest, $param->{digest});
+               my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
 
-           foreach my $entry (@$ipset) {
-               if($entry->{cidr} eq $param->{cidr}) {
-                   $entry->{nomatch} = $param->{nomatch};
-                   $entry->{comment} = $param->{comment};
-                   $class->save_ipset($param, $fw_conf, $ipset);
-                   return;
+               my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
+               PVE::Tools::assert_if_modified($digest, $param->{digest});
+
+               foreach my $entry (@$ipset) {
+                   if($entry->{cidr} eq $param->{cidr}) {
+                       $entry->{nomatch} = $param->{nomatch};
+                       $entry->{comment} = $param->{comment};
+                       $class->save_ipset($param, $fw_conf, $ipset);
+                       return 1;
+                   }
                }
-           }
+
+               return 0;
+           });
+
+           return if $found;
 
            raise_param_exc({ cidr => "no such IP/Network" });
        }});
@@ -312,19 +348,23 @@ sub register_delete_ip {
        code => sub {
            my ($param) = @_;
 
-           my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
 
-           my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
-           PVE::Tools::assert_if_modified($digest, $param->{digest});
+               my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
 
-           my $new = [];
-   
-           foreach my $entry (@$ipset) {
-               push @$new, $entry if $entry->{cidr} ne $param->{cidr};
-           }
+               my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
+               PVE::Tools::assert_if_modified($digest, $param->{digest});
+
+               my $new = [];
+
+               foreach my $entry (@$ipset) {
+                   push @$new, $entry if $entry->{cidr} ne $param->{cidr};
+               }
+
+               $class->save_ipset($param, $fw_conf, $new);
+           });
 
-           $class->save_ipset($param, $fw_conf, $new);
-           
            return undef;
        }});
 }
@@ -349,10 +389,16 @@ use base qw(PVE::API2::Firewall::IPSetBase);
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'cluster';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -381,15 +427,21 @@ use base qw(PVE::API2::Firewall::IPSetBase);
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'vm';
 }
 
-__PACKAGE__->additional_parameters({ 
+__PACKAGE__->additional_parameters({
     node => get_standard_option('pve-node'),
-    vmid => get_standard_option('pve-vmid'),                              
+    vmid => get_standard_option('pve-vmid'),
 });
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -419,15 +471,21 @@ use base qw(PVE::API2::Firewall::IPSetBase);
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'ct';
 }
 
-__PACKAGE__->additional_parameters({ 
+__PACKAGE__->additional_parameters({
     node => get_standard_option('pve-node'),
-    vmid => get_standard_option('pve-vmid'),                              
+    vmid => get_standard_option('pve-vmid'),
 });
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -457,9 +515,15 @@ use PVE::Firewall;
 
 use base qw(PVE::RESTHandler);
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    die "implement this in subclass";
+}
+
 sub load_config {
     my ($class, $param) = @_;
+
     die "implement this in subclass";
 
     #return ($cluster_conf, $fw_conf);
@@ -473,7 +537,7 @@ sub save_config {
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     die "implement this in subclass";
 }
 
@@ -498,7 +562,7 @@ my $get_ipset_list = sub {
 
     my $res = [];
     foreach my $name (sort keys %{$fw_conf->{ipset}}) {
-       my $data = { 
+       my $data = {
            name => $name,
        };
        if (my $comment = $fw_conf->{ipset_comments}->{$name}) {
@@ -531,10 +595,10 @@ sub register_index {
            type => 'array',
            items => {
                type => "object",
-               properties => { 
+               properties => {
                    name => get_standard_option('ipset-name'),
                    digest => get_standard_option('pve-config-digest', { optional => 0} ),
-                   comment => { 
+                   comment => {
                        type => 'string',
                        optional => 1,
                    }
@@ -544,10 +608,10 @@ sub register_index {
        },
        code => sub {
            my ($param) = @_;
-           
+
            my ($cluster_conf, $fw_conf) = $class->load_config($param);
 
-           return &$get_ipset_list($fw_conf); 
+           return &$get_ipset_list($fw_conf);
        }});
 }
 
@@ -580,38 +644,42 @@ sub register_create {
        returns => { type => 'null' },
        code => sub {
            my ($param) = @_;
-           
-           my ($cluster_conf, $fw_conf) = $class->load_config($param);
 
-           if ($param->{rename}) {
-               my (undef, $digest) = &$get_ipset_list($fw_conf);
-               PVE::Tools::assert_if_modified($digest, $param->{digest});
+           $class->lock_config($param, sub {
+               my ($param) = @_;
 
-               raise_param_exc({ name => "IPSet '$param->{rename}' does not exists" }) 
-                   if !$fw_conf->{ipset}->{$param->{rename}};
+               my ($cluster_conf, $fw_conf) = $class->load_config($param);
 
-               # prevent overwriting existing ipset
-               raise_param_exc({ name => "IPSet '$param->{name}' does already exist"})
-                   if $fw_conf->{ipset}->{$param->{name}} &&
-                   $param->{name} ne $param->{rename};
+               if ($param->{rename}) {
+                   my (undef, $digest) = &$get_ipset_list($fw_conf);
+                   PVE::Tools::assert_if_modified($digest, $param->{digest});
 
-               my $data = delete $fw_conf->{ipset}->{$param->{rename}};
-               $fw_conf->{ipset}->{$param->{name}} = $data;
-               if (my $comment = delete $fw_conf->{ipset_comments}->{$param->{rename}}) {
-                   $fw_conf->{ipset_comments}->{$param->{name}} = $comment;
-               }
-               $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
-           } else { 
-               foreach my $name (keys %{$fw_conf->{ipset}}) {
-                   raise_param_exc({ name => "IPSet '$name' already exists" }) 
-                       if $name eq $param->{name};
-               }
+                   raise_param_exc({ name => "IPSet '$param->{rename}' does not exist" })
+                       if !$fw_conf->{ipset}->{$param->{rename}};
 
-               $fw_conf->{ipset}->{$param->{name}} = [];
-               $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
-           }
+                   # prevent overwriting existing ipset
+                   raise_param_exc({ name => "IPSet '$param->{name}' does already exist"})
+                       if $fw_conf->{ipset}->{$param->{name}} &&
+                       $param->{name} ne $param->{rename};
 
-           $class->save_config($param, $fw_conf);
+                   my $data = delete $fw_conf->{ipset}->{$param->{rename}};
+                   $fw_conf->{ipset}->{$param->{name}} = $data;
+                   if (my $comment = delete $fw_conf->{ipset_comments}->{$param->{rename}}) {
+                       $fw_conf->{ipset_comments}->{$param->{name}} = $comment;
+                   }
+                   $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
+               } else {
+                   foreach my $name (keys %{$fw_conf->{ipset}}) {
+                       raise_param_exc({ name => "IPSet '$name' already exists" })
+                           if $name eq $param->{name};
+                   }
+
+                   $fw_conf->{ipset}->{$param->{name}} = [];
+                   $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
+               }
+
+               $class->save_config($param, $fw_conf);
+           });
 
            return undef;
        }});
@@ -634,13 +702,19 @@ use base qw(PVE::API2::Firewall::BaseIPSetList);
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'cluster';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
+
     my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
     return (undef, $cluster_conf);
 }
@@ -654,10 +728,10 @@ sub save_config {
 __PACKAGE__->register_handlers();
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::ClusterIPset",  
+    subclass => "PVE::API2::Firewall::ClusterIPset",
     path => '{name}',
-    # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' 
-    fragmentDelimiter => '', 
+    # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
+    fragmentDelimiter => '',
 });
 
 package PVE::API2::Firewall::VMIPSetList;
@@ -669,20 +743,26 @@ use PVE::Firewall;
 
 use base qw(PVE::API2::Firewall::BaseIPSetList);
 
-__PACKAGE__->additional_parameters({ 
+__PACKAGE__->additional_parameters({
     node => get_standard_option('pve-node'),
-    vmid => get_standard_option('pve-vmid'),                              
+    vmid => get_standard_option('pve-vmid'),
 });
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'vm';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
+
     my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
     my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid});
     return ($cluster_conf, $fw_conf);
@@ -697,10 +777,10 @@ sub save_config {
 __PACKAGE__->register_handlers();
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::VMIPset",  
+    subclass => "PVE::API2::Firewall::VMIPset",
     path => '{name}',
-    # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' 
-    fragmentDelimiter => '', 
+    # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
+    fragmentDelimiter => '',
 });
 
 package PVE::API2::Firewall::CTIPSetList;
@@ -712,20 +792,26 @@ use PVE::Firewall;
 
 use base qw(PVE::API2::Firewall::BaseIPSetList);
 
-__PACKAGE__->additional_parameters({ 
+__PACKAGE__->additional_parameters({
     node => get_standard_option('pve-node'),
-    vmid => get_standard_option('pve-vmid'),                              
+    vmid => get_standard_option('pve-vmid'),
 });
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'ct';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
+
     my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
     my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid});
     return ($cluster_conf, $fw_conf);
@@ -740,10 +826,10 @@ sub save_config {
 __PACKAGE__->register_handlers();
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::CTIPset",  
+    subclass => "PVE::API2::Firewall::CTIPset",
     path => '{name}',
-    # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' 
-    fragmentDelimiter => '', 
+    # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
+    fragmentDelimiter => '',
 });
 
 1;
index 7b2c3452e02cfecb62f5d4602446cef85f8fe0c9..e916755adb6af6e28545ceaadbfb3328f61f1280 100644 (file)
@@ -1,6 +1,6 @@
 DESTDIR=
 PREFIX=/usr
-PERLDIR=${DESTDIR}/${PREFIX}/share/perl5
+PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5
 
 LIB_SOURCES=                   \
        Aliases.pm              \
@@ -15,8 +15,8 @@ all:
 
 .PHONY: install
 install:
-       install -d -m 0755 ${PERLDIR}/PVE/API2/Firewall
-       for i in ${LIB_SOURCES}; do install -D -m 0644 $$i ${PERLDIR}/PVE/API2/Firewall/$$i; done       
+       install -d -m 0755 $(PERLDIR)/PVE/API2/Firewall
+       for i in $(LIB_SOURCES); do install -D -m 0644 $$i $(PERLDIR)/PVE/API2/Firewall/$$i; done       
 
 
 .PHONY: clean
index f0bc562a9037cf7057c919355659b1f7c55cecd1..9fcfb20ba9a88f963fe9b3bb1dd4929d3935aaf7 100644 (file)
@@ -2,6 +2,7 @@ package PVE::API2::Firewall::RulesBase;
 
 use strict;
 use warnings;
+
 use PVE::JSONSchema qw(get_standard_option);
 use PVE::Exception qw(raise raise_param_exc);
 
@@ -9,7 +10,7 @@ use PVE::Firewall;
 
 use base qw(PVE::RESTHandler);
 
-my $api_properties = { 
+my $api_properties = {
     pos => {
        description => "Rule position.",
        type => 'integer',
@@ -17,6 +18,12 @@ my $api_properties = {
     },
 };
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    die "implement this in subclass";
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -35,7 +42,7 @@ my $additional_param_hash = {};
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     die "implement this in subclass";
 }
 
@@ -105,7 +112,7 @@ sub register_get_rule {
     my $properties = $class->additional_parameters();
 
     $properties->{pos} = $api_properties->{pos};
-    
+
     my $rule_env = $class->rule_env();
 
     $class->register_method({
@@ -144,6 +151,10 @@ sub register_get_rule {
                log => PVE::Firewall::get_standard_option('pve-fw-loglevel', {
                    description => 'Log level for firewall rule',
                }),
+               'icmp-type' => {
+                   type => 'string',
+                   optional => 1,
+               },
                iface => {
                    type => 'string',
                    optional => 1,
@@ -182,9 +193,9 @@ sub register_get_rule {
            my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param);
 
            my ($list, $digest) = PVE::Firewall::copy_list_with_digest($rules);
-       
+
            die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$list);
-       
+
            my $rule = $list->[$param->{pos}];
            $rule->{pos} = $param->{pos};
 
@@ -200,7 +211,7 @@ sub register_create_rule {
     my $create_rule_properties = PVE::Firewall::add_rule_properties($properties);
     $create_rule_properties->{action}->{optional} = 0;
     $create_rule_properties->{type}->{optional} = 0;
-    
+
     my $rule_env = $class->rule_env();
 
     $class->register_method({
@@ -219,18 +230,22 @@ sub register_create_rule {
        code => sub {
            my ($param) = @_;
 
-           my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
 
-           my $rule = {};
+               my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param);
 
-           PVE::Firewall::copy_rule_data($rule, $param);
-           PVE::Firewall::verify_rule($rule, $cluster_conf, $fw_conf, $class->rule_env());
+               my $rule = {};
 
-           $rule->{enable} = 0 if !defined($param->{enable});
+               PVE::Firewall::copy_rule_data($rule, $param);
+               PVE::Firewall::verify_rule($rule, $cluster_conf, $fw_conf, $class->rule_env());
 
-           unshift @$rules, $rule;
+               $rule->{enable} = 0 if !defined($param->{enable});
 
-           $class->save_rules($param, $fw_conf, $rules);
+               unshift @$rules, $rule;
+
+               $class->save_rules($param, $fw_conf, $rules);
+           });
 
            return undef;
        }});
@@ -242,7 +257,7 @@ sub register_update_rule {
     my $properties = $class->additional_parameters();
 
     $properties->{pos} = $api_properties->{pos};
-    
+
     my $rule_env = $class->rule_env();
 
     $properties->{moveto} = {
@@ -276,36 +291,40 @@ sub register_update_rule {
        code => sub {
            my ($param) = @_;
 
-           my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
+
+               my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param);
+
+               my (undef, $digest) = PVE::Firewall::copy_list_with_digest($rules);
+               PVE::Tools::assert_if_modified($digest, $param->{digest});
 
-           my (undef, $digest) = PVE::Firewall::copy_list_with_digest($rules);
-           PVE::Tools::assert_if_modified($digest, $param->{digest});
+               die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$rules);
 
-           die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$rules);
-       
-           my $rule = $rules->[$param->{pos}];
+               my $rule = $rules->[$param->{pos}];
 
-           my $moveto = $param->{moveto};
-           if (defined($moveto) && $moveto != $param->{pos}) {
-               my $newrules = [];
-               for (my $i = 0; $i < scalar(@$rules); $i++) {
-                   next if $i == $param->{pos};
-                   if ($i == $moveto) {
-                       push @$newrules, $rule;
+               my $moveto = $param->{moveto};
+               if (defined($moveto) && $moveto != $param->{pos}) {
+                   my $newrules = [];
+                   for (my $i = 0; $i < scalar(@$rules); $i++) {
+                       next if $i == $param->{pos};
+                       if ($i == $moveto) {
+                           push @$newrules, $rule;
+                       }
+                       push @$newrules, $rules->[$i];
                    }
-                   push @$newrules, $rules->[$i];
-               }
-               push @$newrules, $rule if $moveto >= scalar(@$rules);
-               $rules = $newrules;
-           } else {
-               PVE::Firewall::copy_rule_data($rule, $param);
-               
-               PVE::Firewall::delete_rule_properties($rule, $param->{'delete'}) if $param->{'delete'};
+                   push @$newrules, $rule if $moveto >= scalar(@$rules);
+                   $rules = $newrules;
+               } else {
+                   PVE::Firewall::copy_rule_data($rule, $param);
 
-               PVE::Firewall::verify_rule($rule, $cluster_conf, $fw_conf, $class->rule_env());
-           }
+                   PVE::Firewall::delete_rule_properties($rule, $param->{'delete'}) if $param->{'delete'};
 
-           $class->save_rules($param, $fw_conf, $rules);
+                   PVE::Firewall::verify_rule($rule, $cluster_conf, $fw_conf, $class->rule_env());
+               }
+
+               $class->save_rules($param, $fw_conf, $rules);
+           });
 
            return undef;
        }});
@@ -319,7 +338,7 @@ sub register_delete_rule {
     $properties->{pos} = $api_properties->{pos};
 
     $properties->{digest} = get_standard_option('pve-config-digest');
-    
+
     my $rule_env = $class->rule_env();
 
     $class->register_method({
@@ -338,16 +357,20 @@ sub register_delete_rule {
        code => sub {
            my ($param) = @_;
 
-           my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param);
+           $class->lock_config($param, sub {
+               my ($param) = @_;
+
+               my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param);
+
+               my (undef, $digest) = PVE::Firewall::copy_list_with_digest($rules);
+               PVE::Tools::assert_if_modified($digest, $param->{digest});
+
+               die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$rules);
 
-           my (undef, $digest) = PVE::Firewall::copy_list_with_digest($rules);
-           PVE::Tools::assert_if_modified($digest, $param->{digest});
-       
-           die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$rules);
-       
-           splice(@$rules, $param->{pos}, 1);
-           
-           $class->save_rules($param, $fw_conf, $rules);
+               splice(@$rules, $param->{pos}, 1);
+
+               $class->save_rules($param, $fw_conf, $rules);
+           });
 
            return undef;
        }});
@@ -376,10 +399,16 @@ __PACKAGE__->additional_parameters({ group => get_standard_option('pve-security-
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'group';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -413,20 +442,24 @@ __PACKAGE__->register_method({
     },
     parameters => {
        additionalProperties => 0,
-       properties => { 
+       properties => {
            group => get_standard_option('pve-security-group-name'),
        },
     },
     returns => { type => 'null' },
     code => sub {
        my ($param) = @_;
-           
-       my (undef, $cluster_conf, $rules) = __PACKAGE__->load_config($param);
 
-       die "Security group '$param->{group}' is not empty\n" 
-           if scalar(@$rules);
+       __PACKAGE__->lock_config($param, sub {
+           my ($param) = @_;
+
+           my (undef, $cluster_conf, $rules) = __PACKAGE__->load_config($param);
 
-       __PACKAGE__->save_rules($param, $cluster_conf, undef);
+           die "Security group '$param->{group}' is not empty\n"
+               if scalar(@$rules);
+
+           __PACKAGE__->save_rules($param, $cluster_conf, undef);
+       });
 
        return undef;
     }});
@@ -442,10 +475,16 @@ use base qw(PVE::API2::Firewall::RulesBase);
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'cluster';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -476,10 +515,16 @@ __PACKAGE__->additional_parameters({ node => get_standard_option('pve-node')});
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'host';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_hostfw_conf(undef, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -507,17 +552,23 @@ use PVE::JSONSchema qw(get_standard_option);
 
 use base qw(PVE::API2::Firewall::RulesBase);
 
-__PACKAGE__->additional_parameters({ 
+__PACKAGE__->additional_parameters({
     node => get_standard_option('pve-node'),
-    vmid => get_standard_option('pve-vmid'),                              
+    vmid => get_standard_option('pve-vmid'),
 });
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'vm';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
@@ -545,17 +596,23 @@ use PVE::JSONSchema qw(get_standard_option);
 
 use base qw(PVE::API2::Firewall::RulesBase);
 
-__PACKAGE__->additional_parameters({ 
+__PACKAGE__->additional_parameters({
     node => get_standard_option('pve-node'),
-    vmid => get_standard_option('pve-vmid'),                              
+    vmid => get_standard_option('pve-vmid'),
 });
 
 sub rule_env {
     my ($class, $param) = @_;
-    
+
     return 'ct';
 }
 
+sub lock_config {
+    my ($class, $param, $code) = @_;
+
+    PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
+}
+
 sub load_config {
     my ($class, $param) = @_;
 
index 2341b91a6b6104e828bea914b53483c46012633c..422210399bf43454fa6aa21d22b13135afb6e4ba 100644 (file)
@@ -21,7 +21,7 @@ my $add_option_properties = sub {
     foreach my $k (keys %$option_properties) {
        $properties->{$k} = $option_properties->{$k};
     }
-    
+
     return $properties;
 };
 
@@ -121,38 +121,39 @@ sub register_handlers {
        code => sub {
            my ($param) = @_;
 
+           PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, sub {
+               my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
+               my $vmfw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, $rule_env, $param->{vmid});
 
-           my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
-           my $vmfw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, $rule_env, $param->{vmid});
+               my (undef, $digest) = PVE::Firewall::copy_opject_with_digest($vmfw_conf->{options});
+               PVE::Tools::assert_if_modified($digest, $param->{digest});
 
-           my (undef, $digest) = PVE::Firewall::copy_opject_with_digest($vmfw_conf->{options});
-           PVE::Tools::assert_if_modified($digest, $param->{digest});
+               if ($param->{delete}) {
+                   foreach my $opt (PVE::Tools::split_list($param->{delete})) {
+                       raise_param_exc({ delete => "no such option '$opt'" })
+                           if !$option_properties->{$opt};
+                       delete $vmfw_conf->{options}->{$opt};
+                   }
+               }
 
-           if ($param->{delete}) {
-               foreach my $opt (PVE::Tools::split_list($param->{delete})) {
-                   raise_param_exc({ delete => "no such option '$opt'" }) 
-                       if !$option_properties->{$opt};
-                   delete $vmfw_conf->{options}->{$opt};
+               if (defined($param->{enable})) {
+                   $param->{enable} = $param->{enable} ? 1 : 0;
                }
-           }
 
-           if (defined($param->{enable})) {
-               $param->{enable} = $param->{enable} ? 1 : 0;
-           }
+               foreach my $k (keys %$option_properties) {
+                   next if !defined($param->{$k});
+                   $vmfw_conf->{options}->{$k} = $param->{$k};
+               }
 
-           foreach my $k (keys %$option_properties) {
-               next if !defined($param->{$k});
-               $vmfw_conf->{options}->{$k} = $param->{$k}; 
-           }
+               PVE::Firewall::save_vmfw_conf($param->{vmid}, $vmfw_conf);
+           });
 
-           PVE::Firewall::save_vmfw_conf($param->{vmid}, $vmfw_conf);
-           
            return undef;
        }});
 
     $class->register_method({
-       name => 'log', 
-       path => 'log', 
+       name => 'log',
+       path => 'log',
        method => 'GET',
        description => "Read firewall log",
        proxyto => 'node',
@@ -175,11 +176,23 @@ sub register_handlers {
                    minimum => 0,
                    optional => 1,
                },
+               since => {
+                   type => 'integer',
+                   minimum => 0,
+                   description => "Display log since this UNIX epoch.",
+                   optional => 1,
+               },
+               until => {
+                   type => 'integer',
+                   minimum => 0,
+                   description => "Display log until this UNIX epoch.",
+                   optional => 1,
+               },
            },
        },
        returns => {
            type => 'array',
-           items => { 
+           items => {
                type => "object",
                properties => {
                    n => {
@@ -198,15 +211,21 @@ sub register_handlers {
 
            my $rpcenv = PVE::RPCEnvironment::get();
            my $user = $rpcenv->get_user();
-           my $vmid = $param->{vmid};
+           my $filename = "/var/log/pve-firewall.log";
+           my $vmid = $param->{'vmid'};
+
+           my $callback = sub {
+               my ($line) = @_;
+               my $reg = "^$vmid ";
+               return $line =~ m/$reg/;
+           };
+
+           my ($count, $lines) = PVE::Firewall::Helpers::dump_fw_logfile(
+               $filename, $param, $callback);
 
-           my ($count, $lines) = PVE::Tools::dump_logfile("/var/log/pve-firewall.log", 
-                                                          $param->{start}, $param->{limit},
-                                                          "^$vmid ");
-           
            $rpcenv->set_result_attrib('total', $count);
-           
-           return $lines; 
+
+           return $lines;
        }});
 
 
@@ -235,7 +254,7 @@ sub register_handlers {
            type => 'array',
            items => {
                type => "object",
-               properties => { 
+               properties => {
                    type => {
                        type => 'string',
                        enum => ['alias', 'ipset'],
@@ -243,7 +262,13 @@ sub register_handlers {
                    name => {
                        type => 'string',
                    },
-                   comment => { 
+                   ref => {
+                       type => 'string',
+                   },
+                   scope => {
+                       type => 'string',
+                   },
+                   comment => {
                        type => 'string',
                        optional => 1,
                    },
@@ -252,48 +277,14 @@ sub register_handlers {
        },
        code => sub {
            my ($param) = @_;
-           
+
            my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
            my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, $rule_env, $param->{vmid});
 
-           my $ipsets = {};
-           my $aliases = {};
-
-           foreach my $conf (($cluster_conf, $fw_conf)) {
-               next if !$conf;
-               if (!$param->{type} || $param->{type} eq 'ipset') {
-                   foreach my $name (keys %{$conf->{ipset}}) {
-                       my $data = { 
-                           type => 'ipset',
-                           name => $name,
-                           ref => "+$name",
-                       };
-                       if (my $comment = $conf->{ipset_comments}->{$name}) {
-                           $data->{comment} = $comment;
-                       }
-                       $ipsets->{$name} = $data;
-                   }
-               }
-
-               if (!$param->{type} || $param->{type} eq 'alias') {
-                   foreach my $name (keys %{$conf->{aliases}}) {
-                       my $e = $conf->{aliases}->{$name};
-                       my $data = { 
-                           type => 'alias',
-                           name => $name,
-                           ref => $name,
-                       };
-                       $data->{comment} = $e->{comment} if $e->{comment};
-                       $aliases->{$name} = $data;
-                   }
-               }
-           }
+           my $dc_refs = PVE::Firewall::Helpers::collect_refs($cluster_conf, $param->{type}, 'dc');
+           my $vm_refs = PVE::Firewall::Helpers::collect_refs($fw_conf, $param->{type}, 'guest');
 
-           my $res = [];
-           foreach my $e (values %$ipsets) { push @$res, $e; };
-           foreach my $e (values %$aliases) { push @$res, $e; };
-           
-           return $res; 
+           return [@$dc_refs, @$vm_refs];
        }});
 }
 
@@ -305,17 +296,17 @@ use warnings;
 use base qw(PVE::API2::Firewall::VMBase);
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::VMRules",  
+    subclass => "PVE::API2::Firewall::VMRules",
     path => 'rules',
 });
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::VMAliases",  
+    subclass => "PVE::API2::Firewall::VMAliases",
     path => 'aliases',
 });
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::VMIPSetList",  
+    subclass => "PVE::API2::Firewall::VMIPSetList",
     path => 'ipset',
 });
 
@@ -329,17 +320,17 @@ use warnings;
 use base qw(PVE::API2::Firewall::VMBase);
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::CTRules",  
+    subclass => "PVE::API2::Firewall::CTRules",
     path => 'rules',
 });
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::CTAliases",  
+    subclass => "PVE::API2::Firewall::CTAliases",
     path => 'aliases',
 });
 
 __PACKAGE__->register_method ({
-    subclass => "PVE::API2::Firewall::CTIPSetList",  
+    subclass => "PVE::API2::Firewall::CTIPSetList",
     path => 'ipset',
 });
 
index 3c8f9565dc7ede36868ef01f028f25fcc9c83af1..194b8ea7eaaa7edc73ca31611046b434a1a6a98c 100644 (file)
@@ -1,12 +1,12 @@
 DESTDIR=
 PREFIX=/usr
-PERLDIR=${DESTDIR}/${PREFIX}/share/perl5
+PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5
 
 all:
 
 .PHONY: install
 install:
-       install -d -m 0755 ${PERLDIR}/PVE/API2
+       install -d -m 0755 $(PERLDIR)/PVE/API2
        make -C Firewall install
 
 .PHONY: clean
index 1319bfbed3af437d6fe8c87851150eba9325e4a1..0abfeccffc94cec940760e69a894e392dc33f151 100644 (file)
@@ -24,6 +24,8 @@ use PVE::SafeSyslog;
 use PVE::Tools qw($IPV4RE $IPV6RE);
 use PVE::Tools qw(run_command lock_file dir_glob_foreach);
 
+use PVE::Firewall::Helpers;
+
 my $pvefw_conf_dir = "/etc/pve/firewall";
 my $clusterfw_conf_filename = "$pvefw_conf_dir/cluster.fw";
 
@@ -66,9 +68,13 @@ PVE::JSONSchema::register_format('IPorCIDR', \&pve_verify_ip_or_cidr);
 sub pve_verify_ip_or_cidr {
     my ($cidr, $noerr) = @_;
 
-    if ($cidr =~ m!^(?:$IPV6RE|$IPV4RE)(/(\d+))?$!) {
-       return $cidr if Net::IP->new($cidr);
+    if ($cidr =~ m!^(?:$IPV6RE|$IPV4RE)(?:/\d+)?$!) {
+        # Net::IP throws an error if the masked CIDR part isn't zero, e.g., `192.168.1.155/24`
+        # fails but `192.168.1.0/24` succeeds. clean_cidr removes the non zero bits from the CIDR.
+       my $clean_cidr = clean_cidr($cidr);
+       return $cidr if Net::IP->new($clean_cidr);
        return undef if $noerr;
+
        die Net::IP::Error() . "\n";
     }
     return undef if $noerr;
@@ -79,11 +85,24 @@ PVE::JSONSchema::register_format('IPorCIDRorAlias', \&pve_verify_ip_or_cidr_or_a
 sub pve_verify_ip_or_cidr_or_alias {
     my ($cidr, $noerr) = @_;
 
-    return if $cidr =~ m/^(?:$ip_alias_pattern)$/;
+    return if $cidr =~ m@^(dc/|guest/)?(?:$ip_alias_pattern)$@;
 
     return pve_verify_ip_or_cidr($cidr, $noerr);
 }
 
+sub clean_cidr {
+    my ($cidr) = @_;
+    my ($ip, $len) = split('/', $cidr);
+    return $cidr if !$len;
+    my $ver = ($ip =~ m!^$IPV4RE$!) ? 4 : 6;
+
+    my $bin_ip = Net::IP::ip_iptobin( Net::IP::ip_expand_address($ip, $ver), $ver);
+    my $bin_mask = Net::IP::ip_get_mask($len, $ver);
+    my $clean_ip = Net::IP::ip_compress_address( Net::IP::ip_bintoip($bin_ip & $bin_mask, $ver), $ver);
+
+    return "${clean_ip}/$len";
+}
+
 PVE::JSONSchema::register_standard_option('ipset-name', {
     description => "IP set name.",
     type => 'string',
@@ -213,7 +232,7 @@ my $pve_fw_macros = {
        { action => 'PARAM', proto => 'udp', dport => '6881' },
     ],
     'Ceph' => [
-        "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Deamons)",
+        "Ceph Storage Cluster traffic (Ceph Monitors, OSD & MDS Daemons)",
        # Legacy port for protocol v1
         { action => 'PARAM', proto => 'tcp', dport => '6789' },
        # New port for protocol v2
@@ -276,7 +295,7 @@ my $pve_fw_macros = {
        { action => 'PARAM', proto => 'tcp', dport => '9418' },
     ],
     'HKP' => [
-       "OpenPGP HTTP keyserver protocol traffic",
+       "OpenPGP HTTP key server protocol traffic",
        { action => 'PARAM', proto => 'tcp', dport => '11371' },
     ],
     'HTTP' => [
@@ -394,6 +413,10 @@ my $pve_fw_macros = {
        { action => 'PARAM', proto => 'udp', dport => '5632' },
        { action => 'PARAM', proto => 'tcp', dport => '5631' },
     ],
+    'PMG' => [
+       "Proxmox Mail Gateway web interface",
+       { action => 'PARAM', proto => 'tcp', dport => '8006' },
+    ],
     'POP3' => [
        "POP3 traffic",
        { action => 'PARAM', proto => 'tcp', dport => '110' },
@@ -490,6 +513,10 @@ my $pve_fw_macros = {
        { action => 'PARAM', proto => '41' },
        { action => 'PARAM', proto => 'udp', dport => '5072,8374' },
     ],
+    'SPICEproxy' => [
+       "Proxmox VE SPICE display proxy traffic",
+       { action => 'PARAM', proto => 'tcp', dport => '3128' },
+    ],
     'Squid' => [
        "Squid web proxy traffic",
        { action => 'PARAM', proto => 'tcp', dport => '3128' },
@@ -551,6 +578,18 @@ my $pve_fw_macros = {
     ],
 };
 
+my $pve_fw_helpers = {
+    'amanda' => { proto => 'udp', dport => '10080', 'v4' => 1, 'v6' => 1 },
+    'ftp' => { proto => 'tcp', dport => '21', 'v4' => 1, 'v6' => 1},
+    'irc' => { proto => 'tcp', dport => '6667', 'v4' => 1 },
+    'netbios-ns' => { proto => 'udp', dport => '137', 'v4' => 1 },
+    'pptp' => { proto => 'tcp', dport => '1723', 'v4' => 1, },
+    'sane' => { proto => 'tcp', dport => '6566', 'v4' => 1, 'v6' => 1 },
+    'sip' => { proto => 'udp', dport => '5060', 'v4' => 1, 'v6' => 1 },
+    'snmp' => { proto => 'udp', dport => '161', 'v4' => 1 },
+    'tftp' => { proto => 'udp', dport => '69', 'v4' => 1, 'v6' => 1},
+};
+
 my $pve_fw_parsed_macros;
 my $pve_fw_macro_descr;
 my $pve_fw_macro_ipversion = {};
@@ -588,7 +627,6 @@ $pve_std_chains_conf->{4} = {
        # same as shorewall 'Drop', which is equal to DROP,
        # but REJECT/DROP some packages to reduce logging,
        # and ACCEPT critical ICMP types
-       { action => 'PVEFW-reject',  proto => 'tcp', dport => '43' }, # REJECT 'auth'
        # we are not interested in BROADCAST/MULTICAST/ANYCAST
        { action => 'PVEFW-DropBroadcast' },
        # ACCEPT critical ICMP types
@@ -611,7 +649,6 @@ $pve_std_chains_conf->{4} = {
        # same as shorewall 'Reject', which is equal to Reject,
        # but REJECT/DROP some packages to reduce logging,
        # and ACCEPT critical ICMP types
-       { action => 'PVEFW-reject',  proto => 'tcp', dport => '43' }, # REJECT 'auth'
        # we are not interested in BROADCAST/MULTICAST/ANYCAST
        { action => 'PVEFW-DropBroadcast' },
        # ACCEPT critical ICMP types
@@ -632,7 +669,7 @@ $pve_std_chains_conf->{4} = {
     ],
     'PVEFW-tcpflags' => [
        # same as shorewall tcpflags action.
-       # Packets arriving on this interface are checked for som illegal combinations of TCP flags
+       # Packets arriving on this interface are checked for some illegal combinations of TCP flags
        { match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG', target => '-g PVEFW-logflags' },
        { match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE', target => '-g PVEFW-logflags' },
        { match => '-p tcp -m tcp --tcp-flags SYN,RST SYN,RST', target => '-g PVEFW-logflags' },
@@ -725,7 +762,7 @@ $pve_std_chains_conf->{6} = {
     ],
     'PVEFW-tcpflags' => [
        # same as shorewall tcpflags action.
-       # Packets arriving on this interface are checked for som illegal combinations of TCP flags
+       # Packets arriving on this interface are checked for some illegal combinations of TCP flags
        { match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG FIN,PSH,URG', target => '-g PVEFW-logflags' },
        { match => '-p tcp -m tcp --tcp-flags FIN,SYN,RST,PSH,ACK,URG NONE', target => '-g PVEFW-logflags' },
        { match => '-p tcp -m tcp --tcp-flags SYN,RST SYN,RST', target => '-g PVEFW-logflags' },
@@ -781,12 +818,14 @@ my $icmp_type_names = {
 # ip6tables -p icmpv6 -h
 
 my $icmpv6_type_names = {
-    'any' => 1,
     'destination-unreachable' => 1,
     'no-route' => 1,
     'communication-prohibited' => 1,
+    'beyond-scope' => 1,
     'address-unreachable' => 1,
     'port-unreachable' => 1,
+    'failed-policy' => 1,
+    'reject-route' => 1,
     'packet-too-big' => 1,
     'time-exceeded' => 1,
     'ttl-zero-during-transit' => 1,
@@ -806,6 +845,22 @@ my $icmpv6_type_names = {
     'redirect' => 1,
 };
 
+my $is_valid_icmp_type = sub {
+    my ($type, $valid_types) = @_;
+
+    if ($type =~ m/^\d+$/) {
+       # values for icmp-type range between 0 and 255 (8 bit field)
+       die "invalid icmp-type '$type'\n" if $type > 255;
+    } else {
+       die "unknown icmp-type '$type'\n" if !defined($valid_types->{$type});
+    }
+};
+
+my $proto_is_icmp = sub {
+    my $proto = shift;
+    return $proto eq 'icmp' || $proto eq 'icmpv6' || $proto eq 'ipv6-icmp';
+};
+
 sub init_firewall_macros {
 
     $pve_fw_parsed_macros = {};
@@ -978,8 +1033,8 @@ sub local_network {
 }
 
 # ipset names are limited to 31 characters,
-# and we use '-v4' or '-v6' to indicate IP versions, 
-# and we use '_swap' suffix for atomic update, 
+# and we use '-v4' or '-v6' to indicate IP versions,
+# and we use '_swap' suffix for atomic update,
 # for example PVEFW-${VMID}-${ipset_name}_swap
 
 my $max_iptables_ipset_name_length = 31 - length("PVEFW-") - length("_swap");
@@ -1012,7 +1067,7 @@ sub parse_address_list {
        return;
     }
 
-    if ($str =~ m/^${ip_alias_pattern}$/) {
+    if ($str =~ m@^(dc/|guest/)?${ip_alias_pattern}$@) {
        die "alias name too long\n" if length($str) > $max_alias_name_length;
        return;
     }
@@ -1045,6 +1100,9 @@ sub parse_address_list {
     return $ipversion;
 }
 
+# $dport must only be set to 1 if the parsed parameter is dport and the
+# protocol is one of the ICMP variants - ICMP type values used to be stored in
+# the dport parameter.
 sub parse_port_name_number_or_range {
     my ($str, $dport) = @_;
 
@@ -1076,7 +1134,7 @@ sub parse_port_name_number_or_range {
        }
     }
 
-    die "ICPM ports not allowed in port range\n" if $icmp_port && $count > 0;
+    die "ICMP ports not allowed in port range\n" if $icmp_port && $count > 0;
 
     # I really don't like to use the word number here, but it's the only thing
     # that makes sense in a literal way. The range 1:100 counts as 2, not as
@@ -1087,6 +1145,19 @@ sub parse_port_name_number_or_range {
     return (scalar(@elements) > 1);
 }
 
+PVE::JSONSchema::register_format('pve-fw-conntrack-helper', \&pve_fw_verify_conntrack_helper);
+sub pve_fw_verify_conntrack_helper {
+   my ($list) = @_;
+
+   my @helpers = split(/,/, $list);
+   die "extraneous commas in list\n" if $list ne join(',', @helpers);
+   foreach my $helper (@helpers) {
+       die "unknown helper $helper" if !$pve_fw_helpers->{$helper};
+   }
+
+   return $list;
+}
+
 PVE::JSONSchema::register_format('pve-fw-sport-spec', \&pve_fw_verify_sport_spec);
 sub pve_fw_verify_sport_spec {
    my ($portstr) = @_;
@@ -1127,6 +1198,19 @@ sub pve_fw_verify_protocol_spec {
    return $proto;
 }
 
+PVE::JSONSchema::register_format('pve-fw-icmp-type-spec', \&pve_fw_verify_icmp_type_spec);
+sub pve_fw_verify_icmp_type_spec {
+    my ($icmp_type) = @_;
+
+    if ($icmp_type_names->{$icmp_type} ||  $icmpv6_type_names->{$icmp_type}) {
+       return $icmp_type;
+    }
+
+    die "invalid icmp-type value '$icmp_type'\n" if $icmp_type ne '';
+
+    return $icmp_type;
+}
+
 
 # helper function for API
 
@@ -1226,7 +1310,7 @@ our $cluster_option_properties = {
                type => 'integer',
                minimum => 0,
                optional => 1,
-               description => 'Inital burst of packages which will get logged',
+               description => 'Initial burst of packages which will always get logged before the rate is applied',
                default => 5,
            },
        },
@@ -1256,23 +1340,35 @@ our $host_option_properties = {
     tcpflags => {
        description => "Filter illegal combinations of TCP flags.",
        type => 'boolean',
+       default => 0,
        optional => 1,
     },
     nf_conntrack_max => {
        description => "Maximum number of tracked connections.",
        type => 'integer',
        optional => 1,
+       default => 262144,
        minimum => 32768,
     },
     nf_conntrack_tcp_timeout_established => {
        description => "Conntrack established timeout.",
        type => 'integer',
        optional => 1,
+       default => 432000,
        minimum => 7875,
     },
+    nf_conntrack_tcp_timeout_syn_recv => {
+       description => "Conntrack syn recv timeout.",
+       type => 'integer',
+       optional => 1,
+       default => 60,
+       minimum => 30,
+       maximum => 60,
+    },
     ndp => {
-       description => "Enable NDP.",
+       description => "Enable NDP (Neighbor Discovery Protocol).",
        type => 'boolean',
+       default => 0,
        optional => 1,
     },
     nf_conntrack_allow_invalid => {
@@ -1281,33 +1377,68 @@ our $host_option_properties = {
        default => 0,
        optional => 1,
     },
+    nf_conntrack_helpers => {
+       type => 'string', format => 'pve-fw-conntrack-helper',
+       description => "Enable conntrack helpers for specific protocols. ".
+           "Supported protocols: amanda, ftp, irc, netbios-ns, pptp, sane, sip, snmp, tftp",
+       default => '',
+       optional => 1,
+    },
+    protection_synflood => {
+       description => "Enable synflood protection",
+       type => 'boolean',
+       default => 0,
+       optional => 1,
+    },
+    protection_synflood_rate => {
+       description => "Synflood protection rate syn/sec by ip src.",
+       type => 'integer',
+       optional => 1,
+       default => 200,
+    },
+    protection_synflood_burst => {
+       description => "Synflood protection rate burst by ip src.",
+       type => 'integer',
+       optional => 1,
+       default => 1000,
+    },
     log_nf_conntrack => {
        description => "Enable logging of conntrack information.",
        type => 'boolean',
        default => 0,
        optional => 1
     },
+    nftables => {
+       description => "Enable nftables based firewall (tech preview)",
+       type => 'boolean',
+       default => 0,
+       optional => 1,
+    },
 };
 
 our $vm_option_properties = {
     enable => {
        description => "Enable/disable firewall rules.",
        type => 'boolean',
+       default => 0,
        optional => 1,
     },
     macfilter => {
        description => "Enable/disable MAC address filter.",
        type => 'boolean',
+       default => 1,
        optional => 1,
     },
     dhcp => {
        description => "Enable DHCP.",
        type => 'boolean',
+       default => 0,
        optional => 1,
     },
     ndp => {
-       description => "Enable NDP.",
+       description => "Enable NDP (Neighbor Discovery Protocol).",
        type => 'boolean',
+       default => 0,
        optional => 1,
     },
     radv => {
@@ -1385,11 +1516,13 @@ my $rule_properties = {
        description => "Restrict packet source address. $addr_list_descr",
        type => 'string', format => 'pve-fw-addr-spec',
        optional => 1,
+       maxLength => 512,
     },
     dest => {
        description => "Restrict packet destination address. $addr_list_descr",
        type => 'string', format => 'pve-fw-addr-spec',
        optional => 1,
+       maxLength => 512,
     },
     proto => {
        description => "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'.",
@@ -1420,6 +1553,11 @@ my $rule_properties = {
        type => 'string',
        optional => 1,
     },
+    'icmp-type' => {
+       description => "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'.",
+       type => 'string', format => 'pve-fw-icmp-type-spec',
+       optional => 1,
+    },
 };
 
 sub add_rule_properties {
@@ -1540,7 +1678,7 @@ sub verify_rule {
     my $set_ip_version = sub {
        my $vers = shift;
        if ($vers) {
-           die "detected mixed ipv4/ipv6 adresses in rule\n"
+           die "detected mixed ipv4/ipv6 addresses in rule\n"
                if $ipversion && ($vers != $ipversion);
            $ipversion = $vers;
        }
@@ -1551,19 +1689,26 @@ sub verify_rule {
 
        if (my $value = $rule->{$name}) {
            if ($value =~ m/^\+/) {
-               if ($value =~ m/^\+(${ipset_name_pattern})$/) {
-                   &$add_error($name, "no such ipset '$1'")
-                       if !($cluster_conf->{ipset}->{$1} || ($fw_conf && $fw_conf->{ipset}->{$1}));
+               if ($value =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
+                   &$add_error($name, "no such ipset '$2'")
+                       if !($cluster_conf->{ipset}->{$2} || ($fw_conf && $fw_conf->{ipset}->{$2}));
 
                } else {
                    &$add_error($name, "invalid ipset name '$value'");
                }
-           } elsif ($value =~ m/^${ip_alias_pattern}$/){
-               my $alias = lc($value);
+           } elsif ($value =~ m@^(guest/|dc/)?(${ip_alias_pattern})$@){
+               my $scope = $1 // "";
+               my $alias = lc($2);
                &$add_error($name, "no such alias '$value'")
                    if !($cluster_conf->{aliases}->{$alias} || ($fw_conf && $fw_conf->{aliases}->{$alias}));
-               my $e = $fw_conf ? $fw_conf->{aliases}->{$alias} : undef;
-               $e = $cluster_conf->{aliases}->{$alias} if !$e && $cluster_conf;
+
+               my $e;
+               if ($scope ne 'dc/' && $fw_conf) {
+                   $e = $fw_conf->{aliases}->{$alias};
+               }
+               if ($scope ne 'guest/' && !$e && $cluster_conf) {
+                   $e = $cluster_conf->{aliases}->{$alias};
+               }
 
                &$set_ip_version($e->{ipversion});
            }
@@ -1609,22 +1754,37 @@ sub verify_rule {
        }
     }
 
+    my $is_icmp = 0;
     if ($rule->{proto}) {
        eval { pve_fw_verify_protocol_spec($rule->{proto}); };
        &$add_error('proto', $@) if $@;
        &$set_ip_version(4) if $rule->{proto} eq 'icmp';
-       &$set_ip_version(6) if $rule->{proto} eq 'icmpv6';
+       &$set_ip_version(6) if $rule->{proto} eq 'icmpv6';
+       &$set_ip_version(6) if $rule->{proto} eq 'ipv6-icmp';
+       $is_icmp = $proto_is_icmp->($rule->{proto});
     }
 
     if ($rule->{dport}) {
-       eval { parse_port_name_number_or_range($rule->{dport}, 1); };
+       eval { parse_port_name_number_or_range($rule->{dport}, $is_icmp); };
        &$add_error('dport', $@) if $@;
        my $proto = $rule->{proto};
        &$add_error('proto', "missing property - 'dport' requires this property")
            if !$proto;
        &$add_error('dport', "protocol '$proto' does not support ports")
-           if !$PROTOCOLS_WITH_PORTS->{$proto} &&
-               $proto ne 'icmp' && $proto ne 'icmpv6'; # special cases
+           if !$PROTOCOLS_WITH_PORTS->{$proto} && !$is_icmp; #special cases
+    }
+
+    if (my $icmp_type = $rule ->{'icmp-type'}) {
+       my $proto = $rule->{proto};
+       &$add_error('proto', "missing property - 'icmp-type' requires this property")
+           if !$is_icmp;
+       &$add_error('icmp-type', "'icmp-type' cannot be specified together with 'dport'")
+           if $rule->{dport};
+       if ($proto eq 'icmp' && !$icmp_type_names->{$icmp_type}) {
+           &$add_error('icmp-type', "invalid icmp-type '$icmp_type' for proto 'icmp'");
+       } elsif (($proto eq 'icmpv6' || $proto eq 'ipv6-icmp') && !$icmpv6_type_names->{$icmp_type}) {
+           &$add_error('icmp-type', "invalid icmp-type '$icmp_type' for proto '$proto'");
+       }
     }
 
     if ($rule->{sport}) {
@@ -1638,7 +1798,7 @@ sub verify_rule {
     }
 
     if ($rule->{source}) {
-       eval { 
+       eval {
            my $source_ipversion = parse_address_list($rule->{source});
            &$set_ip_version($source_ipversion);
        };
@@ -1647,8 +1807,8 @@ sub verify_rule {
     }
 
     if ($rule->{dest}) {
-       eval { 
-           my $dest_ipversion = parse_address_list($rule->{dest}); 
+       eval {
+           my $dest_ipversion = parse_address_list($rule->{dest});
            &$set_ip_version($dest_ipversion);
        };
        &$add_error('dest', $@) if $@;
@@ -1733,11 +1893,9 @@ sub rules_audit_permissions {
 }
 
 # core functions
-my $bridge_firewall_enabled = 0;
 
 sub enable_bridge_firewall {
 
-    return if $bridge_firewall_enabled; # only once
 
     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-iptables", "1");
     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/bridge/bridge-nf-call-ip6tables", "1");
@@ -1745,19 +1903,20 @@ sub enable_bridge_firewall {
     # make sure syncookies are enabled (which is default on newer 3.X kernels anyways)
     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/ipv4/tcp_syncookies", "1");
 
-    $bridge_firewall_enabled = 1;
 }
 
 sub iptables_restore_cmdlist {
-    my ($cmdlist) = @_;
+    my ($cmdlist, $table) = @_;
 
-    run_command(['iptables-restore', '-n'], input => $cmdlist, errmsg => "iptables_restore_cmdlist");
+    $table = 'filter' if !$table;
+    run_command(['iptables-restore', '-T', $table, '-n'], input => $cmdlist, errmsg => "iptables_restore_cmdlist");
 }
 
 sub ip6tables_restore_cmdlist {
-    my ($cmdlist) = @_;
+    my ($cmdlist, $table) = @_;
 
-    run_command(['ip6tables-restore', '-n'], input => $cmdlist, errmsg => "iptables_restore_cmdlist");
+    $table = 'filter' if !$table;
+    run_command(['ip6tables-restore', '-T', $table, '-n'], input => $cmdlist, errmsg => "iptables_restore_cmdlist");
 }
 
 sub ipset_restore_cmdlist {
@@ -1773,9 +1932,10 @@ sub ebtables_restore_cmdlist {
 }
 
 sub iptables_get_chains {
-    my ($iptablescmd) = @_;
+    my ($iptablescmd, $t) = @_;
 
     $iptablescmd = "iptables" if !$iptablescmd;
+    $t = 'filter' if !$t;
 
     my $res = {};
 
@@ -1810,7 +1970,7 @@ sub iptables_get_chains {
            return;
        }
 
-       return if $table ne 'filter';
+       return if $table ne $t;
 
        if ($line =~ m/^:(\S+)\s/) {
            my $chain = $1;
@@ -1820,7 +1980,7 @@ sub iptables_get_chains {
            my ($chain, $sig) = ($1, $2);
            return if !&$is_pvefw_chain($chain);
            $res->{$chain} = $sig;
-       } elsif ($line =~ m/^-A\s+(INPUT|OUTPUT|FORWARD)\s+-j\s+PVEFW-\1$/) {
+       } elsif ($line =~ m/^-A\s+(INPUT|OUTPUT|FORWARD|PREROUTING)\s+-j\s+PVEFW-\1$/) {
            $hooks->{$1} = 1;
        } else {
            # simply ignore the rest
@@ -1864,6 +2024,8 @@ sub ipset_get_chains {
        return if $line =~ m/^\s*$/;
        if ($line =~ m/^(?:\S+)\s(PVEFW-\S+)\s(?:\S+).*/) {
            my $chain = $1;
+           # ignore initval from ipset v7.7+, won't set that yet so it'd mess up change detection
+           $line =~ s/\binitval 0x[0-9a-f]+//;
            $line =~ s/\s+$//; # delete trailing white space
            push @{$chains->{$chain}}, $line;
        } else {
@@ -1886,13 +2048,22 @@ sub ebtables_get_chains {
 
     my $res = {};
     my $chains = {};
+    my $table;
     my $parser = sub {
        my $line = shift;
        return if $line =~ m/^#/;
        return if $line =~ m/^\s*$/;
-       if ($line =~ m/^:(\S+)\s\S+$/) {
+       if ($line =~ m/^\*(\S+)$/) {
+           $table = $1;
+           return;
+       }
+
+       return if $table ne "filter";
+
+       if ($line =~ m/^:(\S+)\s(ACCEPT|DROP|RETURN)$/) {
            # Make sure we know chains exist even if they're empty.
            $chains->{$1} //= [];
+           $res->{$1}->{policy} = $2;
        } elsif ($line =~ m/^(?:\S+)\s(\S+)\s(?:\S+).*/) {
            my $chain = $1;
            $line =~ s/\s+$//;
@@ -1912,7 +2083,7 @@ sub ebtables_get_chains {
     return $res;
 }
 
-# substitude action of rule according to action hash
+# substitute action of rule according to action hash
 sub rule_substitude_action {
     my ($rule, $actions) = @_;
 
@@ -1937,12 +2108,13 @@ sub ipt_gen_src_or_dst_match {
 
     my $match;
     if ($adr =~ m/^\+/) {
-       if ($adr =~ m/^\+(${ipset_name_pattern})$/) {
-           my $name = $1;
+       if ($adr =~ m@^\+(guest/|dc/)?(${ipset_name_pattern})$@) {
+           my $scope = $1 // "";
+           my $name = $2;
            my $ipset_chain;
-           if ($fw_conf && $fw_conf->{ipset}->{$name}) {
+           if ($scope ne 'dc/' && $fw_conf && $fw_conf->{ipset}->{$name}) {
                $ipset_chain = compute_ipset_chain_name($fw_conf->{vmid}, $name, $ipversion);
-           } elsif ($cluster_conf && $cluster_conf->{ipset}->{$name}) {
+           } elsif ($scope ne 'guest/' && $cluster_conf && $cluster_conf->{ipset}->{$name}) {
                $ipset_chain = compute_ipset_chain_name(0, $name, $ipversion);
            } else {
                die "no such ipset '$name'\n";
@@ -1951,10 +2123,16 @@ sub ipt_gen_src_or_dst_match {
        } else {
            die "invalid security group name '$adr'\n";
        }
-    } elsif ($adr =~ m/^${ip_alias_pattern}$/){
-       my $alias = lc($adr);
-       my $e = $fw_conf ? $fw_conf->{aliases}->{$alias} : undef;
-       $e = $cluster_conf->{aliases}->{$alias} if !$e && $cluster_conf;
+    } elsif ($adr =~ m@^(dc/|guest/)?(${ip_alias_pattern})$@){
+       my $scope = $1 // "";
+       my $alias = lc($2);
+       my $e;
+       if ($scope ne 'dc/' && $fw_conf) {
+           $e = $fw_conf->{aliases}->{$alias};
+       }
+       if ($scope ne 'guest/' && !$e && $cluster_conf) {
+           $e = $cluster_conf->{aliases}->{$alias};
+       }
        die "no such alias '$adr'\n" if !$e;
        $match = "-${dir} $e->{cidr}";
     } elsif ($adr =~ m/\-/){
@@ -1989,28 +2167,27 @@ sub ipt_rule_to_cmds {
 
        if (my $proto = $rule->{proto}) {
            push @match, "-p $proto";
+           my $is_icmp = $proto_is_icmp->($proto);
 
-           my $multidport = defined($rule->{dport}) && parse_port_name_number_or_range($rule->{dport}, 1);
+           my $multidport = defined($rule->{dport}) && parse_port_name_number_or_range($rule->{dport}, $is_icmp);
            my $multisport = defined($rule->{sport}) && parse_port_name_number_or_range($rule->{sport}, 0);
 
            my $add_dport = sub {
-               return if !$rule->{dport};
+               return if !defined($rule->{dport});
 
+               # NOTE: we re-use dport to store --icmp-type for icmp* protocol
                if ($proto eq 'icmp') {
-                   # Note: we use dport to store --icmp-type
-                   die "unknown icmp-type '$rule->{dport}'\n"
-                       if $rule->{dport} !~ /^\d+$/ && !defined($icmp_type_names->{$rule->{dport}});
+                   $is_valid_icmp_type->($rule->{dport}, $icmp_type_names);
                    push @match, "-m icmp --icmp-type $rule->{dport}";
                } elsif ($proto eq 'icmpv6') {
-                   # Note: we use dport to store --icmpv6-type
-                   die "unknown icmpv6-type '$rule->{dport}'\n"
-                       if $rule->{dport} !~ /^\d+$/ && !defined($icmpv6_type_names->{$rule->{dport}});
+                   $is_valid_icmp_type->($rule->{dport}, $icmpv6_type_names);
                    push @match, "-m icmpv6 --icmpv6-type $rule->{dport}";
                } elsif (!$PROTOCOLS_WITH_PORTS->{$proto}) {
                    die "protocol $proto does not have ports\n";
                } elsif ($multidport) {
                    push @match, "--match multiport", "--dports $rule->{dport}";
                } else {
+                   return if !$rule->{dport};
                    push @match, "--dport $rule->{dport}";
                }
            };
@@ -2027,7 +2204,18 @@ sub ipt_rule_to_cmds {
                }
            };
 
+           my $add_icmp_type = sub {
+               return if !defined($rule->{'icmp-type'}) || $rule->{'icmp-type'} eq '';
+
+               die "'icmp-type' can only be set if 'icmp', 'icmpv6' or 'ipv6-icmp' is specified\n"
+                   if !$is_icmp;
+               my $type = $proto eq 'icmp' ? 'icmp-type' : 'icmpv6-type';
+
+               push @match, "-m $proto --$type $rule->{'icmp-type'}";
+           };
+
            # order matters - single port before multiport!
+           $add_icmp_type->();
            $add_dport->() if $multisport;
            $add_sport->();
            $add_dport->() if !$multisport;
@@ -2045,8 +2233,7 @@ sub ipt_rule_to_cmds {
        $targetstr = $rule->{target};
     } else {
        my $action = (defined $rule->{action}) ? $rule->{action} : "";
-       my $goto = 1 if $action eq 'PVEFW-SET-ACCEPT-MARK';
-       $targetstr = ($goto) ? "-g $action" : "-j $action";
+       $targetstr = $action eq 'PVEFW-SET-ACCEPT-MARK' ? "-g $action" : "-j $action";
     }
 
     my @iptcmds;
@@ -2223,7 +2410,7 @@ sub ruleset_create_vm_chain {
     if (!(defined($options->{dhcp}) && $options->{dhcp} == 0)) {
        if ($ipversion == 4) {
            if ($direction eq 'OUT') {
-               ruleset_generate_rule($ruleset, $chain, $ipversion, 
+               ruleset_generate_rule($ruleset, $chain, $ipversion,
                                      { action => 'PVEFW-SET-ACCEPT-MARK',
                                        proto => 'udp', sport => 68, dport => 67 });
            } else {
@@ -2360,7 +2547,8 @@ sub generate_tap_rules_direction {
     my $tapchain = "$iface-$direction";
 
     my $ipfilter_name = compute_ipfilter_ipset_name($netid);
-    my $ipfilter_ipset = compute_ipset_chain_name($vmid, $ipfilter_name, $ipversion)
+    my $ipfilter_ipset;
+    $ipfilter_ipset = compute_ipset_chain_name($vmid, $ipfilter_name, $ipversion)
        if $options->{ipfilter} || $vmfw_conf->{ipset}->{$ipfilter_name};
 
     if ($options->{enable}) {
@@ -2450,6 +2638,7 @@ sub enable_host_firewall {
        $rule->{iface_in} = $rule->{iface} if $rule->{iface};
 
        eval {
+           $rule->{logmsg} = "$rule->{action}: ";
            if ($rule->{type} eq 'group') {
                ruleset_add_group_rule($ruleset, $cluster_conf, $chain, $rule, 'IN', $accept_action, $ipversion);
            } elsif ($rule->{type} eq 'in') {
@@ -2468,6 +2657,7 @@ sub enable_host_firewall {
     ruleset_addrule($ruleset, $chain, "$mngmntsrc -p tcp --dport 5900:5999", "-j $accept_action");  # PVE VNC Console
     ruleset_addrule($ruleset, $chain, "$mngmntsrc -p tcp --dport 3128", "-j $accept_action");  # SPICE Proxy
     ruleset_addrule($ruleset, $chain, "$mngmntsrc -p tcp --dport 22", "-j $accept_action");  # SSH
+    ruleset_addrule($ruleset, $chain, "$mngmntsrc -p tcp --dport 60000:60050", "-j $accept_action");  # Migration
 
     # corosync inbound rules
     if (defined($corosync_conf)) {
@@ -2649,32 +2839,36 @@ sub parse_fw_rule {
 
        last if $rule->{type} eq 'group';
 
-       if ($line =~ s/^-p (\S+)\s*//) {
+       if ($line =~ s/^(?:-p|--?proto) (\S+)\s*//) {
            $rule->{proto} = $1;
            next;
        }
 
-       if ($line =~ s/^-dport (\S+)\s*//) {
+       if ($line =~ s/^--?dport (\S+)\s*//) {
            $rule->{dport} = $1;
            next;
        }
 
-       if ($line =~ s/^-sport (\S+)\s*//) {
+       if ($line =~ s/^--?sport (\S+)\s*//) {
            $rule->{sport} = $1;
            next;
        }
-       if ($line =~ s/^-source (\S+)\s*//) {
+       if ($line =~ s/^--?source (\S+)\s*//) {
            $rule->{source} = $1;
            next;
        }
-       if ($line =~ s/^-dest (\S+)\s*//) {
+       if ($line =~ s/^--?dest (\S+)\s*//) {
            $rule->{dest} = $1;
            next;
        }
-       if ($line =~ s/^-log (emerg|alert|crit|err|warning|notice|info|debug|nolog)\s*//) {
+       if ($line =~ s/^--?log (emerg|alert|crit|err|warning|notice|info|debug|nolog)\s*//) {
            $rule->{log} = $1;
            next;
        }
+       if ($line =~ s/^--?icmp-type (\S+)\s*//) {
+           $rule->{'icmp-type'} = $1;
+           next;
+       }
 
        last;
     }
@@ -2741,13 +2935,17 @@ sub parse_hostfw_option {
 
     my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
 
-    if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp|log_nf_conntrack|nf_conntrack_allow_invalid):\s*(0|1)\s*$/i) {
+    if ($line =~ m/^(enable|nosmurfs|tcpflags|ndp|log_nf_conntrack|nf_conntrack_allow_invalid|protection_synflood|nftables):\s*(0|1)\s*$/i) {
        $opt = lc($1);
        $value = int($2);
     } elsif ($line =~ m/^(log_level_in|log_level_out|tcp_flags_log_level|smurf_log_level):\s*(($loglevels)\s*)?$/i) {
        $opt = lc($1);
        $value = $2 ? lc($3) : '';
-    } elsif ($line =~ m/^(nf_conntrack_max|nf_conntrack_tcp_timeout_established):\s*(\d+)\s*$/i) {
+    } elsif ($line =~ m/^(nf_conntrack_helpers):\s*(((\S+)[,]?)+)\s*$/i) {
+       $opt = lc($1);
+       $value = lc($2);
+       pve_fw_verify_conntrack_helper($value);
+    } elsif ($line =~ m/^(nf_conntrack_max|nf_conntrack_tcp_timeout_established|nf_conntrack_tcp_timeout_syn_recv|protection_synflood_rate|protection_synflood_burst|protection_limit):\s*(\d+)\s*$/i) {
        $opt = lc($1);
        $value = int($2);
     } else {
@@ -2785,11 +2983,26 @@ sub parse_clusterfw_option {
 }
 
 sub resolve_alias {
-    my ($clusterfw_conf, $fw_conf, $cidr) = @_;
+    my ($clusterfw_conf, $fw_conf, $cidr, $scope) = @_;
+
+    # When we're on the cluster level, the cluster config only gets
+    # saved into fw_conf, so we need some extra handling here (to
+    # stay consistent)
+    my ($cluster_config, $local_config);
+    if (!$clusterfw_conf) {
+       ($cluster_config, $local_config) = ($fw_conf, undef);
+    } else {
+       ($cluster_config, $local_config) = ($clusterfw_conf, $fw_conf);
+    }
 
     my $alias = lc($cidr);
-    my $e = $fw_conf ? $fw_conf->{aliases}->{$alias} : undef;
-    $e = $clusterfw_conf->{aliases}->{$alias} if !$e && $clusterfw_conf;
+    my $e;
+    if ($scope ne 'dc/' && $local_config) {
+       $e = $local_config->{aliases}->{$alias};
+    }
+    if ($scope ne 'guest/' && !$e && $cluster_config) {
+       $e = $cluster_config->{aliases}->{$alias};
+    }
 
     die "no such alias '$cidr'\n" if !$e;;
 
@@ -2800,7 +3013,7 @@ sub parse_ip_or_cidr {
     my ($cidr) = @_;
 
     my $ipversion;
-    
+
     if ($cidr =~ m!^(?:$IPV6RE)(/(\d+))?$!) {
        $cidr =~ s|/128$||;
        $ipversion = 6;
@@ -2818,7 +3031,7 @@ sub parse_alias {
     my ($line) = @_;
 
     # we can add single line comments to the end of the line
-    my $comment = decode('utf8', $1) if $line =~ s/\s*#\s*(.*?)\s*$//;
+    my $comment = $line =~ s/\s*#\s*(.*?)\s*$// ? decode('utf8', $1) : undef;
 
     if ($line =~ m/^(\S+)\s(\S+)$/) {
        my ($name, $cidr) = ($1, $2);
@@ -2854,6 +3067,8 @@ sub generic_fw_config_parser {
     }
     return {} if !$raw;
 
+    my $curr_group_keys = {};
+
     my $linenr = 0;
     while ($raw =~ /^\h*(.*?)\h*$/gm) {
        my $line = $1;
@@ -2887,7 +3102,7 @@ sub generic_fw_config_parser {
                warn "$prefix: $err";
                next;
            }
-           
+
            $res->{$section}->{$group} = [];
            $res->{group_comments}->{$group} =  decode('utf8', $comment)
                if $comment;
@@ -2903,7 +3118,7 @@ sub generic_fw_config_parser {
            $section = 'ipset';
            $group = lc($1);
            my $comment = $2;
-           eval {      
+           eval {
                die "ipset name too long\n" if length($group) > $max_ipset_name_length;
                die "invalid ipset name '$group'\n" if $group !~ m/^${ipset_name_pattern}$/;
            };
@@ -2914,6 +3129,8 @@ sub generic_fw_config_parser {
            }
 
            $res->{$section}->{$group} = [];
+           $curr_group_keys = {};
+
            $res->{ipset_comments}->{$group} = decode('utf8', $comment)
                if $comment;
            next;
@@ -2961,7 +3178,7 @@ sub generic_fw_config_parser {
            push @{$res->{$section}->{$group}}, $rule;
        } elsif ($section eq 'ipset') {
            # we can add single line comments to the end of the rule
-           my $comment = decode('utf8', $1) if $line =~ s/#\s*(.*?)\s*$//;
+           my $comment = $line =~ s/#\s*(.*?)\s*$// ? decode('utf8', $1) : undef;
 
            $line =~ m/^(\!)?\s*(\S+)\s*$/;
            my $nomatch = $1;
@@ -2972,12 +3189,16 @@ sub generic_fw_config_parser {
                $errors->{nomatch} = "nomatch not supported by kernel";
            }
 
-           eval { 
-               if ($cidr =~ m/^${ip_alias_pattern}$/) {
-                   resolve_alias($cluster_conf, $res, $cidr); # make sure alias exists
+           eval {
+               if ($cidr =~ m@^(dc/|guest/)?(${ip_alias_pattern}$)@) {
+                   my $scope = $1 // "";
+                   my $alias = $2;
+                   resolve_alias($cluster_conf, $res, $alias, $scope); # make sure alias exists
                } else {
                    $cidr = parse_ip_or_cidr($cidr);
                }
+               die "duplicate ipset entry for '$cidr'\n"
+                   if defined($curr_group_keys->{$cidr});
            };
            if (my $err = $@) {
                chomp $err;
@@ -3001,6 +3222,7 @@ sub generic_fw_config_parser {
            }
 
            push @{$res->{$section}->{$group}}, $entry;
+           $curr_group_keys->{$cidr} = 1;
        } else {
            warn "$prefix: skip line - unknown section\n";
            next;
@@ -3010,6 +3232,8 @@ sub generic_fw_config_parser {
     return $res;
 }
 
+# this is only used to prevent concurrent runs of rule compilation/application
+# see lock_*_conf for cfs locks protectiong config modification
 sub run_locked {
     my ($code, @param) = @_;
 
@@ -3046,18 +3270,23 @@ sub read_local_vm_config {
                }
            }
         } elsif ($d->{type} eq 'lxc') {
-            if ($have_lxc) {
-                my $cfspath = PVE::LXC::Config->cfs_config_path($vmid);
-                if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
-                    $lxc->{$vmid} = $conf;
-                }
-            }
-        }
+           if ($have_lxc) {
+               my $cfspath = PVE::LXC::Config->cfs_config_path($vmid);
+               if (my $conf = PVE::Cluster::cfs_read_file($cfspath)) {
+                   $lxc->{$vmid} = $conf;
+               }
+           }
+       }
     }
 
     return $vmdata;
 };
 
+# FIXME: move use sites over to moved helper and break older packages, then remove this here
+sub lock_vmfw_conf {
+    return PVE::Firewall::Helpers::lock_vmfw_conf(@_);
+}
+
 sub load_vmfw_conf {
     my ($cluster_conf, $rule_env, $vmid, $dir) = @_;
 
@@ -3103,6 +3332,7 @@ my $format_rules = sub {
                $raw .= " -dport $rule->{dport}" if $rule->{dport};
                $raw .= " -sport $rule->{sport}" if $rule->{sport};
                $raw .= " -log $rule->{log}" if $rule->{log};
+               $raw .= " -icmp-type $rule->{'icmp-type'}" if defined($rule->{'icmp-type'}) && $rule->{'icmp-type'} ne '';
            }
 
            $raw .= " # " . encode('utf8', $rule->{comment})
@@ -3136,7 +3366,7 @@ my $format_aliases = sub {
     my $raw = '';
 
     $raw .= "[ALIASES]\n\n";
-    foreach my $k (keys %$aliases) {
+    foreach my $k (sort keys %$aliases) {
        my $e = $aliases->{$k};
        $raw .= "$e->{name} $e->{cidr}";
        $raw .= " # " . encode('utf8', $e->{comment})
@@ -3150,7 +3380,7 @@ my $format_aliases = sub {
 
 my $format_ipsets = sub {
     my ($fw_conf) = @_;
-    
+
     my $raw = '';
 
     foreach my $ipset (sort keys %{$fw_conf->{ipset}}) {
@@ -3164,7 +3394,13 @@ my $format_ipsets = sub {
 
        my $nethash = {};
        foreach my $entry (@$options) {
-           $nethash->{$entry->{cidr}} = $entry;
+           my $cidr = $entry->{cidr};
+           if (defined($nethash->{$cidr})) {
+               warn "ignoring duplicate ipset entry '$cidr'\n";
+               next;
+           }
+
+           $nethash->{$cidr} = $entry;
        }
 
        foreach my $cidr (sort keys %$nethash) {
@@ -3211,27 +3447,14 @@ sub save_vmfw_conf {
     }
 }
 
+# FIXME: remove with 8.0 and break older qemu-server/pve-container
 sub remove_vmfw_conf {
-    my ($vmid) = @_;
-
-    my $vmfw_conffile = "$pvefw_conf_dir/$vmid.fw";
-
-    unlink $vmfw_conffile;
+    return PVE::Firewall::Helpers::remove_vmfw_conf(@_);
 }
 
+# FIXME: remove with 8.0 and break older qemu-server/pve-container
 sub clone_vmfw_conf {
-    my ($vmid, $newid) = @_;
-
-    my $sourcevm_conffile = "$pvefw_conf_dir/$vmid.fw";
-    my $clonevm_conffile = "$pvefw_conf_dir/$newid.fw";
-
-    if (-f $clonevm_conffile) {
-       unlink $clonevm_conffile;
-    }
-    if (-f $sourcevm_conffile) {
-       my $data = PVE::Tools::file_get_contents($sourcevm_conffile);
-       PVE::Tools::file_set_contents($clonevm_conffile, $data);
-    }
+    return PVE::Firewall::Helpers::clone_vmfw_conf(@_);
 }
 
 sub read_vm_firewall_configs {
@@ -3241,12 +3464,12 @@ sub read_vm_firewall_configs {
 
     foreach my $vmid (keys %{$vmdata->{qemu}}) {
        my $vmfw_conf = load_vmfw_conf($cluster_conf, 'vm', $vmid, $dir);
-       next if !$vmfw_conf->{options}; # skip if file does not exists
+       next if !$vmfw_conf->{options}; # skip if file does not exist
        $vmfw_configs->{$vmid} = $vmfw_conf;
     }
     foreach my $vmid (keys %{$vmdata->{lxc}}) {
         my $vmfw_conf = load_vmfw_conf($cluster_conf, 'ct', $vmid, $dir);
-        next if !$vmfw_conf->{options}; # skip if file does not exists
+        next if !$vmfw_conf->{options}; # skip if file does not exist
         $vmfw_configs->{$vmid} = $vmfw_conf;
     }
 
@@ -3321,8 +3544,10 @@ sub generate_ipset_chains {
            next if $entry->{errors}; # skip entries with errors
            eval {
                my ($cidr, $ver);
-               if ($entry->{cidr} =~ m/^${ip_alias_pattern}$/) {
-                   ($cidr, $ver) = resolve_alias($clusterfw_conf, $fw_conf, $entry->{cidr});
+               if ($entry->{cidr} =~ m@^(dc/|guest/)?(${ip_alias_pattern})$@) {
+                   my $scope = $1 // "";
+                   my $alias = $2;
+                   ($cidr, $ver) = resolve_alias($clusterfw_conf, $fw_conf, $alias, $scope);
                } else {
                    ($cidr, $ver) = parse_ip_or_cidr($entry->{cidr});
                }
@@ -3354,9 +3579,13 @@ sub generate_ipset_chains {
                $hashsize = round_powerof2($hashsize);
            }
 
+           my $bucketsize = 12; # lower than the default of 14, faster but slightly more memory use
+
            my $family = $ipversion == "6" ? "inet6" : "inet";
 
-           $ipset_ruleset->{$name} = ["create $name hash:net family $family hashsize $hashsize maxelem $hashsize"];
+           $ipset_ruleset->{$name} = [
+               "create $name hash:net family $family hashsize $hashsize maxelem $hashsize bucketsize $bucketsize"
+           ];
 
            foreach my $cidr (sort keys %$data) {
                my $entry = $data->{$cidr};
@@ -3405,6 +3634,15 @@ my $set_global_log_ratelimit = sub {
     }
 };
 
+sub lock_clusterfw_conf {
+    my ($timeout, $code, @param) = @_;
+
+    my $res = PVE::Cluster::cfs_lock_firewall("cluster", $timeout, $code, @param);
+    die $@ if $@;
+
+    return $res;
+}
+
 sub load_clusterfw_conf {
     my ($filename) = @_;
 
@@ -3437,7 +3675,7 @@ sub save_clusterfw_conf {
     $raw .= &$format_aliases($aliases) if $aliases && scalar(keys %$aliases);
 
     $raw .= &$format_ipsets($cluster_conf) if $cluster_conf->{ipset};
+
     my $rules = $cluster_conf->{rules};
     if ($rules && scalar(@$rules)) {
        $raw .= "[RULES]\n\n";
@@ -3468,6 +3706,17 @@ sub save_clusterfw_conf {
     }
 }
 
+sub lock_hostfw_conf : prototype($$$@) {
+    my ($node, $timeout, $code, @param) = @_;
+
+    $node = $nodename if !defined($node);
+
+    my $res = PVE::Cluster::cfs_lock_firewall("host-$node", $timeout, $code, @param);
+    die $@ if $@;
+
+    return $res;
+}
+
 sub load_hostfw_conf {
     my ($cluster_conf, $filename) = @_;
 
@@ -3478,7 +3727,9 @@ sub load_hostfw_conf {
 }
 
 sub save_hostfw_conf {
-    my ($hostfw_conf) = @_;
+    my ($hostfw_conf, $filename) = @_;
+
+    $filename = $hostfw_conf_filename if !defined($filename);
 
     my $raw = '';
 
@@ -3493,9 +3744,9 @@ sub save_hostfw_conf {
     }
 
     if ($raw) {
-       PVE::Tools::file_set_contents($hostfw_conf_filename, $raw);
+       PVE::Tools::file_set_contents($filename, $raw);
     } else {
-       unlink $hostfw_conf_filename;
+       unlink $filename;
     }
 }
 
@@ -3544,14 +3795,49 @@ sub compile {
 
     push @{$cluster_conf->{ipset}->{management}}, { cidr => $localnet };
 
-    my $ruleset = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 4);
-    my $rulesetv6 = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 6);
+    my $ruleset = {};
+    my $rulesetv6 = {};
+    $ruleset->{filter} = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 4);
+    $ruleset->{raw} = compile_iptables_raw($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 4);
+    $rulesetv6->{filter} = compile_iptables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 6);
+    $rulesetv6->{raw} = compile_iptables_raw($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, 6);
     my $ebtables_ruleset = compile_ebtables_filter($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata);
     my $ipset_ruleset = compile_ipsets($cluster_conf, $vmfw_configs, $vmdata);
 
     return ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset);
 }
 
+sub compile_iptables_raw {
+    my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, $ipversion) = @_;
+
+    my $ruleset = {};
+
+    my $hostfw_options = $hostfw_conf->{options} || {};
+    my $protection_synflood = $hostfw_options->{protection_synflood} || 0;
+    my $conntrack_helpers = $hostfw_options->{nf_conntrack_helpers} || '';
+
+    ruleset_create_chain($ruleset, "PVEFW-PREROUTING") if $protection_synflood != 0 || $conntrack_helpers ne '';
+
+    if($protection_synflood) {
+
+       my $protection_synflood_rate = $hostfw_options->{protection_synflood_rate} ? $hostfw_options->{protection_synflood_rate} : 200;
+       my $protection_synflood_burst = $hostfw_options->{protection_synflood_burst} ? $hostfw_options->{protection_synflood_burst} : 1000;
+       my $protection_synflood_limit = $hostfw_options->{protection_synflood_limit} ? $hostfw_options->{protection_synflood_limit} : 3000;
+       my $protection_synflood_expire = $hostfw_options->{nf_conntrack_tcp_timeout_syn_recv} ? $hostfw_options->{nf_conntrack_tcp_timeout_syn_recv} : 60;
+       $protection_synflood_expire = $protection_synflood_expire * 1000;
+       my $protection_synflood_mask = $ipversion == 4 ? 32 : 64;
+
+       ruleset_addrule($ruleset, "PVEFW-PREROUTING", "-p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m hashlimit --hashlimit-above $protection_synflood_rate/sec --hashlimit-burst $protection_synflood_burst --hashlimit-mode srcip --hashlimit-name syn --hashlimit-htable-size 2097152 --hashlimit-srcmask $protection_synflood_mask --hashlimit-htable-expire $protection_synflood_expire", "-j DROP");
+    }
+
+    foreach my $conntrack_helper (split(/,/, $conntrack_helpers)) {
+       my $helper = $pve_fw_helpers->{$conntrack_helper};
+       ruleset_addrule($ruleset, "PVEFW-PREROUTING", "-p $helper->{proto} -m $helper->{proto} --dport $helper->{dport} -j CT", "--helper $conntrack_helper") if $helper && $helper->{"v$ipversion"};
+    }
+
+    return $ruleset;
+}
+
 sub compile_iptables_filter {
     my ($cluster_conf, $hostfw_conf, $vmfw_configs, $vmdata, $corosync_conf, $ipversion) = @_;
 
@@ -3662,7 +3948,7 @@ sub compile_ipsets {
        my $localnet_ver;
        ($localnet, $localnet_ver) = parse_ip_or_cidr(local_network() || '127.0.0.0/8');
 
-       $cluster_conf->{aliases}->{local_network} = { 
+       $cluster_conf->{aliases}->{local_network} = {
            name => 'local_network', cidr => $localnet, ipversion => $localnet_ver };
     }
 
@@ -3679,7 +3965,7 @@ sub compile_ipsets {
            return if !$vmfw_conf;
 
            # When the 'ipfilter' option is enabled every device for which there
-           # is no 'ipfilter-netX' ipset defiend gets an implicit empty default
+           # is no 'ipfilter-netX' ipset defined gets an implicit empty default
            # ipset.
            # The reason is that ipfilter ipsets are always filled with standard
            # IPv6 link-local filters.
@@ -3718,7 +4004,7 @@ sub compile_ipsets {
            return if !$vmfw_conf;
 
            # When the 'ipfilter' option is enabled every device for which there
-           # is no 'ipfilter-netX' ipset defiend gets an implicit empty default
+           # is no 'ipfilter-netX' ipset defined gets an implicit empty default
            # ipset.
            # The reason is that ipfilter ipsets are always filled with standard
            # IPv6 link-local filters, as well as the IP addresses configured
@@ -3783,7 +4069,7 @@ sub compile_ebtables_filter {
        eval {
            my $conf = $vmdata->{qemu}->{$vmid};
            my $vmfw_conf = $vmfw_configs->{$vmid};
-           return if !$vmfw_conf;
+           return if !$vmfw_conf || !$vmfw_conf->{options}->{enable};
            my $ipsets = $vmfw_conf->{ipset};
 
            foreach my $netid (sort keys %$conf) {
@@ -3833,7 +4119,9 @@ sub compile_ebtables_filter {
                    # ebtables changes this to a .0/MASK network but we just
                    # want the address here, no network - see #2193
                    $ip =~ s|/(\d+)$||;
-                   push @$arpfilter, $ip;
+                   if ($ip ne 'dhcp') {
+                       push @$arpfilter, $ip;
+                   }
                }
                generate_tap_layer2filter($ruleset, $iface, $macaddr, $vmfw_conf, $vmid, $arpfilter);
            }
@@ -3857,7 +4145,7 @@ sub generate_tap_layer2filter {
     ruleset_create_chain($ruleset, $tapchain);
 
     if (defined($macaddr) && !(defined($options->{macfilter}) && $options->{macfilter} == 0)) {
-           ruleset_addrule($ruleset, $tapchain, "-s ! $macaddr", '-j DROP');
+       ruleset_addrule($ruleset, $tapchain, "-s ! $macaddr", '-j DROP');
     }
 
     if (@$arpfilter){
@@ -3932,6 +4220,7 @@ sub get_ruleset_status {
        if (defined($change_only_regex)) {
            $action = 'ignore' if ($chain !~ m/$change_only_regex/);
            $statushash->{$chain}->{rules} = $active_chains->{$chain}->{rules};
+           $statushash->{$chain}->{policy} = $active_chains->{$chain}->{policy};
            $sig = $sig->{sig};
        }
        $statushash->{$chain}->{action} = $action;
@@ -3950,11 +4239,13 @@ sub print_sig_rule {
 }
 
 sub get_ruleset_cmdlist {
-    my ($ruleset, $iptablescmd) = @_;
+    my ($ruleset, $iptablescmd, $table) = @_;
+
+    $table = 'filter' if !$table;
 
-    my $cmdlist = "*filter\n"; # we pass this to iptables-restore;
+    my $cmdlist = "*$table\n"; # we pass this to iptables-restore;
 
-    my ($active_chains, $hooks) = iptables_get_chains($iptablescmd);
+    my ($active_chains, $hooks) = iptables_get_chains($iptablescmd, $table);
     my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest);
 
     # create missing chains first
@@ -3966,7 +4257,7 @@ sub get_ruleset_cmdlist {
        $cmdlist .= ":$chain - [0:0]\n";
     }
 
-    foreach my $h (qw(INPUT OUTPUT FORWARD)) {
+    foreach my $h (qw(INPUT OUTPUT FORWARD PREROUTING)) {
        my $chain = "PVEFW-$h";
        if ($ruleset->{$chain} && !$hooks->{$h}) {
            $cmdlist .= "-A $h -j $chain\n";
@@ -4001,10 +4292,11 @@ sub get_ruleset_cmdlist {
        next if $chain eq 'PVEFW-INPUT';
        next if $chain eq 'PVEFW-OUTPUT';
        next if $chain eq 'PVEFW-FORWARD';
+       next if $chain eq 'PVEFW-PREROUTING';
        $cmdlist .= "-X $chain\n";
     }
 
-    my $changes = $cmdlist ne "*filter\n" ? 1 : 0;
+    my $changes = $cmdlist ne "*$table\n" ? 1 : 0;
 
     $cmdlist .= "COMMIT\n";
 
@@ -4029,7 +4321,8 @@ sub get_ebtables_cmdlist {
     my $pve_include = 0;
     foreach my $chain (sort keys %$statushash) {
        next if ($statushash->{$chain}->{action} eq 'delete');
-       $cmdlist .= ":$chain ACCEPT\n";
+       my $policy = $statushash->{$chain}->{policy} // 'ACCEPT';
+       $cmdlist .= ":$chain $policy\n";
        $pve_include = 1 if ($chain eq 'PVEFW-FORWARD');
     }
 
@@ -4117,9 +4410,11 @@ sub apply_ruleset {
     my ($ipset_create_cmdlist, $ipset_delete_cmdlist, $ipset_changes) =
        get_ipset_cmdlist($ipset_ruleset);
 
-    my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset);
-    my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6, "ip6tables");
+    my ($cmdlist, $changes) = get_ruleset_cmdlist($ruleset->{filter});
+    my ($cmdlistv6, $changesv6) = get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables");
     my ($ebtables_cmdlist, $ebtables_changes) = get_ebtables_cmdlist($ebtables_ruleset);
+    my ($cmdlist_raw, $changes_raw) = get_ruleset_cmdlist($ruleset->{raw}, undef, 'raw');
+    my ($cmdlistv6_raw, $changesv6_raw) = get_ruleset_cmdlist($rulesetv6->{raw}, "ip6tables", 'raw');
 
     if ($verbose) {
        if ($ipset_changes) {
@@ -4138,6 +4433,16 @@ sub apply_ruleset {
            print $cmdlistv6;
        }
 
+       if ($changes_raw) {
+           print "iptables table raw changes:\n";
+           print $cmdlist_raw;
+       }
+
+       if ($changesv6_raw) {
+           print "ip6tables table raw changes:\n";
+           print $cmdlistv6_raw;
+       }
+
        if ($ebtables_changes) {
            print "ebtables changes:\n";
            print $ebtables_cmdlist;
@@ -4154,11 +4459,21 @@ sub apply_ruleset {
 
     iptables_restore_cmdlist($cmdlist);
 
+    $tmpfile = "$pve_fw_status_dir/ip4cmdlistraw";
+    PVE::Tools::file_set_contents($tmpfile, $cmdlist_raw || '');
+
+    iptables_restore_cmdlist($cmdlist_raw, 'raw');
+
     $tmpfile = "$pve_fw_status_dir/ip6cmdlist";
     PVE::Tools::file_set_contents($tmpfile, $cmdlistv6 || '');
 
     ip6tables_restore_cmdlist($cmdlistv6);
 
+    $tmpfile = "$pve_fw_status_dir/ip6cmdlistraw";
+    PVE::Tools::file_set_contents($tmpfile, $cmdlistv6_raw || '');
+
+    ip6tables_restore_cmdlist($cmdlistv6_raw, 'raw');
+
     $tmpfile = "$pve_fw_status_dir/ipsetcmdlist2";
     PVE::Tools::file_set_contents($tmpfile, $ipset_delete_cmdlist || '');
 
@@ -4170,11 +4485,12 @@ sub apply_ruleset {
     PVE::Tools::file_set_contents($tmpfile, $ebtables_cmdlist || '');
 
     # test: re-read status and check if everything is up to date
+    my $ruleset_filter = $ruleset->{filter};
     my $active_chains = iptables_get_chains();
-    my $statushash = get_ruleset_status($ruleset, $active_chains, \&iptables_chain_digest);
+    my $statushash = get_ruleset_status($ruleset_filter, $active_chains, \&iptables_chain_digest);
 
     my $errors;
-    foreach my $chain (sort keys %$ruleset) {
+    foreach my $chain (sort keys %$ruleset_filter) {
        my $stat = $statushash->{$chain};
        if ($stat->{action} ne 'exists') {
            warn "unable to update chain '$chain'\n";
@@ -4182,10 +4498,11 @@ sub apply_ruleset {
        }
     }
 
+    my $rulesetv6_filter = $rulesetv6->{filter};
     my $active_chainsv6 = iptables_get_chains("ip6tables");
-    my $statushashv6 = get_ruleset_status($rulesetv6, $active_chainsv6, \&iptables_chain_digest);
+    my $statushashv6 = get_ruleset_status($rulesetv6_filter, $active_chainsv6, \&iptables_chain_digest);
 
-    foreach my $chain (sort keys %$rulesetv6) {
+    foreach my $chain (sort keys %$rulesetv6_filter) {
        my $stat = $statushashv6->{$chain};
        if ($stat->{action} ne 'exists') {
            warn "unable to update chain '$chain'\n";
@@ -4193,6 +4510,30 @@ sub apply_ruleset {
        }
     }
 
+    my $ruleset_raw = $ruleset->{raw};
+    my $active_chains_raw = iptables_get_chains(undef, 'raw');
+    my $statushash_raw = get_ruleset_status($ruleset_raw, $active_chains_raw, \&iptables_chain_digest);
+
+    foreach my $chain (sort keys %$ruleset_raw) {
+       my $stat = $statushash_raw->{$chain};
+       if ($stat->{action} ne 'exists') {
+           warn "unable to update chain '$chain'\n";
+           $errors = 1;
+       }
+    }
+
+    my $rulesetv6_raw = $rulesetv6->{raw};
+    my $active_chainsv6_raw = iptables_get_chains("ip6tables", 'raw');
+    my $statushashv6_raw = get_ruleset_status($rulesetv6_raw, $active_chainsv6_raw, \&iptables_chain_digest);
+
+    foreach my $chain (sort keys %$rulesetv6_raw) {
+       my $stat = $statushashv6_raw->{$chain};
+       if ($stat->{action} ne 'exists') {
+           warn "unable to update chain '$chain'\n";
+           $errors = 1;
+       }
+    }
+
     my $active_ebtables_chains = ebtables_get_chains();
     my $ebtables_statushash = get_ruleset_status($ebtables_ruleset,
                                $active_ebtables_chains, \&iptables_chain_digest,
@@ -4212,13 +4553,15 @@ sub apply_ruleset {
 
     update_nf_conntrack_tcp_timeout_established($hostfw_conf);
 
+    update_nf_conntrack_tcp_timeout_syn_recv($hostfw_conf);
+
     update_nf_conntrack_logging($hostfw_conf);
 }
 
 sub update_nf_conntrack_max {
     my ($hostfw_conf) = @_;
 
-    my $max = 65536; # reasonable default
+    my $max = 262144; # reasonable default (2^16 * 4), see nf_conntrack-sysctl docs
 
     my $options = $hostfw_conf->{options} || {};
 
@@ -4249,6 +4592,16 @@ sub update_nf_conntrack_tcp_timeout_established {
     PVE::ProcFSTools::write_proc_entry("/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established", $value);
 }
 
+sub update_nf_conntrack_tcp_timeout_syn_recv {
+    my ($hostfw_conf) = @_;
+
+    my $options = $hostfw_conf->{options} || {};
+
+    my $value = defined($options->{nf_conntrack_tcp_timeout_syn_recv}) ? $options->{nf_conntrack_tcp_timeout_syn_recev} : 60;
+
+    PVE::ProcFSTools::write_proc_entry("/proc/sys/net/netfilter/nf_conntrack_tcp_timeout_syn_recv", $value);
+}
+
 my $log_nf_conntrack_enabled = undef;
 sub update_nf_conntrack_logging {
     my ($hostfw_conf) = @_;
@@ -4270,18 +4623,22 @@ sub remove_pvefw_chains {
 
     PVE::Firewall::remove_pvefw_chains_iptables("iptables");
     PVE::Firewall::remove_pvefw_chains_iptables("ip6tables");
+    PVE::Firewall::remove_pvefw_chains_iptables("iptables", "raw");
+    PVE::Firewall::remove_pvefw_chains_iptables("ip6tables", "raw");
     PVE::Firewall::remove_pvefw_chains_ipset();
     PVE::Firewall::remove_pvefw_chains_ebtables();
 
 }
 
 sub remove_pvefw_chains_iptables {
-    my ($iptablescmd) = @_;
+    my ($iptablescmd, $table) = @_;
 
-    my ($chash, $hooks) = iptables_get_chains($iptablescmd);
-    my $cmdlist = "*filter\n";
+    $table = 'filter' if !$table;
+
+    my ($chash, $hooks) = iptables_get_chains($iptablescmd, $table);
+    my $cmdlist = "*$table\n";
 
-    foreach my $h (qw(INPUT OUTPUT FORWARD)) {
+    foreach my $h (qw(INPUT OUTPUT FORWARD PREROUTING)) {
        if ($hooks->{$h}) {
            $cmdlist .= "-D $h -j PVEFW-$h\n";
        }
@@ -4297,9 +4654,9 @@ sub remove_pvefw_chains_iptables {
     $cmdlist .= "COMMIT\n";
 
     if($iptablescmd eq "ip6tables") {
-       ip6tables_restore_cmdlist($cmdlist);
+       ip6tables_restore_cmdlist($cmdlist, $table);
     } else {
-       iptables_restore_cmdlist($cmdlist);
+       iptables_restore_cmdlist($cmdlist, $table);
     }
 }
 
@@ -4308,7 +4665,7 @@ sub remove_pvefw_chains_ipset {
     my $ipset_chains = ipset_get_chains();
 
     my $cmdlist = "";
+
     foreach my $chain (keys %$ipset_chains) {
        $cmdlist .= "flush $chain\n";
        $cmdlist .= "destroy $chain\n";
@@ -4322,12 +4679,30 @@ sub remove_pvefw_chains_ebtables {
     ebtables_restore_cmdlist(get_ebtables_cmdlist({}));
 }
 
-sub init {
-    my $cluster_conf = load_clusterfw_conf();
-    my $cluster_options = $cluster_conf->{options};
-    my $enable = $cluster_options->{enable};
+sub is_nftables {
+    my ($cluster_conf, $host_conf) = @_;
 
-    return if !$enable;
+    if (!-x "/usr/libexec/proxmox/proxmox-firewall") {
+       return 0;
+    }
+
+    $cluster_conf = load_clusterfw_conf() if !defined($cluster_conf);
+    $host_conf = load_hostfw_conf($cluster_conf) if !defined($host_conf);
+
+    return $host_conf->{options}->{nftables};
+}
+
+sub is_enabled_and_not_nftables {
+    my ($cluster_conf, $host_conf) = @_;
+
+    $cluster_conf = load_clusterfw_conf() if !defined($cluster_conf);
+    $host_conf = load_hostfw_conf($cluster_conf) if !defined($host_conf);
+
+    return $cluster_conf->{options}->{enable} && !is_nftables($cluster_conf, $host_conf);
+}
+
+sub init {
+    return if !is_enabled_and_not_nftables();
 
     # load required modules here
 }
@@ -4336,14 +4711,13 @@ sub update {
     my $code = sub {
 
        my $cluster_conf = load_clusterfw_conf();
-       my $cluster_options = $cluster_conf->{options};
+       my $hostfw_conf = load_hostfw_conf($cluster_conf);
 
-       if (!$cluster_options->{enable}) {
+       if (!is_enabled_and_not_nftables($cluster_conf, $hostfw_conf)) {
            PVE::Firewall::remove_pvefw_chains();
            return;
        }
 
-       my $hostfw_conf = load_hostfw_conf($cluster_conf);
 
        my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = compile($cluster_conf, $hostfw_conf);
 
diff --git a/src/PVE/Firewall/Helpers.pm b/src/PVE/Firewall/Helpers.pm
new file mode 100644 (file)
index 0000000..7dcbca3
--- /dev/null
@@ -0,0 +1,172 @@
+package PVE::Firewall::Helpers;
+
+use strict;
+use warnings;
+
+use Date::Parse qw(str2time);
+use Errno qw(ENOENT);
+use File::Basename qw(fileparse);
+use IO::Zlib;
+use PVE::Cluster;
+use PVE::Tools qw(file_get_contents file_set_contents);
+
+use base 'Exporter';
+our @EXPORT_OK = qw(
+lock_vmfw_conf
+remove_vmfw_conf
+clone_vmfw_conf
+collect_refs
+);
+
+my $pvefw_conf_dir = "/etc/pve/firewall";
+
+sub lock_vmfw_conf {
+    my ($vmid, $timeout, $code, @param) = @_;
+
+    die "can't lock VM firewall config for undefined VMID\n"
+       if !defined($vmid);
+
+    my $res = PVE::Cluster::cfs_lock_firewall("vm-$vmid", $timeout, $code, @param);
+    die $@ if $@;
+
+    return $res;
+}
+
+sub remove_vmfw_conf {
+    my ($vmid) = @_;
+
+    my $vmfw_conffile = "$pvefw_conf_dir/$vmid.fw";
+
+    unlink $vmfw_conffile;
+}
+
+sub clone_vmfw_conf {
+    my ($vmid, $newid) = @_;
+
+    my $sourcevm_conffile = "$pvefw_conf_dir/$vmid.fw";
+    my $clonevm_conffile = "$pvefw_conf_dir/$newid.fw";
+
+    lock_vmfw_conf($newid, 10, sub {
+       if (-f $clonevm_conffile) {
+           unlink $clonevm_conffile;
+       }
+       if (-f $sourcevm_conffile) {
+           my $data = file_get_contents($sourcevm_conffile);
+           file_set_contents($clonevm_conffile, $data);
+       }
+    });
+}
+
+sub dump_fw_logfile {
+    my ($filename, $param, $callback) = @_;
+    my ($start, $limit, $since, $until) = $param->@{qw(start limit since until)};
+
+    my $filter = sub {
+       my ($line) = @_;
+
+       if (defined($callback)) {
+           return undef if !$callback->($line);
+       }
+
+       if ($since || $until) {
+           my @words = split / /, $line;
+           my $timestamp = str2time($words[3], $words[4]);
+           return undef if $since && $timestamp < $since;
+           return undef if $until && $timestamp > $until;
+       }
+
+       return $line;
+    };
+
+    if (!defined($since) && !defined($until)) {
+       return PVE::Tools::dump_logfile($filename, $start, $limit, $filter);
+    }
+
+    my %state = (
+       'count' => 0,
+       'lines' => [],
+       'start' => $start,
+       'limit' => $limit,
+    );
+
+    # Take into consideration also rotated logs
+    my ($basename, $logdir, $type) = fileparse($filename);
+    my $regex = qr/^\Q$basename\E(\.[\d]+(\.gz)?)?$/;
+    my @files = ();
+
+    PVE::Tools::dir_glob_foreach($logdir, $regex, sub {
+       my ($file) = @_;
+       push @files,  $file;
+    });
+
+    @files = reverse sort @files;
+
+    my $filecount = 0;
+    for my $filename (@files) {
+       $state{'final'} = $filecount == $#files;
+       $filecount++;
+
+       my $fh;
+       if ($filename =~ /\.gz$/) {
+           $fh = IO::Zlib->new($logdir.$filename, "r");
+       } else {
+           $fh = IO::File->new($logdir.$filename, "r");
+       }
+
+       if (!$fh) {
+           # If file vanished since reading dir entries, ignore
+           next if $!{ENOENT};
+
+           my $lines = $state{'lines'};
+           my $count = ++$state{'count'};
+           push @$lines, ($count, { n => $count, t => "unable to open file - $!"});
+           last;
+       }
+
+       PVE::Tools::dump_logfile_by_filehandle($fh, $filter, \%state);
+
+       close($fh);
+    }
+
+    return ($state{'count'}, $state{'lines'});
+}
+
+sub collect_refs {
+    my ($conf, $type, $scope) = @_;
+
+
+    my $res = [];
+
+    if (!$type || $type eq 'ipset') {
+       foreach my $name (keys %{$conf->{ipset}}) {
+           my $data = {
+               type => 'ipset',
+               name => $name,
+               ref => "+$name",
+               scope => $scope,
+           };
+           if (my $comment = $conf->{ipset_comments}->{$name}) {
+               $data->{comment} = $comment;
+           }
+           push @$res, $data;
+       }
+    }
+
+    if (!$type || $type eq 'alias') {
+       foreach my $name (keys %{$conf->{aliases}}) {
+           my $e = $conf->{aliases}->{$name};
+           my $data = {
+               type => 'alias',
+               name => $name,
+               ref => $name,
+               scope => $scope,
+           };
+           $data->{comment} = $e->{comment} if $e->{comment};
+           push @$res, $data;
+       }
+    }
+
+    return $res;
+}
+
+1;
diff --git a/src/PVE/Firewall/Makefile b/src/PVE/Firewall/Makefile
new file mode 100644 (file)
index 0000000..3274bc6
--- /dev/null
@@ -0,0 +1,12 @@
+DESTDIR=
+PREFIX=/usr
+PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5
+
+SOURCES=Helpers.pm
+
+.PHONY: install
+install: $(SOURCES)
+       install -d -m 0755 $(PERLDIR)/PVE/Firewall
+       for i in $(SOURCES); do install -D -m 0644 $$i $(PERLDIR)/PVE/Firewall/$$i; done
+
+clean:
index 188c38fa52325e79f9a3d5cf40a887eddf83d9f1..140c46ebf5d9b9ca07dbabcd399ad8aa0ad79737 100644 (file)
@@ -29,12 +29,10 @@ my $NUMBER_RE = qr/0x[0-9a-fA-F]+|\d+/;
 
 sub debug {
     my $new_value = shift;
-
     $debug = $new_value if defined($new_value);
-
     return $debug;
 }
-    
+
 sub reset_trace {
     $trace = '';
 }
@@ -120,26 +118,33 @@ sub rule_match {
 
            return undef if $cstate eq 'INVALID'; # no match
            return undef if $cstate eq 'RELATED,ESTABLISHED'; # no match
-           
+
            next if $cstate =~ m/NEW/;
-           
+
            die "cstate test '$cstate' not implemented\n";
        }
 
        if ($rule =~ s/^-m addrtype --src-type (\S+)\s*//) {
            my $atype = $1;
-           die "missing source address type (srctype)\n" 
+           die "missing source address type (srctype)\n"
                if !$pkg->{srctype};
            return undef if $atype ne $pkg->{srctype};
        }
 
        if ($rule =~ s/^-m addrtype --dst-type (\S+)\s*//) {
            my $atype = $1;
-           die "missing destination address type (dsttype)\n" 
+           die "missing destination address type (dsttype)\n"
                if !$pkg->{dsttype};
            return undef if $atype ne $pkg->{dsttype};
        }
 
+       if ($rule =~ s/^-m icmp(v6)? --icmp-type (\S+)\s*//) {
+           my $icmpv6 = !!$1;
+           my $icmptype = $2;
+           die "missing destination address type (dsttype)\n" if !defined($pkg->{dport});
+           return undef if $icmptype ne $pkg->{dport};
+       }
+
        if ($rule =~ s/^-i (\S+)\s*//) {
            my $devre = $1;
            die "missing interface (iface_in)\n" if !$pkg->{iface_in};
@@ -178,7 +183,7 @@ sub rule_match {
            return undef if !$ip->overlaps(Net::IP->new($pkg->{source})); # no match
            next;
        }
-    
+
        if ($rule =~ s/^-d (\S+)\s*//) {
            die "missing destination" if !$pkg->{dest};
            my $ip = Net::IP->new($1);
@@ -248,7 +253,7 @@ sub rule_match {
        }
 
        if ($rule =~ s/^-j NFLOG --nflog-prefix \"[^\"]+\"$//) {
-           return undef; 
+           return undef;
        }
 
        last;
@@ -261,7 +266,7 @@ sub ruleset_simulate_chain {
     my ($ruleset, $ipset_ruleset, $chain, $pkg) = @_;
 
     add_trace("ENTER chain $chain\n");
-    
+
     my $counter = 0;
 
     if ($chain eq 'PVEFW-Drop') {
@@ -289,7 +294,7 @@ sub ruleset_simulate_chain {
            next;
        }
        add_trace("MATCH: $rule\n");
-       
+
        if ($action eq 'ACCEPT' || $action eq 'DROP' || $action eq 'REJECT') {
            add_trace("TERMINATE chain $chain: $action\n");
            return ($action, $counter);
@@ -382,7 +387,7 @@ sub route_packet {
            $pkg->{iface_out} = $from_info->{fwbr} || die 'internal error';
            $pkg->{physdev_in} = $from_info->{tapdev} || die 'internal error';
            $pkg->{physdev_out} = $from_info->{fwln} || die 'internal error';
-       
+
        } elsif ($route_state eq 'fwbr-in') {
 
            $chain = 'PVEFW-FORWARD';
@@ -393,7 +398,7 @@ sub route_packet {
            $pkg->{physdev_out} = $target->{tapdev} || die 'internal error';
 
        } elsif ($route_state =~ m/^vmbr\d+$/) {
-           
+
            die "missing physdev_in - internal error?" if !$physdev_in;
            $pkg->{physdev_in} = $physdev_in;
 
@@ -443,7 +448,7 @@ sub route_packet {
            my ($res, $ctr) = ruleset_simulate_chain($ruleset, $ipset_ruleset, $chain, $pkg);
            $rule_check_counter += $ctr;
            return ($res, $ipt_invocation_counter, $rule_check_counter) if $res ne 'ACCEPT';
-       } 
+       }
 
        $route_state = $next_route_state;
 
@@ -494,9 +499,9 @@ sub simulate_firewall {
     my $from = $test->{from} || die "missing 'from' field";
     my $to = $test->{to} || die "missing 'to' field";
     my $action = $test->{action} || die "missing 'action'";
-    
+
     my $testid = $test->{id};
-    
+
     die "from/to needs to be different" if $from eq $to;
 
     my $pkg = {
@@ -545,14 +550,14 @@ sub simulate_firewall {
        return 'SKIPPED' if !$have_lxc;
        my $vmid = $1;
        $from_info = extract_ct_info($vmdata, $vmid, 0);
-       $start_state = 'fwbr-out'; 
+       $start_state = 'fwbr-out';
        $pkg->{mac_source} = $from_info->{macaddr};
     } elsif ($from =~ m/^vm(\d+)(i(\d))?$/) {
        return 'SKIPPED' if !$have_qemu_server;
        my $vmid = $1;
        my $netnum = $3 || 0;
        $from_info = extract_vm_info($vmdata, $vmid, $netnum);
-       $start_state = 'fwbr-out'; 
+       $start_state = 'fwbr-out';
        $pkg->{mac_source} = $from_info->{macaddr};
     } else {
        die "unable to parse \"from => '$from'\"\n";
@@ -593,16 +598,16 @@ sub simulate_firewall {
     $pkg->{source} = '100.100.1.2' if !defined($pkg->{source});
     $pkg->{dest} = '100.200.3.4' if !defined($pkg->{dest});
 
-    my ($res, $ic, $rc) = route_packet($ruleset, $ipset_ruleset, $pkg, 
+    my ($res, $ic, $rc) = route_packet($ruleset, $ipset_ruleset, $pkg,
                                       $from_info, $target, $start_state);
 
     add_trace("IPT statistics: invocation = $ic, checks = $rc\n");
+
     return $res if $action eq 'QUERY';
 
     die "test failed ($res != $action)\n" if $action ne $res;
 
-    return undef; 
+    return undef;
 }
 
 1;
index 9fdfd9b77e04d793f6242f02b2fbf56f506024b7..26570866c81a55f6098c528e9f8cbeac04da2062 100644 (file)
@@ -1,6 +1,6 @@
 DESTDIR=
 PREFIX= /usr
-PERLDIR=${DESTDIR}/${PREFIX}/share/perl5
+PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5
 
 LIB_SOURCES=                   \
        FirewallSimulator.pm    \
@@ -10,13 +10,15 @@ all:
 
 .PHONY: install
 install:
-       install -d -m 0755 ${PERLDIR}/PVE
-       for i in ${LIB_SOURCES}; do install -D -m 0644 $$i ${PERLDIR}/PVE/$$i; done
+       install -d -m 0755 $(PERLDIR)/PVE
+       for i in $(LIB_SOURCES); do install -D -m 0644 $$i $(PERLDIR)/PVE/$$i; done
        make -C API2 install
        make -C Service install
+       make -C Firewall install
 
 .PHONY: clean
 clean:
        rm -rf *~
        make -C API2 clean
        make -C Service clean
+       make -C Firewall clean
index 423f322f8acfb14f90c0278976bf5f609345b5d7..e1bee4a38ced738b7f94c0d5c62fb1e2a32c88b6 100644 (file)
@@ -1,12 +1,12 @@
 DESTDIR=
 PREFIX=/usr
-PERLDIR=${DESTDIR}/${PREFIX}/share/perl5
+PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5
 
 SOURCES=pve_firewall.pm
 
 .PHONY: install
-install: ${SOURCES}
-       install -d -m 0755 ${PERLDIR}/PVE/Service
-       for i in ${SOURCES}; do install -D -m 0644 $$i ${PERLDIR}/PVE/Service/$$i; done
+install: $(SOURCES)
+       install -d -m 0755 $(PERLDIR)/PVE/Service
+       for i in $(SOURCES); do install -D -m 0644 $$i $(PERLDIR)/PVE/Service/$$i; done
 
 clean:
index d78bcb1daf5daba719f0f7f94d8851148fcfb38d..30d14d96374b207cccf98ee7c0159ad50b178998 100755 (executable)
@@ -2,20 +2,22 @@ package PVE::Service::pve_firewall;
 
 use strict;
 use warnings;
-use PVE::SafeSyslog;
-use PVE::Daemon;
 
-use Time::HiRes qw (gettimeofday);
-use PVE::Tools qw(dir_glob_foreach file_read_firstline);
-use PVE::ProcFSTools;
-use PVE::INotify;
+use Data::Dumper;
+use Time::HiRes qw (gettimeofday usleep);
+
+use PVE::CLIHandler;
 use PVE::Cluster qw(cfs_read_file);
 use PVE::Corosync;
+use PVE::Daemon;
+use PVE::INotify;
+use PVE::ProcFSTools;
 use PVE::RPCEnvironment;
-use PVE::CLIHandler;
+use PVE::SafeSyslog;
+use PVE::Tools qw(dir_glob_foreach file_read_firstline);
+
 use PVE::Firewall;
 use PVE::FirewallSimulator;
-use Data::Dumper;
 
 use base qw(PVE::Daemon);
 
@@ -28,16 +30,12 @@ my $daemon = __PACKAGE__->new('pve-firewall', $cmdline, %daemon_options);
 my $nodename = PVE::INotify::nodename();
 
 sub init {
-
     PVE::Cluster::cfs_update();
 
     PVE::Firewall::init();
 }
 
-my $restart_request = 0;
-my $next_update = 0;
-
-my $cycle = 0;
+my ($next_update, $cycle, $restart_request) = (0, 0, 0);
 my $updatetime = 10;
 
 my $initial_memory_usage;
@@ -45,12 +43,12 @@ my $initial_memory_usage;
 sub shutdown {
     my ($self) = @_;
 
-    syslog('info' , "server closing");
+    syslog('info' , "server shutting down");
 
     # wait for children
     1 while (waitpid(-1, POSIX::WNOHANG()) > 0);
 
-    syslog('info' , "clear firewall rules");
+    syslog('info' , "clear PVE-generated firewall rules");
 
     eval { PVE::Firewall::remove_pvefw_chains(); };
     warn $@ if $@;
@@ -70,7 +68,6 @@ sub run {
     local $SIG{'__WARN__'} = 'IGNORE'; # do not fill up logs
 
     for (;;) { # forever
-
        $next_update = time() + $updatetime;
 
        my ($ccsec, $cusec) = gettimeofday ();
@@ -78,13 +75,11 @@ sub run {
            PVE::Cluster::cfs_update();
            PVE::Firewall::update();
        };
-       my $err = $@;
-
-       if ($err) {
+       if (my $err = $@) {
            syslog('err', "status update error: $err");
        }
 
-       my ($ccsec_end, $cusec_end) = gettimeofday ();
+       my ($ccsec_end, $cusec_end) = gettimeofday();
        my $cptime = ($ccsec_end-$ccsec) + ($cusec_end - $cusec)/1000000;
 
        syslog('info', sprintf("firewall update time (%.3f seconds)", $cptime))
@@ -98,7 +93,7 @@ sub run {
            $initial_memory_usage = $mem->{resident};
        } else {
            my $diff = $mem->{resident} - $initial_memory_usage;
-           if ($diff > 5*1024*1024) {
+           if ($diff > 5 * 1024 * 1024) {
                syslog ('info', "restarting server after $cycle cycles to " .
                        "reduce memory usage (free $mem->{resident} ($diff) bytes)");
                $self->restart_daemon();
@@ -116,9 +111,10 @@ sub run {
 
 $daemon->register_start_command("Start the Proxmox VE firewall service.");
 $daemon->register_restart_command(1, "Restart the Proxmox VE firewall service.");
-$daemon->register_stop_command("Stop firewall. This removes all Proxmox VE " .
-                              "related iptable rules. " .
-                              "The host is unprotected afterwards.");
+$daemon->register_stop_command(
+    "Stop the Proxmox VE firewall service. Note, stopping actively removes all Proxmox VE related"
+    ." iptable rules rendering the host potentially unprotected."
+);
 
 __PACKAGE__->register_method ({
     name => 'status',
@@ -170,11 +166,13 @@ __PACKAGE__->register_method ({
 
                PVE::Firewall::set_verbose(0); # do not show iptables details
                my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset);
-               my ($test, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset);
-               my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, "ip6tables");
+               my ($test, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{filter});
+               my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables");
+               my (undef, $ruleset_changes_raw) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{raw}, undef, 'raw');
+               my (undef, $ruleset_changesv6_raw) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{raw}, "ip6tables", 'raw');
                my (undef, $ebtables_changes) = PVE::Firewall::get_ebtables_cmdlist($ebtables_ruleset);
 
-               $res->{changes} = ($ipset_changes || $ruleset_changes || $ruleset_changesv6 || $ebtables_changes) ? 1 : 0;
+               $res->{changes} = ($ipset_changes || $ruleset_changes || $ruleset_changesv6 || $ebtables_changes || $ruleset_changes_raw || $ruleset_changesv6_raw) ? 1 : 0;
            }
 
            return $res;
@@ -210,15 +208,22 @@ __PACKAGE__->register_method ({
            my (undef, undef, $ipset_changes) = PVE::Firewall::get_ipset_cmdlist($ipset_ruleset);
 
            print "\niptables cmdlist:\n";
-           my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset);
+           my (undef, $ruleset_changes) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{filter});
 
            print "\nip6tables cmdlist:\n";
-           my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6, "ip6tables");
+           my (undef, $ruleset_changesv6) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables");
 
            print "\nebtables cmdlist:\n";
            my (undef, $ebtables_changes) = PVE::Firewall::get_ebtables_cmdlist($ebtables_ruleset);
 
-           if ($ipset_changes || $ruleset_changes || $ruleset_changesv6 || $ebtables_changes) {
+           print "\niptables table raw cmdlist:\n";
+           my (undef, $ruleset_changes_raw) = PVE::Firewall::get_ruleset_cmdlist($ruleset->{raw}, undef, 'raw');
+
+           print "\nip6tables table raw cmdlist:\n";
+           my (undef, $ruleset_changesv6_raw) = PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{raw}, "ip6tables", 'raw');
+
+
+           if ($ipset_changes || $ruleset_changes || $ruleset_changesv6 || $ebtables_changes || $ruleset_changes_raw || $ruleset_changesv6_raw) {
                print "detected changes\n";
            } else {
                print "no changes\n";
@@ -226,7 +231,6 @@ __PACKAGE__->register_method ({
            if (!$cluster_conf->{options}->{enable}) {
                print "firewall disabled\n";
            }
-
        };
 
        PVE::Firewall::run_locked($code);
@@ -259,8 +263,8 @@ __PACKAGE__->register_method ({
 
        my $localnet = PVE::Firewall::local_network() || '127.0.0.0/8';
        print "network auto detect: $localnet\n";
-       if ($cluster_conf->{aliases}->{local_network}) {
-           print "using user defined local_network: $cluster_conf->{aliases}->{local_network}->{cidr}\n";
+       if (my $local_network = $cluster_conf->{aliases}->{local_network}) {
+           print "using user defined local_network: $local_network->{cidr}\n";
        } else {
            print "using detected local_network: $localnet\n";
        }
@@ -294,7 +298,8 @@ __PACKAGE__->register_method ({
     name => 'simulate',
     path => 'simulate',
     method => 'GET',
-    description => "Simulate firewall rules. This does not simulate kernel 'routing' table. Instead, this simply assumes that routing from source zone to destination zone is possible.",
+    description => "Simulate firewall rules. This does not simulates the kernel 'routing' table,"
+       ." but simply assumes that routing from source zone to destination zone is possible.",
     parameters => {
        additionalProperties => 0,
        properties => {
@@ -366,7 +371,8 @@ __PACKAGE__->register_method ({
        my $host_ip = PVE::Cluster::remote_node_ip($nodename);
 
        PVE::FirewallSimulator::reset_trace();
-       print Dumper($ruleset) if $param->{verbose};
+       print Dumper($ruleset->{filter}) if $param->{verbose};
+       print Dumper($ruleset->{raw}) if $param->{verbose};
 
        my $test = {
            from => $param->{from},
@@ -397,8 +403,8 @@ __PACKAGE__->register_method ({
 
        $test->{action} = 'QUERY';
 
-       my $res = PVE::FirewallSimulator::simulate_firewall($ruleset, $ipset_ruleset,
-                                                           $host_ip, $vmdata, $test);
+       my $res = PVE::FirewallSimulator::simulate_firewall(
+           $ruleset->{filter}, $ipset_ruleset, $host_ip, $vmdata, $test);
 
        print "ACTION: $res\n";
 
index 181d5f1e8b7520b008b7761943ce9d04f6e3b6cc..f76250516b92b61fc650c26f743520f024c74591 100644 (file)
@@ -1,6 +1,6 @@
 /*
 
-  Copyright (C) 2014 Proxmox Server Solutions GmbH
+  Copyright (C) 2014 - 2021 Proxmox Server Solutions GmbH
 
   This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
 
@@ -65,7 +65,7 @@ gboolean conntrack = FALSE;
 
 LOG FORMAT:
 
-Special care was taken to allow fast parsing (and filer messages for a singl VM).
+Special care was taken to allow fast parsing and filering messages for a single VM.
 
 <VMID> <LOGLEVEL> <CHAIN> <TIME> <TIMEZONE> <MSG>
 
@@ -1035,11 +1035,11 @@ main(int argc, char *argv[])
     for (int i = 10; i >= 0; i--) {
         if (flock(lockfd, LOCK_EX|LOCK_NB) != 0) {
             if (!i) {
-                fprintf(stderr, "unable to aquire lock '%s': %s\n", LOCKFILE, strerror (errno));
+                fprintf(stderr, "unable to acquire lock '%s': %s\n", LOCKFILE, strerror (errno));
                 exit(-1);
             }
             if (i == 10)
-                fprintf(stderr, "unable to aquire lock '%s' - trying again.\n", LOCKFILE);
+                fprintf(stderr, "unable to acquire lock '%s' - trying again.\n", LOCKFILE);
 
             sleep(1);
         }
index ca976ac8609746d7635c065379cb6b180de73444..76c78fc5496794ecc4c47a61e8e2a75b2fab01c4 100644 (file)
@@ -1,49 +1,41 @@
-=A simple simulator to test our iptables rules=
+= A simple simulator to test our iptables rule generation =
 
-==Invovation==
+== Invocation ==
 
  # ./fwtester.pl
 
-This scans for subdirectory named test-* an invokes fwtester.pl 
-for each subdirectory with:
+This scans for subdirectory named test-* an invokes fwtester.pl for each
+subdirectory with:
 
- # ./fwtester.pl test-<name>/tests 
+ # ./fwtester.pl test-<name>/tests
 
-==Test directory contents==
+== Test directory contents ==
 
 Each test directory can contain the following files:
 
-*cluster.fw  Cluster wide firewall config
-  
-*host.fw     Host firewall config
+ * cluster.fw  Cluster wide firewall config
+ * host.fw     Host firewall config
+ * <VMID>.fw   Firewall config for VMs
+ * tests       Test descriptions
 
-*<VMID>.fw   Firewall config for VMs     
+== Test description ==
 
-*tests      Test descriptions
-
-==Test description==
-
-The test description file can contain one or more tests using
-the following syntax:
+The test description file can contain one or more tests using the following
+syntax:
 
  { from => '<zone>' , to => '<zone>', action => '<DROP|RECECT|ACCEPT>', [ source => '<ip>',] [ dest => '<ip>',] [ proto => '<tcp|udp>',] [ dport => <port>,], [ sport => <port>,] }
 
 The following <zone> definition exist currently:
 
-* host:              The host itself
-
-* outside:          The outside world (alias for 'vmbr0/eth0')
-
-* vm<ID>:           A qemu virtual machine
+ * host:              The host itself
+ * outside:           The outside world (alias for 'vmbr0/eth0')
+ * vm<ID>:            A qemu virtual machine
+ * ct<ID>:            An openvz container
+ * nfvm:              Non firewalled VM (alias for 'vmbr0/tapXYZ')
+ * vmbr<\d+>/<bport>: Unmanaged bridge port
 
-* ct<ID>:           An openvz container
 
-* nfvm:              Non firewalled VM (alias for 'vmbr0/tapXYZ')
-
-* vmbr<\d+>/<bport>: Unmanaged bridge port
-
-==Test examples==
+== Test examples ==
 
  { from => 'outside', to => 'ct200', dport => 22, action => 'ACCEPT' }
  { from => 'vm101', to => 'vm100', dport => 443, action => 'ACCEPT', id => 'vm2vm'}
@@ -51,4 +43,3 @@ The following <zone> definition exist currently:
 You can assign an 'id' to each test, so that you can run them separately:
 
  ./fwtester.pl -d test-basic1/tests vm2vm
-
index e9ed6d16bd2ade8c79168b58246181339becc37d..457f352100b09395dbe05813cd8cd91aca2c82f2 100755 (executable)
@@ -1,16 +1,19 @@
 #!/usr/bin/perl
 
 use lib '../src';
+
 use strict;
 use warnings;
+
 use Data::Dumper;
-use PVE::FirewallSimulator;
-use PVE::INotify;
-use PVE::Corosync;
-use Getopt::Long;
 use File::Basename;
+use Getopt::Long;
 use Net::IP;
 
+use PVE::Corosync;
+use PVE::FirewallSimulator;
+use PVE::INotify;
+
 my $debug = 0;
 
 sub print_usage_and_exit {
@@ -29,7 +32,7 @@ my $local_hostname = PVE::INotify::nodename();
 my $corosync_conf = PVE::Corosync::parse_conf($corosync_conf_fn, $raw_replaced);
 
 PVE::FirewallSimulator::debug($debug);
+
 my $testfilename = shift;
 my $testid = shift;
 
@@ -61,7 +64,7 @@ sub run_tests {
            die $@ if $@;
            next if defined($testid) && (!defined($test->{id}) || ($testid ne $test->{id}));
            PVE::FirewallSimulator::reset_trace();
-           print Dumper($ruleset) if $debug;
+           print Dumper($ruleset->{filter}) if $debug;
            $testcount++;
            eval {
                my @test_zones = qw(host outside nfvm vm100 ct200);
@@ -72,7 +75,7 @@ sub run_tests {
                        next if $zone eq $test->{from};
                        $test->{to} = $zone;
                        PVE::FirewallSimulator::add_trace("Set Zone: to => '$zone'\n"); 
-                       PVE::FirewallSimulator::simulate_firewall($ruleset, $ipset_ruleset, 
+                       PVE::FirewallSimulator::simulate_firewall($ruleset->{filter}, $ipset_ruleset, 
                                                                  $host_ip, $vmdata, $test);
                    }
                } elsif (!defined($test->{from})) {
@@ -80,24 +83,19 @@ sub run_tests {
                        next if $zone eq $test->{to};
                        $test->{from} = $zone;
                        PVE::FirewallSimulator::add_trace("Set Zone: from => '$zone'\n"); 
-                       PVE::FirewallSimulator::simulate_firewall($ruleset, $ipset_ruleset, 
+                       PVE::FirewallSimulator::simulate_firewall($ruleset->{filter}, $ipset_ruleset, 
                                                                  $host_ip, $vmdata, $test);
                    }
                } else {
-                   PVE::FirewallSimulator::simulate_firewall($ruleset, $ipset_ruleset, 
+                   PVE::FirewallSimulator::simulate_firewall($ruleset->{filter}, $ipset_ruleset, 
                                                              $host_ip, $vmdata, $test);
                }
            };
            if (my $err = $@) {
-
-               print Dumper($ruleset) if !$debug;
-
+               print Dumper($ruleset->{filter}) if !$debug;
                print PVE::FirewallSimulator::get_trace() . "\n" if !$debug;
-
                print "$filename line $.: $line";
-
                print "test failed: $err\n";
-
                exit(-1);
            }
        } else {
index c9d675e9e2ff2528870215df04ebe63474e1c95c..9dbe2f37fbfa9cd43137e35563f3126c0cb8d260 100644 (file)
@@ -5,4 +5,6 @@ enable: 1
 [RULES]
 
 IN ACCEPT -p tcp -dport 443
+IN ACCEPT -p icmp -dport 0
+IN ACCEPT -p icmp -dport host-unreachable
 OUT REJECT -p tcp -dport 81
index d575bbe806c322263d597237c5cb51eae0391f5d..a993e5d46ba12bcd6c32cac9f463ca89d9bdfe0c 100644 (file)
 { from => 'vm110', to => 'vm100', dport => 22, action => 'DROP' }
 { from => 'vm110', to => 'vm100', dport => 443, action => 'ACCEPT' }
 
+{ from => 'vm110', to => 'vm100', dport => 0, proto => 'icmp', action => 'ACCEPT' }
+{ from => 'vm110', to => 'vm100', dport => 'host-unreachable', proto => 'icmp', action => 'ACCEPT' }
+{ from => 'vm110', to => 'vm100', dport => 255, proto => 'icmpv6', action => 'DROP' }
+
 { from => 'outside', to => 'ct200', dport => 22, action => 'ACCEPT' }
 { from => 'outside', to => 'ct200', dport => 23, action => 'DROP' }
 { from => 'outside', to => 'vm100', dport => 22, action => 'DROP' }