├── debian ├── source │ └── format ├── dirs ├── docs ├── pve-firewall.triggers ├── pve-firewall.default ├── pve-firewall.logrotate ├── rules ├── pvefw-logger.service ├── example │ ├── host.fw │ ├── cluster.fw │ └── 100.fw ├── postinst ├── copyright ├── pve-firewall.service ├── control └── README ├── test ├── test-unconfigured │ ├── host.fw │ ├── 101.fw │ ├── 201.fw │ ├── cluster.fw │ └── tests ├── test-basic1 │ ├── cluster.fw │ ├── 200.fw │ ├── 100.fw │ ├── host.fw │ └── tests ├── test-errors1 │ ├── cluster.fw │ ├── 100.fw │ └── tests ├── test-errors2 │ ├── cluster.fw │ ├── 100.fw │ └── tests ├── test-errors3 │ ├── cluster.fw │ ├── 100.fw │ ├── host.fw │ └── tests ├── test-errors4 │ ├── cluster.fw │ ├── 100.fw │ └── tests ├── test-ipset2 │ ├── cluster.fw │ ├── tests │ └── 100.fw ├── test-vm-aliases1 │ ├── cluster.fw │ ├── 100.fw │ └── tests ├── test-default-rules1 │ ├── 101.fw │ ├── 201.fw │ ├── cluster.fw │ └── tests ├── test-vm-ipfilter2 │ ├── 200.fw │ ├── cluster.fw │ └── tests ├── test-group1 │ ├── host.fw │ ├── 200.fw │ ├── 100.fw │ ├── cluster.fw │ └── tests ├── test-vm-ipfilter1 │ ├── cluster.fw │ ├── 100.fw │ └── tests ├── test-ipset1 │ ├── host.fw │ ├── cluster.fw │ └── tests ├── Makefile ├── corosync.conf ├── README └── fwtester.pl ├── .gitignore ├── src ├── pve-firewall-sysctl.conf ├── PVE │ ├── API2 │ │ ├── Makefile │ │ └── Firewall │ │ │ ├── Makefile │ │ │ ├── Helpers.pm │ │ │ ├── Vnet.pm │ │ │ ├── Host.pm │ │ │ ├── Groups.pm │ │ │ ├── Cluster.pm │ │ │ ├── VM.pm │ │ │ ├── Aliases.pm │ │ │ ├── Rules.pm │ │ │ └── IPSet.pm │ ├── Firewall │ │ ├── Makefile │ │ └── Helpers.pm │ ├── Service │ │ ├── Makefile │ │ └── pve_firewall.pm │ ├── Makefile │ └── FirewallSimulator.pm ├── pve-firewall └── Makefile └── Makefile /debian/source/format: -------------------------------------------------------------------------------- 1 | 1.0 2 | -------------------------------------------------------------------------------- /test/test-unconfigured/host.fw: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | /var/lib/pve-firewall 2 | -------------------------------------------------------------------------------- /test/test-basic1/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 -------------------------------------------------------------------------------- /test/test-errors1/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 -------------------------------------------------------------------------------- /test/test-errors2/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 -------------------------------------------------------------------------------- /test/test-errors3/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 -------------------------------------------------------------------------------- /test/test-errors4/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 -------------------------------------------------------------------------------- /test/test-ipset2/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 -------------------------------------------------------------------------------- /test/test-unconfigured/101.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | -------------------------------------------------------------------------------- /test/test-unconfigured/201.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | -------------------------------------------------------------------------------- /test/test-vm-aliases1/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | debian/README 2 | debian/SOURCE 3 | debian/example/ 4 | -------------------------------------------------------------------------------- /debian/pve-firewall.triggers: -------------------------------------------------------------------------------- 1 | activate-noawait pve-api-updates 2 | -------------------------------------------------------------------------------- /test/test-default-rules1/101.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | -------------------------------------------------------------------------------- /test/test-default-rules1/201.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | -------------------------------------------------------------------------------- /test/test-unconfigured/cluster.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | -------------------------------------------------------------------------------- /test/test-vm-ipfilter2/200.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 4 | ipfilter: 1 5 | -------------------------------------------------------------------------------- /test/test-default-rules1/cluster.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | policy_out: DROP -------------------------------------------------------------------------------- /test/test-group1/host.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | GROUP group1 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.build 2 | /*.buildinfo 3 | /*.changes 4 | /*.deb 5 | /*.dsc 6 | /*.tar* 7 | /pve-firewall-*/ 8 | -------------------------------------------------------------------------------- /src/pve-firewall-sysctl.conf: -------------------------------------------------------------------------------- 1 | # Enables source route verification 2 | net.ipv4.conf.all.rp_filter = 2 3 | 4 | -------------------------------------------------------------------------------- /debian/pve-firewall.default: -------------------------------------------------------------------------------- 1 | # Should pve-firewall run automatically on startup? (default: yes) 2 | START_FIREWALL=yes -------------------------------------------------------------------------------- /test/test-errors3/100.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | IN ACCEPT -p tcp -dport 82 8 | 9 | -------------------------------------------------------------------------------- /test/test-vm-ipfilter2/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 4 | 5 | [rules] 6 | 7 | IN ACCEPT -p tcp -dport 80 8 | -------------------------------------------------------------------------------- /test/test-vm-ipfilter1/cluster.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 4 | 5 | [rules] 6 | 7 | IN ACCEPT -p tcp -dport 80 -source 1.2.3.0/24 -------------------------------------------------------------------------------- /test/test-errors3/host.fw: -------------------------------------------------------------------------------- 1 | [RULES] 2 | 3 | # rule with dport but missing protocol - should be ignored 4 | IN ACCEPT -dport 80 5 | 6 | -------------------------------------------------------------------------------- /test/test-basic1/200.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | IN ACCEPT -p tcp -dport 22 8 | OUT REJECT -p tcp -dport 81 9 | -------------------------------------------------------------------------------- /test/test-errors4/100.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | dafadfsdf: afda # unknown option - should be skipped 4 | 5 | enable: 1 6 | 7 | [rules] 8 | 9 | IN ACCEPT -p tcp -dport 80 -------------------------------------------------------------------------------- /test/test-errors4/tests: -------------------------------------------------------------------------------- 1 | { from => 'outside', to => 'vm100', dport => 82, action => 'DROP' } 2 | { from => 'outside', to => 'vm100', dport => 80, action => 'ACCEPT' } 3 | -------------------------------------------------------------------------------- /test/test-ipset1/host.fw: -------------------------------------------------------------------------------- 1 | 2 | 3 | [RULES] 4 | 5 | IN REJECT -source +myipset -dest +dmzhosts -p tcp -dport 22 6 | 7 | IN ACCEPT -source +myipset -p tcp -dport 22 8 | 9 | -------------------------------------------------------------------------------- /test/test-unconfigured/tests: -------------------------------------------------------------------------------- 1 | { to => 'ct200', action => 'ACCEPT' } 2 | { to => 'ct201', action => 'DROP' } 3 | { to => 'vm100', action => 'ACCEPT' } 4 | { to => 'vm101', action => 'DROP' } 5 | 6 | -------------------------------------------------------------------------------- /test/test-ipset2/tests: -------------------------------------------------------------------------------- 1 | { from => 'outside', to => 'vm100', dest => '1.2.3.4', dport => 80, action => 'ACCEPT' } 2 | 3 | { from => 'outside', to => 'vm100', dest => '1.2.3.4', dport => 22, action => 'DROP' } 4 | -------------------------------------------------------------------------------- /test/test-vm-aliases1/100.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [ALIASES] 6 | 7 | testip1 1.2.3.4 8 | testip2 1.2.3.5 9 | 10 | [RULES] 11 | 12 | OUT DROP -source testip1 13 | OUT DROP -dest testip2 14 | -------------------------------------------------------------------------------- /test/test-basic1/100.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | IN ACCEPT -p tcp -dport 443 8 | IN ACCEPT -p icmp -dport 0 9 | IN ACCEPT -p icmp -dport host-unreachable 10 | OUT REJECT -p tcp -dport 81 11 | -------------------------------------------------------------------------------- /test/test-errors1/100.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | # rule with dport but missing protocol - should be ignored 8 | IN ACCEPT -dport 80 9 | # correct rule 10 | IN ACCEPT -p tcp -dport 82 11 | 12 | -------------------------------------------------------------------------------- /test/test-errors2/100.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | IN ACCEPT -source non-existing-alias -dport 443 8 | IN ACCEPT -source +non-existing-ipset -dport 443 9 | IN ACCEPT -p tcp -dport 80 10 | 11 | -------------------------------------------------------------------------------- /test/test-vm-ipfilter1/100.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 4 | 5 | [ipset ipfilter-net0] 6 | 1.2.3.4 7 | 1.2.3.5 8 | 9 | [ipset ipfilter-net2] # empty, allow nothing 10 | 11 | [rules] 12 | 13 | IN ACCEPT -p tcp -dport 80 -------------------------------------------------------------------------------- /test/test-basic1/host.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | OUT REJECT -p tcp -dport 81 8 | IN ACCEPT -p tcp -dport 22 9 | IN REJECT -i vmbr0 -p tcp -dport 100 10 | IN REJECT -i vmbr1 -p tcp -dport 101 11 | -------------------------------------------------------------------------------- /test/test-errors2/tests: -------------------------------------------------------------------------------- 1 | { from => 'outside', to => 'vm100', dport => 443, action => 'DROP' } 2 | { from => 'outside', to => 'vm100', dport => 80, action => 'ACCEPT' } 3 | { from => 'outside', to => 'vm100', dport => 81, action => 'DROP' } -------------------------------------------------------------------------------- /test/test-errors1/tests: -------------------------------------------------------------------------------- 1 | { from => 'outside', to => 'vm100', dport => 80, action => 'DROP' } 2 | { from => 'outside', to => 'vm100', dport => 81, action => 'DROP' } 3 | { from => 'outside', to => 'vm100', dport => 82, action => 'ACCEPT' } 4 | -------------------------------------------------------------------------------- /test/test-errors3/tests: -------------------------------------------------------------------------------- 1 | { from => 'outside', to => 'host', dport => 80, action => 'DROP' } 2 | { from => 'outside', to => 'vm100', dport => 80, action => 'DROP' } 3 | { from => 'outside', to => 'vm100', dport => 82, action => 'ACCEPT' } 4 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: 3 | 4 | .PHONY: check 5 | check: 6 | ./fwtester.pl 7 | 8 | .PHONY: install 9 | install: check 10 | 11 | .PHONY: clean 12 | clean: 13 | rm -f *~ test-*/*~ 14 | 15 | .PHONY: distclean 16 | distclean: clean 17 | 18 | -------------------------------------------------------------------------------- /test/test-ipset2/100.fw: -------------------------------------------------------------------------------- 1 | [options] 2 | 3 | enable: 1 4 | 5 | [ipset a-longer-name-which-translates-into-fnv-digest] 6 | 1.2.3.4 7 | 1.2.3.5 8 | 9 | 10 | [rules] 11 | 12 | IN ACCEPT -p tcp -dport 80 -dest +a-longer-name-which-translates-into-fnv-digest -------------------------------------------------------------------------------- /test/test-group1/200.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | IN ACCEPT -source 192.168.2.1 -p tcp -dport 22 8 | IN ACCEPT -source 192.168.2.1 -p tcp -dport 80 9 | IN ACCEPT -source 127.0.0.1 -p tcp -dport 80 10 | 11 | GROUP group3 -i net0 -------------------------------------------------------------------------------- /test/test-vm-ipfilter2/tests: -------------------------------------------------------------------------------- 1 | { from => 'ct200', source => '1.2.3.4', dport => 80, action => 'DROP' } 2 | { from => 'ct200', source => '10.0.200.1', dport => 80, action => 'ACCEPT' } 3 | { from => 'ct200', source => '10.0.200.2', dport => 80, action => 'DROP' } 4 | -------------------------------------------------------------------------------- /src/PVE/API2/Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR= 2 | PREFIX=/usr 3 | PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5 4 | 5 | all: 6 | 7 | .PHONY: install 8 | install: 9 | install -d -m 0755 $(PERLDIR)/PVE/API2 10 | make -C Firewall install 11 | 12 | .PHONY: clean 13 | clean: 14 | rm -rf *~ 15 | make -C Firewall clean 16 | -------------------------------------------------------------------------------- /test/test-group1/100.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [RULES] 6 | 7 | IN ACCEPT -source 192.168.2.1 -p tcp -dport 22 8 | IN ACCEPT -source 192.168.2.1 -p tcp -dport 80 9 | IN ACCEPT -source 127.0.0.1 -p tcp -dport 80 10 | 11 | IN ACCEPT -i net0 -source 192.168.5.0/24 -p tcp -dport 22 12 | GROUP group2 -i net0 -------------------------------------------------------------------------------- /src/PVE/Firewall/Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR= 2 | PREFIX=/usr 3 | PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5 4 | 5 | SOURCES=Helpers.pm 6 | 7 | .PHONY: install 8 | install: $(SOURCES) 9 | install -d -m 0755 $(PERLDIR)/PVE/Firewall 10 | for i in $(SOURCES); do install -D -m 0644 $$i $(PERLDIR)/PVE/Firewall/$$i; done 11 | 12 | clean: 13 | -------------------------------------------------------------------------------- /src/PVE/Service/Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR= 2 | PREFIX=/usr 3 | PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5 4 | 5 | SOURCES=pve_firewall.pm 6 | 7 | .PHONY: install 8 | install: $(SOURCES) 9 | install -d -m 0755 $(PERLDIR)/PVE/Service 10 | for i in $(SOURCES); do install -D -m 0644 $$i $(PERLDIR)/PVE/Service/$$i; done 11 | 12 | clean: 13 | -------------------------------------------------------------------------------- /debian/pve-firewall.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/pve-firewall.log { 2 | rotate 7 3 | daily 4 | missingok 5 | notifempty 6 | delaycompress 7 | compress 8 | sharedscripts 9 | create 640 root adm 10 | postrotate 11 | invoke-rc.d pvefw-logger restart 2>/dev/null >/dev/null || true 12 | endscript 13 | } 14 | -------------------------------------------------------------------------------- /test/test-vm-aliases1/tests: -------------------------------------------------------------------------------- 1 | { from => 'vm100', to => 'outside', source => '1.2.3.4', action => 'DROP' } 2 | { from => 'vm100', to => 'outside', source => '1.2.3.5', action => 'ACCEPT' } 3 | { from => 'vm100', to => 'outside', dest => '1.2.3.4', action => 'ACCEPT' } 4 | { from => 'vm100', to => 'outside', dest => '1.2.3.5', action => 'DROP' } 5 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | # Uncomment this to turn on verbose mode. 4 | #export DH_VERBOSE=1 5 | 6 | %: 7 | dh $@ 8 | 9 | override_dh_installsystemd: 10 | dh_installsystemd --name pvefw-logger pvefw-logger.service 11 | dh_installsystemd --name pve-firewall --no-stop-on-upgrade --no-restart-after-upgrade pve-firewall.service 12 | 13 | override_dh_installinit: 14 | -------------------------------------------------------------------------------- /debian/pvefw-logger.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Proxmox VE firewall logger 3 | ConditionPathExists=/usr/sbin/pvefw-logger 4 | DefaultDependencies=no 5 | Before=shutdown.target 6 | After=local-fs.target 7 | Conflicts=shutdown.target 8 | 9 | [Service] 10 | ExecStart=/usr/sbin/pvefw-logger 11 | PIDFile=/run/pvefw-logger.pid 12 | TimeoutStopSec=5 13 | Type=forking 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /test/test-group1/cluster.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [GROUP group1] 6 | 7 | IN ACCEPT -source 192.168.2.0/24 -p tcp -dport 22 8 | IN REJECT -source 192.168.2.0/24 -p tcp -dport 80 9 | OUT REJECT -source 192.168.2.0/24 -p tcp -dport 80 10 | OUT REJECT -p tcp -dport 443 11 | 12 | [GROUP group2] 13 | 14 | IN ACCEPT -source 192.168.3.0/24 -p tcp -dport 22 15 | 16 | [GROUP group3] 17 | 18 | IN ACCEPT -source 192.168.6.0/24 -p tcp -dport 22 19 | -------------------------------------------------------------------------------- /test/test-ipset1/cluster.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | enable: 1 4 | 5 | [ALIASES] 6 | 7 | myserveralias 10.2.0.111 8 | mynetworkalias 10.3.0.0/24 9 | 10 | [ipset management] 11 | 12 | 192.168.128.2 13 | 14 | [ipset myipset] 15 | 16 | 192.168.0.1 17 | 172.16.0.10 18 | 192.168.1.0/24 19 | mynetworkalias 20 | myserveralias 21 | 22 | [ipset dmzhosts] 23 | 10.10.10.0/24 24 | 10.10.11.1 25 | 26 | #global ipset blacklist 27 | [ipset blacklist] 28 | 29 | 10.0.0.8 30 | 192.168.0.0/24 31 | 32 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR= 2 | PREFIX=/usr 3 | PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5 4 | 5 | LIB_SOURCES= \ 6 | Aliases.pm \ 7 | Helpers.pm \ 8 | IPSet.pm \ 9 | Rules.pm \ 10 | Cluster.pm \ 11 | Host.pm \ 12 | VM.pm \ 13 | Vnet.pm \ 14 | Groups.pm 15 | 16 | all: 17 | 18 | .PHONY: install 19 | install: 20 | install -d -m 0755 $(PERLDIR)/PVE/API2/Firewall 21 | for i in $(LIB_SOURCES); do install -D -m 0644 $$i $(PERLDIR)/PVE/API2/Firewall/$$i; done 22 | 23 | 24 | .PHONY: clean 25 | clean: 26 | rm -rf *~ 27 | -------------------------------------------------------------------------------- /src/PVE/Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR= 2 | PREFIX= /usr 3 | PERLDIR=$(DESTDIR)/$(PREFIX)/share/perl5 4 | 5 | LIB_SOURCES= \ 6 | FirewallSimulator.pm \ 7 | Firewall.pm 8 | 9 | all: 10 | 11 | .PHONY: install 12 | install: 13 | install -d -m 0755 $(PERLDIR)/PVE 14 | for i in $(LIB_SOURCES); do install -D -m 0644 $$i $(PERLDIR)/PVE/$$i; done 15 | make -C API2 install 16 | make -C Service install 17 | make -C Firewall install 18 | 19 | .PHONY: clean 20 | clean: 21 | rm -rf *~ 22 | make -C API2 clean 23 | make -C Service clean 24 | make -C Firewall clean 25 | -------------------------------------------------------------------------------- /debian/example/host.fw: -------------------------------------------------------------------------------- 1 | # /etc/pve/local/host.fw 2 | 3 | [OPTIONS] 4 | 5 | enable: 0 6 | tcp_flags_log_level: info 7 | smurf_log_level: nolog 8 | log_level_in: info 9 | log_level_out: info 10 | 11 | # allow more connections (default is 65536) 12 | nf_conntrack_max: 196608 13 | 14 | # reduce conntrack established timeout (default is 432000 - 5days) 15 | nf_conntrack_tcp_timeout_established: 7875 16 | 17 | # disable SMURFS filter 18 | nosmurfs: 0 19 | 20 | # filter illegal combinations of TCP flags 21 | tcpflags: 1 22 | 23 | [RULES] 24 | 25 | IN SSH(ACCEPT) 26 | OUT SSH(ACCEPT) 27 | -------------------------------------------------------------------------------- /src/pve-firewall: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | use PVE::SafeSyslog; 6 | use PVE::Service::pve_firewall; 7 | 8 | $SIG{'__WARN__'} = sub { 9 | my $err = $@; 10 | my $t = $_[0]; 11 | chomp $t; 12 | print STDERR "$t\n"; 13 | syslog('warning', "%s", $t); 14 | $@ = $err; 15 | }; 16 | 17 | my $prepare = sub { 18 | my $rpcenv = PVE::RPCEnvironment->init('cli'); 19 | 20 | $rpcenv->init_request(); 21 | $rpcenv->set_language($ENV{LANG}); 22 | $rpcenv->set_user('root@pam'); 23 | }; 24 | 25 | PVE::Service::pve_firewall->run_cli_handler(prepare => $prepare); 26 | -------------------------------------------------------------------------------- /debian/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | #DEBHELPER# 6 | 7 | case "$1" in 8 | configure) 9 | # modeled after dh_systemd_start output 10 | systemctl --system daemon-reload >/dev/null || true 11 | if [ -n "$2" ]; then 12 | _dh_action=try-reload-or-restart 13 | else 14 | _dh_action=start 15 | fi 16 | deb-systemd-invoke $_dh_action pve-firewall.service >/dev/null || true 17 | ;; 18 | 19 | abort-upgrade|abort-remove|abort-deconfigure) 20 | ;; 21 | 22 | *) 23 | echo "postinst called with unknown argument \`$1'" >&2 24 | exit 1 25 | ;; 26 | esac 27 | 28 | exit 0 29 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Proxmox Server Solutions GmbH 2 | 3 | This software is written by Proxmox Server Solutions GmbH 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU Affero General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU Affero General Public License for more details. 14 | 15 | You should have received a copy of the GNU Affero General Public License 16 | along with this program. If not, see . 17 | -------------------------------------------------------------------------------- /debian/pve-firewall.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Proxmox VE firewall 3 | ConditionPathExists=/usr/sbin/pve-firewall 4 | Wants=pve-cluster.service pvefw-logger.service 5 | After=pvefw-logger.service pve-cluster.service network.target systemd-modules-load.service 6 | DefaultDependencies=no 7 | Before=shutdown.target 8 | Conflicts=shutdown.target 9 | 10 | [Service] 11 | ExecStartPre=-/usr/bin/update-alternatives --set ebtables /usr/sbin/ebtables-legacy 12 | ExecStartPre=-/usr/bin/update-alternatives --set iptables /usr/sbin/iptables-legacy 13 | ExecStartPre=-/usr/bin/update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy 14 | ExecStart=/usr/sbin/pve-firewall start 15 | ExecStop=/usr/sbin/pve-firewall stop 16 | ExecReload=/usr/sbin/pve-firewall restart 17 | PIDFile=/run/pve-firewall.pid 18 | Type=forking 19 | 20 | [Install] 21 | WantedBy=multi-user.target 22 | -------------------------------------------------------------------------------- /test/test-vm-ipfilter1/tests: -------------------------------------------------------------------------------- 1 | { from => 'vm100i1', source => '1.2.3.3', dport => 80, action => 'ACCEPT' } 2 | { from => 'vm100i1', source => '1.2.3.4', dport => 80, action => 'ACCEPT' } 3 | { from => 'vm100i1', source => '1.2.3.5', dport => 80, action => 'ACCEPT' } 4 | { from => 'vm100i1', source => '1.2.3.6', dport => 80, action => 'ACCEPT' } 5 | 6 | 7 | { from => 'vm100', source => '1.2.3.3', dport => 80, action => 'DROP' } 8 | { from => 'vm100', source => '1.2.3.4', dport => 80, action => 'ACCEPT' } 9 | { from => 'vm100', source => '1.2.3.5', dport => 80, action => 'ACCEPT' } 10 | { from => 'vm100', source => '1.2.3.6', dport => 80, action => 'DROP' } 11 | 12 | { from => 'vm100i2', source => '1.2.3.3', dport => 80, action => 'DROP' } 13 | { from => 'vm100i2', source => '1.2.3.4', dport => 80, action => 'DROP' } 14 | { from => 'vm100i2', source => '1.2.3.5', dport => 80, action => 'DROP' } 15 | { from => 'vm100i2', source => '1.2.3.6', dport => 80, action => 'DROP' } 16 | -------------------------------------------------------------------------------- /test/corosync.conf: -------------------------------------------------------------------------------- 1 | logging { 2 | debug: off 3 | to_syslog: yes 4 | } 5 | 6 | nodelist { 7 | node { 8 | name: prox1 9 | nodeid: 1 10 | quorum_votes: 1 11 | ring0_addr: 172.16.1.11 12 | ring1_addr: 172.16.2.11 13 | ring2_addr: hostname1 14 | } 15 | node { 16 | name: prox2 17 | nodeid: 1 18 | quorum_votes: 1 19 | ring0_addr: 172.16.1.12 20 | ring1_addr: 172.16.2.12 21 | ring2_addr: hostname2 22 | } 23 | node { 24 | name: prox3 25 | nodeid: 1 26 | quorum_votes: 1 27 | ring0_addr: 172.16.1.3 28 | ring1_addr: 172.16.2.3 29 | ring2_addr: hostname3 30 | } 31 | node { 32 | name: proxself 33 | nodeid: 1 34 | quorum_votes: 1 35 | ring0_addr: 172.16.1.2 36 | ring1_addr: 172.16.2.2 37 | ring2_addr: proxself 38 | } 39 | } 40 | 41 | quorum { 42 | provider: corosync_votequorum 43 | } 44 | 45 | totem { 46 | cluster_name: cloud 47 | config_version: 1 48 | ip_version: ipv4 49 | secauth: on 50 | transport: udp 51 | version: 2 52 | } 53 | 54 | -------------------------------------------------------------------------------- /test/test-group1/tests: -------------------------------------------------------------------------------- 1 | { from => 'outside', to => 'vm100', source => '192.168.4.1', dport => 22, action => 'DROP' } 2 | { from => 'outside', to => 'vm100', source => '192.168.3.1', dport => 22, action => 'ACCEPT' } 3 | 4 | { from => 'host', source => '192.168.2.1', dport => 22, action => 'ACCEPT' } 5 | { from => 'host', source => '192.168.2.1', dport => 443, action => 'REJECT' } 6 | { from => 'host', source => '192.168.2.1', dport => 80, action => 'REJECT' } 7 | { from => 'host', source => '127.0.0.1', dport => 80, action => 'ACCEPT' } 8 | 9 | { to => 'host', source => '1.2.3.4', dport => 22, action => 'DROP' } 10 | { to => 'host', source => '192.168.2.1', dport => 22, action => 'ACCEPT' } 11 | { to => 'host', source => '192.168.2.1', dport => 80, action => 'REJECT' } 12 | 13 | { to => 'vm100', source => '192.168.3.1', dport => 22, action => 'ACCEPT' } 14 | { to => 'vm100', source => '192.168.4.1', dport => 22, action => 'DROP' } 15 | 16 | { from => 'outside', to => 'ct200', source => '192.168.6.1', dport => 22, action => 'ACCEPT' } 17 | { from => 'outside', to => 'ct200', source => '192.168.7.1', dport => 22, action => 'DROP' } 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: pve-firewall 2 | Section: admin 3 | Priority: optional 4 | Maintainer: Proxmox Support Team 5 | Build-Depends: debhelper-compat (= 13), 6 | libanyevent-perl, 7 | libglib2.0-dev, 8 | libnetfilter-conntrack-dev, 9 | libnetfilter-log-dev, 10 | libpve-access-control, 11 | libpve-cluster-perl, 12 | libpve-common-perl (>= 7.3-2), 13 | pve-cluster (>= 6.0-4), 14 | pve-doc-generator (>= 5.3-3), 15 | Standards-Version: 4.6.2 16 | 17 | Package: pve-firewall 18 | Architecture: any 19 | Conflicts: ulogd, 20 | Depends: conntrack, 21 | ebtables, 22 | ipset, 23 | iptables, 24 | libpve-access-control, 25 | libpve-cluster-perl, 26 | libpve-common-perl (>= 9.0.2), 27 | libpve-network-perl (>= 0.9.9~), 28 | libpve-rs-perl (>= 0.8.13), 29 | pve-cluster (>= 6.1-6), 30 | ${misc:Depends}, 31 | ${perl:Depends}, 32 | ${shlibs:Depends}, 33 | Description: Proxmox VE Firewall 34 | This package contains the Proxmox VE Firewall. 35 | -------------------------------------------------------------------------------- /debian/example/cluster.fw: -------------------------------------------------------------------------------- 1 | [OPTIONS] 2 | 3 | # enable firewall (cluster wide setting, default is disabled) 4 | enable: 1 5 | 6 | # default policy for host rules 7 | policy_in: DROP 8 | policy_out: ACCEPT 9 | 10 | [ALIASES] 11 | 12 | myserveralias 10.0.0.111 13 | mynetworkalias 10.0.0.0/24 14 | myserveraliasipv6 2001:db8:0:85a3:0:0:ac1f:8001 15 | myserveraliasipv6short 2001:db8:0:85a3::ac1f:8001 16 | 17 | 18 | [RULES] 19 | 20 | IN SSH(ACCEPT) -i vmbr0 21 | 22 | [group group1] 23 | 24 | IN ACCEPT -p tcp -dport 22 25 | OUT ACCEPT -p tcp -dport 80 26 | OUT ACCEPT -p icmp 27 | 28 | [group group3] 29 | 30 | IN ACCEPT -source 10.0.0.1 31 | IN ACCEPT -source 10.0.0.1-10.0.0.10 32 | IN ACCEPT -source 10.0.0.1,10.0.0.2,10.0.0.3 33 | IN ACCEPT -source +mynetgroup 34 | IN ACCEPT -source myserveralias 35 | IN ACCEPT -source myserveraliasipv6 36 | IN ACCEPT -source 2001:db8:0:85a3:0:0:ac1f:8001 37 | 38 | [ipset myipset] 39 | 40 | 192.168.0.1 #mycomment 41 | 172.16.0.10 42 | 192.168.0.0/24 43 | ! 10.0.0.0/8 #nomatch - needs kernel 3.7 or newer 44 | mynetworkalias 45 | 2001:db8:0:85a3::ac1f:8001 46 | 2001:db8:0:85a3:0:0:ac1f:8002 47 | 48 | #global ipset blacklist 49 | [ipset blacklist] 50 | 51 | 10.0.0.8 52 | 192.168.0.0/24 53 | 2001:db8:0:85a3:0:0:ac1f:8001 54 | -------------------------------------------------------------------------------- /test/test-ipset1/tests: -------------------------------------------------------------------------------- 1 | # blacklisted 2 | { from => 'outside', to => 'host', source => '192.168.0.1', dest => '1.2.3.4', dport => 22, action => 'DROP' } 3 | # accept in myipset 4 | { from => 'outside', to => 'host', source => '172.16.0.10', dest => '1.2.3.4', dport => 22, action => 'ACCEPT' } 5 | { from => 'outside', to => 'host', source => '192.168.1.10', dest => '1.2.3.4', dport => 22, action => 'ACCEPT' } 6 | # network alias inside myipset 7 | { from => 'outside', to => 'host', source => '10.3.0.1', dest => '1.2.3.4', dport => 22, action => 'ACCEPT' } 8 | # server alias inside myipset 9 | { from => 'outside', to => 'host', source => '10.2.0.111', dest => '1.2.3.4', dport => 22, action => 'ACCEPT' } 10 | 11 | # not inside myipset 12 | { from => 'outside', to => 'host', source => '10.2.0.112', dest => '1.2.3.4', dport => 22, action => 'DROP' } 13 | 14 | # reject dmzhosts if from myipset 15 | { from => 'outside', to => 'host', source => '172.16.0.10', dest => '10.10.10.1', dport => 22, action => 'REJECT' } 16 | { from => 'outside', to => 'host', source => '172.16.0.10', dest => '10.10.11.1', dport => 22, action => 'REJECT' } 17 | 18 | # management ipset 19 | { from => 'outside', to => 'host', source => '192.168.128.1', dport => 8006, action => 'DROP' } 20 | { from => 'outside', to => 'host', source => '192.168.128.1', dport => 22, action => 'DROP' } 21 | { from => 'outside', to => 'host', source => '192.168.128.2', dport => 8006, action => 'ACCEPT' } 22 | { from => 'outside', to => 'host', source => '192.168.128.2', dport => 22, action => 'ACCEPT' } 23 | 24 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | = A simple simulator to test our iptables rule generation = 2 | 3 | == Invocation == 4 | 5 | # ./fwtester.pl 6 | 7 | This scans for subdirectory named test-* an invokes fwtester.pl for each 8 | subdirectory with: 9 | 10 | # ./fwtester.pl test-/tests 11 | 12 | == Test directory contents == 13 | 14 | Each test directory can contain the following files: 15 | 16 | * cluster.fw Cluster wide firewall config 17 | * host.fw Host firewall config 18 | * .fw Firewall config for VMs 19 | * tests Test descriptions 20 | 21 | == Test description == 22 | 23 | The test description file can contain one or more tests using the following 24 | syntax: 25 | 26 | { from => '' , to => '', action => '', [ source => '',] [ dest => '',] [ proto => '',] [ dport => ,], [ sport => ,] } 27 | 28 | The following definition exist currently: 29 | 30 | * host: The host itself 31 | * outside: The outside world (alias for 'vmbr0/eth0') 32 | * vm: A qemu virtual machine 33 | * ct: An openvz container 34 | * nfvm: Non firewalled VM (alias for 'vmbr0/tapXYZ') 35 | * vmbr<\d+>/: Unmanaged bridge port 36 | 37 | 38 | == Test examples == 39 | 40 | { from => 'outside', to => 'ct200', dport => 22, action => 'ACCEPT' } 41 | { from => 'vm101', to => 'vm100', dport => 443, action => 'ACCEPT', id => 'vm2vm'} 42 | 43 | You can assign an 'id' to each test, so that you can run them separately: 44 | 45 | ./fwtester.pl -d test-basic1/tests vm2vm 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include /usr/share/dpkg/pkg-info.mk 2 | include /usr/share/dpkg/architecture.mk 3 | 4 | PACKAGE=pve-firewall 5 | 6 | BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION) 7 | GITVERSION:=$(shell git rev-parse HEAD) 8 | 9 | DEB=$(PACKAGE)_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb 10 | DSC=$(PACKAGE)_$(DEB_VERSION).dsc 11 | DEB2=$(PACKAGE)-dbgsym_$(DEB_VERSION)_$(DEB_HOST_ARCH).deb 12 | DEBS=$(DEB) $(DEB2) 13 | 14 | all: $(DEBS) 15 | 16 | .PHONY: tidy 17 | tidy: 18 | git ls-files ':*.p[ml]'| xargs -n4 -P0 proxmox-perltidy 19 | 20 | .PHONY: dinstall 21 | dinstall: $(DEB) 22 | dpkg -i $< 23 | 24 | $(BUILDDIR): 25 | rm -rf $(BUILDDIR) 26 | rsync -a src/ debian $(BUILDDIR) 27 | echo "git clone git://git.proxmox.com/git/pve-firewall.git\\ngit checkout $(GITVERSION)" > $(BUILDDIR)/debian/SOURCE 28 | 29 | .PHONY: deb 30 | deb: $(DEBS) 31 | $(DEB2): $(DEB) 32 | $(DEB): $(BUILDDIR) 33 | cd $(BUILDDIR); dpkg-buildpackage -b -us -uc 34 | lintian $(DEBS) 35 | 36 | .PHONY: dsc 37 | dsc: 38 | rm -rf $(DSC) $(BUILDDIR) 39 | $(MAKE) $(DSC) 40 | lintian $(DSC) 41 | 42 | $(DSC): $(BUILDDIR) 43 | cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d 44 | 45 | sbuild: $(DSC) 46 | sbuild $(DSC) 47 | 48 | check: 49 | make -C test check 50 | 51 | .PHONY: clean distclean 52 | distclean: clean 53 | clean: 54 | make -C src clean 55 | make -C test clean 56 | rm -rf *.deb *.dsc *.changes *.build *.buildinfo $(PACKAGE)-[0-9]*/ $(PACKAGE)*.tar* 57 | 58 | .PHONY: upload 59 | upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION) 60 | upload: $(DEBS) 61 | tar cf - $(DEBS) | ssh repoman@repo.proxmox.com -- upload --product pve --dist $(UPLOAD_DIST) --arch $(DEB_HOST_ARCH) 62 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/Helpers.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::Helpers; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use PVE::Cluster; 7 | use PVE::Network::SDN::Vnets; 8 | use PVE::RPCEnvironment; 9 | 10 | sub get_allowed_vnets { 11 | my $rpcenv = eval { PVE::RPCEnvironment::get() }; 12 | 13 | if ($@) { 14 | warn "could not initialize RPCEnvironment"; 15 | return {}; 16 | } 17 | 18 | my $authuser = $rpcenv->get_user(); 19 | 20 | my $vnets = PVE::Network::SDN::Vnets::config(1); 21 | my $privs = ['SDN.Audit', 'SDN.Allocate']; 22 | 23 | my $allowed_vnets = []; 24 | foreach my $vnet (sort keys %{ $vnets->{ids} }) { 25 | my $zone = $vnets->{ids}->{$vnet}->{zone}; 26 | next if !$rpcenv->check_any($authuser, "/sdn/zones/$zone/$vnet", $privs, 1); 27 | push @$allowed_vnets, $vnet; 28 | } 29 | 30 | return $allowed_vnets; 31 | } 32 | 33 | sub get_allowed_vms { 34 | my $rpcenv = eval { PVE::RPCEnvironment::get() }; 35 | 36 | if ($@) { 37 | warn "could not initialize RPCEnvironment"; 38 | return {}; 39 | } 40 | 41 | my $authuser = $rpcenv->get_user(); 42 | 43 | my $guests = PVE::Cluster::get_vmlist(); 44 | 45 | return [ 46 | grep { $rpcenv->check($authuser, "/vms/$_", ['VM.Audit'], 1) } 47 | sort keys $guests->{ids}->%* 48 | ]; 49 | } 50 | 51 | sub check_vnet_access { 52 | my ($vnetid, $privileges) = @_; 53 | 54 | my $vnet = PVE::Network::SDN::Vnets::get_vnet($vnetid, 1) 55 | or die "invalid vnet specified"; 56 | 57 | my $zoneid = $vnet->{zone}; 58 | 59 | my $rpcenv = PVE::RPCEnvironment::get(); 60 | my $authuser = $rpcenv->get_user(); 61 | 62 | $rpcenv->check_any($authuser, "/sdn/zones/$zoneid/$vnetid", $privileges); 63 | } 64 | 65 | 1; 66 | -------------------------------------------------------------------------------- /debian/example/100.fw: -------------------------------------------------------------------------------- 1 | # Example VM firewall configuration 2 | 3 | # VM specific firewall options 4 | [OPTIONS] 5 | 6 | # disable/enable the whole thing 7 | enable: 1 8 | 9 | # disable/enable MAC address filter 10 | macfilter: 0 11 | 12 | # limit layer2 specific protocols 13 | layer2_protocols: ARP,802_1Q,IPX,NetBEUI,PPP 14 | 15 | # default policy 16 | policy_in: DROP 17 | policy_out: REJECT 18 | 19 | # log dropped incoming connection 20 | log_level_in: info 21 | 22 | # disable log for outgoing connections 23 | log_level_out: nolog 24 | 25 | # enable DHCP 26 | dhcp: 1 27 | 28 | # enable ips 29 | ips: 1 30 | 31 | # specify nfqueue queues (optionnal) 32 | #ips_queues: 0 33 | ips_queues: 0:3 34 | 35 | [IPSET ipfilter-net0] # only allow specified IPs on net0 36 | 192.168.2.10 37 | 38 | [RULES] 39 | 40 | #TYPE ACTION [OPTIONS] 41 | # -i 42 | # -source 43 | # -dest 44 | # -p 45 | # -dport 46 | # -sport 47 | 48 | IN SSH(ACCEPT) -i net0 49 | IN SSH(ACCEPT) -i net0 # a comment 50 | IN SSH(ACCEPT) -i net0 -source 192.168.2.192 # only allow SSH from 192.168.2.192 51 | IN SSH(ACCEPT) -i net0 -source 10.0.0.1-10.0.0.10 #accept SSH for ip in range 10.0.0.1 to 10.0.0.10 52 | IN SSH(ACCEPT) -i net0 -source 10.0.0.1,10.0.0.2,10.0.0.3 #accept ssh for 10.0.0.1 or 10.0.0.2 or 10.0.0.3 53 | IN SSH(ACCEPT) -i net0 -source +mynetgroup #accept ssh for ipset mynetgroup 54 | IN SSH(ACCEPT) -i net0 -source myserveralias #accept ssh for alias myserveralias 55 | IN SSH(ACCEPT) -i net0 -source FE80:0000:0000:0000:0202:B3FF:FE1E:8329 56 | IN ACCEPT -i net0 -p icmpv6 57 | 58 | |IN SSH(ACCEPT) -i net0 # disabled rule 59 | 60 | # add a security group 61 | GROUP group1 -i net0 62 | 63 | OUT DNS(ACCEPT) -i net0 64 | OUT Ping(ACCEPT) -i net0 65 | OUT SSH(ACCEPT) 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR= 2 | PREFIX= /usr 3 | BINDIR=$(DESTDIR)/$(PREFIX)/bin 4 | SBINDIR=$(DESTDIR)/$(PREFIX)/sbin 5 | MANDIR=$(DESTDIR)/$(PREFIX)/share/man 6 | DOCDIR=$(DESTDIR)/$(PREFIX)/share/doc/pve-firewall 7 | MAN1DIR=$(MANDIR)/man1/ 8 | MAN8DIR=$(MANDIR)/man8/ 9 | BASHCOMPLDIR=$(DESTDIR)/$(PREFIX)/share/bash-completion/completions 10 | ZSHCOMPLDIR=$(DESTDIR)/$(PREFIX)/share/zsh/vendor-completions 11 | 12 | -include /usr/share/pve-doc-generator/pve-doc-generator.mk 13 | 14 | all: pve-firewall.8 pvefw-logger 15 | 16 | pve-firewall.bash-completion: PVE/Service/pve_firewall.pm 17 | perl -I. -T -e "use PVE::Service::pve_firewall; PVE::Service::pve_firewall->generate_bash_completions();" >$@.tmp 18 | mv $@.tmp $@ 19 | 20 | pve-firewall.zsh-completion: PVE/Service/pve_firewall.pm 21 | perl -I. -T -e "use PVE::Service::pve_firewall; PVE::Service::pve_firewall->generate_zsh_completions();" >$@.tmp 22 | mv $@.tmp $@ 23 | 24 | CFLAGS:=$(shell dpkg-buildflags --get CFLAGS) 25 | CFLAGS+=$(shell pkg-config libnetfilter_log libnetfilter_conntrack glib-2.0 --libs --cflags) 26 | LDFLAGS:=$(shell dpkg-buildflags --get LDFLAGS) 27 | 28 | pvefw-logger: pvefw-logger.c 29 | gcc -Wall -Werror pvefw-logger.c -o pvefw-logger -std=gnu99 $(CFLAGS) $(LDFLAGS) 30 | 31 | .PHONY: install 32 | install: pve-firewall pve-firewall.8 pve-firewall.bash-completion pve-firewall.zsh-completion pvefw-logger 33 | make -C PVE install 34 | install -d -m 0755 $(SBINDIR) 35 | install -m 0755 pve-firewall $(SBINDIR) 36 | install -m 0755 pvefw-logger $(SBINDIR) 37 | install -d $(MAN8DIR) 38 | install -m 0644 pve-firewall.8 $(MAN8DIR) 39 | install -m 0644 -D pve-firewall.bash-completion $(BASHCOMPLDIR)/pve-firewall 40 | install -m 0644 -D pve-firewall.zsh-completion $(ZSHCOMPLDIR)/_pve-firewall 41 | install -d -m 0755 $(DESTDIR)/usr/lib/sysctl.d/ 42 | install -m 0644 pve-firewall-sysctl.conf $(DESTDIR)/usr/lib/sysctl.d/pve-firewall.conf 43 | 44 | .PHONY: clean 45 | clean: 46 | make -C PVE clean 47 | rm -f *.xml.tmp *.1 *.5 *.8 *{synopsis,opts}.adoc docinfo.xml *~ 48 | rm -rf pvefw-logger 49 | 50 | 51 | .PHONY: distclean 52 | distclean: clean 53 | -------------------------------------------------------------------------------- /debian/README: -------------------------------------------------------------------------------- 1 | Experimental software, only used for testing! 2 | ============================================= 3 | 4 | 5 | Quick Intro 6 | =========== 7 | 8 | VM firewall rules are read from: 9 | 10 | /etc/pve/firewall/.fw 11 | 12 | Cluster wide rules and security group are read from: 13 | 14 | /etc/pve/firewall/cluster.fw 15 | 16 | Host firewall rules are read from: 17 | 18 | /etc/pve/local/host.fw 19 | 20 | You can find examples in the example/ dir 21 | 22 | 23 | Use the following command to mange the firewall: 24 | 25 | To test the firewall configuration: 26 | 27 | ./pvefw compile 28 | 29 | To start or update the firewall: 30 | 31 | ./pvefw start 32 | 33 | To update the firewall rules (the firewall is not started if it 34 | is not already running): 35 | 36 | ./pvefw update 37 | 38 | To stop the firewall: 39 | 40 | ./pvefw stop 41 | 42 | 43 | Implementation details 44 | ====================== 45 | 46 | We write iptables rules directly, an generate the following chains 47 | as entry points in the 'forward' table: 48 | 49 | PVEFW-INPUT 50 | PVEFW-OUTPUT 51 | PVEFW-FORWARD 52 | 53 | We do not touch other (user defined) chains. 54 | 55 | Each VM can have its own firewall definition file in 56 | 57 | /etc/pve/firewall/.fw 58 | 59 | That file has a section [RULES] to define firewall rules. 60 | 61 | Format is: TYPE ACTION IFACE SOURCE DEST PROTO D-PORT S-PORT 62 | 63 | * TYPE: IN|OUT|GROUP 64 | * ACTION: action or macro 65 | * IFACE: vm network interface (net0 - net5), or '-' for all interfaces 66 | * SOURCE: source IP address, or '-' for any source 67 | * DEST: dest IP address, or '-' for any destination address 68 | * PROTO: see /etc/protocols 69 | * D-PORT: destination port 70 | * S-PORT: source port 71 | 72 | A rule for inbound traffic looks like this: 73 | 74 | IN SSH(ACCEPT) net0 75 | 76 | Outbound rules looks like: 77 | 78 | OUT SSH(ACCEPT) 79 | 80 | Problems 81 | =================== 82 | 83 | There are a number of restrictions when using iptables to filter 84 | bridged traffic. The physdev match feature does not work correctly 85 | when traffic is routed from host to bridge: 86 | 87 | * when a packet being sent through a bridge entered the firewall on 88 | another interface and was being forwarded to the bridge. 89 | 90 | * when a packet originating on the firewall itself is being sent through 91 | a bridge. 92 | 93 | We use a second bridge for each interface to avoid above problem. 94 | 95 | eth0-->vmbr0<--tapXiY (non firewalled tap) 96 | <--linkXiY-->linkXiYp-->fwbrXiY-->tapXiY (firewalled tap) 97 | -------------------------------------------------------------------------------- /test/test-basic1/tests: -------------------------------------------------------------------------------- 1 | { from => 'ct200', to => 'host', dport => 22, action => 'ACCEPT' } 2 | { from => 'ct200', to => 'host', dport => 23, action => 'DROP' } 3 | 4 | { from => 'vm100', to => 'host', dport => 22, action => 'ACCEPT' } 5 | 6 | { from => 'host' , to => 'ct200', dport => 80, action => 'DROP' } 7 | { from => 'host' , to => 'ct200', dport => 22, action => 'ACCEPT' } 8 | 9 | { from => 'host' , to => 'vm100', dport => 80, action => 'DROP' } 10 | 11 | { from => 'ct200' , to => 'vm100', dport => 80, action => 'DROP' } 12 | 13 | { from => 'vm100' , to => 'ct200', dport => 22, action => 'ACCEPT' } 14 | 15 | { from => 'vm101', to => 'vm100', dport => 22, action => 'DROP' } 16 | { from => 'vm101', to => 'vm100', dport => 443, action => 'ACCEPT', id => 'vm2vm'} 17 | 18 | { from => 'ct201', to => 'ct200', dport => 22, action => 'ACCEPT' } 19 | { from => 'ct201', to => 'ct200', dport => 23, action => 'DROP' } 20 | 21 | { from => 'vm110', to => 'vm100', dport => 22, action => 'DROP' } 22 | { from => 'vm110', to => 'vm100', dport => 443, action => 'ACCEPT' } 23 | 24 | { from => 'vm110', to => 'vm100', dport => 0, proto => 'icmp', action => 'ACCEPT' } 25 | { from => 'vm110', to => 'vm100', dport => 'host-unreachable', proto => 'icmp', action => 'ACCEPT' } 26 | { from => 'vm110', to => 'vm100', dport => 255, proto => 'icmpv6', action => 'DROP' } 27 | 28 | { from => 'outside', to => 'ct200', dport => 22, action => 'ACCEPT' } 29 | { from => 'outside', to => 'ct200', dport => 23, action => 'DROP' } 30 | { from => 'outside', to => 'vm100', dport => 22, action => 'DROP' } 31 | { from => 'outside', to => 'vm100', dport => 443, action => 'ACCEPT' } 32 | { from => 'outside', to => 'host', dport => 22, action => 'ACCEPT' } 33 | { from => 'outside', to => 'host', dport => 23, action => 'DROP' } 34 | 35 | { from => 'host' , to => 'outside', dport => 80, action => 'ACCEPT'} 36 | { from => 'host' , to => 'outside', dport => 81, action => 'REJECT' } 37 | { from => 'vm100' , to => 'outside', dport => 80, action => 'ACCEPT' } 38 | { from => 'vm100' , to => 'outside', dport => 81, action => 'REJECT' } 39 | { from => 'ct200' , to => 'outside', dport => 80, action => 'ACCEPT' } 40 | { from => 'ct200' , to => 'outside', dport => 81, action => 'REJECT' } 41 | 42 | { from => 'outside', to => 'host', dport => 100, action => 'REJECT' } 43 | { from => 'outside', to => 'host', dport => 101, action => 'DROP' } 44 | 45 | { from => 'nfvm', to => 'host', dport => 22, action => 'ACCEPT' } 46 | { from => 'nfvm', to => 'host', dport => 80, action => 'DROP' } 47 | { from => 'nfvm', to => 'outside', dport => 22, action => 'ACCEPT' } 48 | { from => 'nfvm', to => 'outside', dport => 80, action => 'ACCEPT' } 49 | { from => 'nfvm', to => 'vm100', dport => 443, action => 'ACCEPT', id => 'nfw2vm'} 50 | { from => 'nfvm', to => 'vm100', dport => 80, action => 'DROP' } 51 | { from => 'nfvm', to => 'ct200', dport => 22, action => 'ACCEPT' } 52 | { from => 'nfvm', to => 'ct200', dport => 80, action => 'DROP' } 53 | 54 | { from => 'ct200', to => 'nfvm', dport => 80, action => 'ACCEPT' } 55 | { from => 'vm100', to => 'nfvm', dport => 80, action => 'ACCEPT' } 56 | { from => 'outside', to => 'nfvm', dport => 80, action => 'ACCEPT' } 57 | { from => 'host', to => 'nfvm', dport => 80, action => 'ACCEPT' } 58 | 59 | { from => 'vmbr0/eth0', to => 'host', dport => 22, action => 'ACCEPT' } 60 | { from => 'host' , to => 'vmbr0/eth0', dport => 22, action => 'ACCEPT' } 61 | -------------------------------------------------------------------------------- /test/test-default-rules1/tests: -------------------------------------------------------------------------------- 1 | { from => 'outside', to => 'host', action => 'DROP' } 2 | { from => 'host', to => 'outside', action => 'DROP' } 3 | 4 | # traffic to other node 5 | { from => 'host', to => 'outside', dest => '172.16.1.3', dport => 21, action => 'DROP' } 6 | { from => 'host', to => 'outside', dest => '172.16.1.3', dport => 22, action => 'ACCEPT' } 7 | { from => 'host', to => 'outside', dest => '172.16.1.3', dport => 3128, action => 'ACCEPT' } 8 | { from => 'host', to => 'outside', dest => '172.16.1.3', dport => 8006, action => 'ACCEPT' } 9 | { from => 'host', to => 'outside', dest => '172.16.1.3', dport => 5900, action => 'ACCEPT' } 10 | { from => 'host', to => 'outside', dest => '172.16.1.3', dport => 5999, action => 'ACCEPT' } 11 | { from => 'host', to => 'outside', dest => '172.16.1.3', dport => 6000, action => 'DROP' } 12 | { from => 'host', to => 'outside', dest => '172.16.1.3', proto => 'udp', dport => 5404, action => 'ACCEPT' } 13 | { from => 'host', to => 'outside', dest => '172.16.1.3', proto => 'udp', dport => 5405, action => 'ACCEPT' } 14 | { from => 'host', to => 'outside', dest => '172.16.1.3', proto => 'udp', dport => 5406, action => 'DROP' } 15 | { from => 'host', to => 'outside', dest => '239.192.158.83', proto => 'udp', dport => 5404, dsttype => 'UNICAST', action => 'DROP' } 16 | { from => 'host', to => 'outside', dest => '239.192.158.83', proto => 'udp', dport => 5404, dsttype => 'MULTICAST', action => 'ACCEPT' } 17 | { from => 'host', to => 'outside', source => '172.16.2.2', dest => '172.16.2.3', proto => 'udp', dport => 5404, action => 'ACCEPT' } 18 | { from => 'host', to => 'outside', dest => '172.16.2.3', proto => 'udp', dport => 5404, action => 'DROP' } 19 | 20 | 21 | # traffic from other node 22 | 23 | { from => 'outside', to => 'host', source => '172.16.1.3', dport => 21, action => 'DROP' } 24 | { from => 'outside', to => 'host', source => '172.16.1.3', dport => 22, action => 'ACCEPT' } 25 | { from => 'outside', to => 'host', source => '172.16.1.3', dport => 3128, action => 'ACCEPT' } 26 | { from => 'outside', to => 'host', source => '172.16.1.3', dport => 8006, action => 'ACCEPT' } 27 | { from => 'outside', to => 'host', source => '172.16.1.3', dport => 5900, action => 'ACCEPT' } 28 | { from => 'outside', to => 'host', source => '172.16.1.3', dport => 5999, action => 'ACCEPT' } 29 | { from => 'outside', to => 'host', source => '172.16.1.3', dport => 6000, action => 'DROP' } 30 | { from => 'outside', to => 'host', source => '172.16.1.3', proto => 'udp', dport => 5404, action => 'ACCEPT' } 31 | { from => 'outside', to => 'host', source => '172.16.1.3', proto => 'udp', dport => 5405, action => 'ACCEPT' } 32 | { from => 'outside', to => 'host', source => '172.16.1.3', proto => 'udp', dport => 5406, action => 'DROP' } 33 | { from => 'outside', to => 'host', source => '172.16.1.3', dest => '239.192.158.83', proto => 'udp', dport => 5404, dsttype => 'UNICAST', action => 'DROP' } 34 | { from => 'outside', to => 'host', source => '172.16.1.3', dest => '239.192.158.83', proto => 'udp', dport => 5404, dsttype => 'MULTICAST', action => 'ACCEPT' } 35 | { from => 'outside', to => 'host', source => '172.16.2.11', dest => '172.16.2.2', proto => 'udp', dport => 5404, action => 'ACCEPT' } 36 | { from => 'outside', to => 'host', source => '172.16.2.11', dest => '172.16.1.2', proto => 'udp', dport => 5404, action => 'DROP' } 37 | 38 | 39 | { from => 'host', to => 'ct200', action => 'DROP' } 40 | { from => 'outside', to => 'ct200', action => 'ACCEPT' } 41 | { to => 'ct201', action => 'DROP' } 42 | { from => 'host', to => 'vm100', action => 'DROP' } 43 | { from => 'outside', to => 'vm100', action => 'ACCEPT' } 44 | { to => 'vm101', action => 'DROP' } 45 | 46 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/Vnet.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::Vnet; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Storable qw(dclone); 7 | 8 | use PVE::Exception qw(raise_param_exc); 9 | use PVE::JSONSchema qw(get_standard_option); 10 | use PVE::RPCEnvironment; 11 | 12 | use PVE::Firewall; 13 | use PVE::API2::Firewall::Rules; 14 | use PVE::API2::Firewall::Helpers; 15 | 16 | use base qw(PVE::RESTHandler); 17 | 18 | __PACKAGE__->register_method({ 19 | subclass => "PVE::API2::Firewall::VnetRules", 20 | path => 'rules', 21 | }); 22 | 23 | __PACKAGE__->register_method({ 24 | name => 'index', 25 | path => '', 26 | method => 'GET', 27 | description => "Directory index.", 28 | parameters => { 29 | additionalProperties => 0, 30 | properties => { 31 | vnet => get_standard_option('pve-sdn-vnet-id'), 32 | }, 33 | }, 34 | returns => { 35 | type => 'array', 36 | items => { 37 | type => "object", 38 | properties => {}, 39 | }, 40 | links => [{ rel => 'child', href => "{name}" }], 41 | }, 42 | code => sub { 43 | my ($param) = @_; 44 | 45 | my $result = [ 46 | { name => 'rules' }, { name => 'options' }, 47 | ]; 48 | 49 | return $result; 50 | }, 51 | }); 52 | 53 | my $option_properties = dclone($PVE::Firewall::vnet_option_properties); 54 | 55 | my sub add_option_properties { 56 | my ($properties) = @_; 57 | 58 | foreach my $k (keys %$option_properties) { 59 | $properties->{$k} = $option_properties->{$k}; 60 | } 61 | 62 | return $properties; 63 | } 64 | 65 | __PACKAGE__->register_method({ 66 | name => 'get_options', 67 | path => 'options', 68 | method => 'GET', 69 | description => "Get vnet firewall options.", 70 | permissions => { 71 | description => 72 | "Needs SDN.Audit or SDN.Allocate permissions on '/sdn/zones//'", 73 | user => 'all', 74 | }, 75 | parameters => { 76 | additionalProperties => 0, 77 | properties => { 78 | vnet => get_standard_option('pve-sdn-vnet-id'), 79 | }, 80 | }, 81 | returns => { 82 | type => "object", 83 | properties => $option_properties, 84 | }, 85 | code => sub { 86 | my ($param) = @_; 87 | 88 | PVE::API2::Firewall::Helpers::check_vnet_access( 89 | $param->{vnet}, 90 | ['SDN.Allocate', 'SDN.Audit'], 91 | ); 92 | 93 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 94 | my $vnetfw_conf = 95 | PVE::Firewall::load_vnetfw_conf($cluster_conf, 'vnet', $param->{vnet}); 96 | 97 | return PVE::Firewall::copy_opject_with_digest($vnetfw_conf->{options}); 98 | }, 99 | }); 100 | 101 | __PACKAGE__->register_method({ 102 | name => 'set_options', 103 | path => 'options', 104 | method => 'PUT', 105 | description => "Set Firewall options.", 106 | protected => 1, 107 | permissions => { 108 | description => "Needs SDN.Allocate permissions on '/sdn/zones//'", 109 | user => 'all', 110 | }, 111 | parameters => { 112 | additionalProperties => 0, 113 | properties => add_option_properties({ 114 | vnet => get_standard_option('pve-sdn-vnet-id'), 115 | delete => { 116 | type => 'string', 117 | format => 'pve-configid-list', 118 | description => "A list of settings you want to delete.", 119 | optional => 1, 120 | }, 121 | digest => get_standard_option('pve-config-digest'), 122 | }), 123 | }, 124 | returns => { type => "null" }, 125 | code => sub { 126 | my ($param) = @_; 127 | 128 | PVE::API2::Firewall::Helpers::check_vnet_access($param->{vnet}, ['SDN.Allocate']); 129 | 130 | PVE::Firewall::lock_vnetfw_conf( 131 | $param->{vnet}, 132 | 10, 133 | sub { 134 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 135 | my $vnetfw_conf = 136 | PVE::Firewall::load_vnetfw_conf($cluster_conf, 'vnet', $param->{vnet}); 137 | 138 | my (undef, $digest) = 139 | PVE::Firewall::copy_opject_with_digest($vnetfw_conf->{options}); 140 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 141 | 142 | if ($param->{delete}) { 143 | for my $opt (PVE::Tools::split_list($param->{delete})) { 144 | raise_param_exc({ delete => "no such option '$opt'" }) 145 | if !$option_properties->{$opt}; 146 | delete $vnetfw_conf->{options}->{$opt}; 147 | } 148 | } 149 | 150 | if (defined($param->{enable})) { 151 | $param->{enable} = $param->{enable} ? 1 : 0; 152 | } 153 | 154 | for my $k (keys %$option_properties) { 155 | next if !defined($param->{$k}); 156 | $vnetfw_conf->{options}->{$k} = $param->{$k}; 157 | } 158 | 159 | PVE::Firewall::save_vnetfw_conf($param->{vnet}, $vnetfw_conf); 160 | }, 161 | ); 162 | 163 | return undef; 164 | }, 165 | }); 166 | 167 | 1; 168 | -------------------------------------------------------------------------------- /test/fwtester.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use lib '../src'; 4 | 5 | use strict; 6 | use warnings; 7 | 8 | use Data::Dumper; 9 | use File::Basename; 10 | use Getopt::Long; 11 | use Net::IP; 12 | 13 | use PVE::Corosync; 14 | use PVE::FirewallSimulator; 15 | use PVE::INotify; 16 | 17 | my $debug = 0; 18 | 19 | sub print_usage_and_exit { 20 | die "usage: $0 [--debug] [testfile [testid]]\n"; 21 | } 22 | 23 | if (!GetOptions('debug' => \$debug)) { 24 | print_usage_and_exit(); 25 | } 26 | 27 | # load dummy corosync config to have fw create according rules 28 | my $corosync_conf_fn = "corosync.conf"; 29 | my $raw = PVE::Tools::file_get_contents($corosync_conf_fn); 30 | my $local_hostname = PVE::INotify::nodename(); 31 | (my $raw_replaced = $raw) =~ s/proxself$/$local_hostname\n/gm; 32 | my $corosync_conf = PVE::Corosync::parse_conf($corosync_conf_fn, $raw_replaced); 33 | 34 | PVE::FirewallSimulator::debug($debug); 35 | 36 | my $testfilename = shift; 37 | my $testid = shift; 38 | 39 | sub run_tests { 40 | my ($vmdata, $testdir, $testfile, $testid) = @_; 41 | 42 | $testfile = 'tests' if !$testfile; 43 | 44 | $vmdata->{testdir} = $testdir; 45 | 46 | my $host_ip = '172.16.1.2'; 47 | 48 | PVE::Firewall::local_network('172.16.1.0/24'); 49 | 50 | my ($ruleset, $ipset_ruleset) = PVE::Firewall::compile(undef, undef, $vmdata, $corosync_conf); 51 | 52 | my $filename = "$testdir/$testfile"; 53 | my $fh = IO::File->new($filename) 54 | || die "unable to open '$filename' - $!\n"; 55 | 56 | my $testcount = 0; 57 | while (defined(my $line = <$fh>)) { 58 | next if $line =~ m/^\s*$/; 59 | next if $line =~ m/^#.*$/; 60 | if ($line =~ m/^\{.*\}\s*$/) { 61 | my $test = eval $line; 62 | die $@ if $@; 63 | next if defined($testid) && (!defined($test->{id}) || ($testid ne $test->{id})); 64 | PVE::FirewallSimulator::reset_trace(); 65 | print Dumper($ruleset->{filter}) if $debug; 66 | $testcount++; 67 | eval { 68 | my @test_zones = qw(host outside nfvm vm100 ct200); 69 | if (!defined($test->{from}) && !defined($test->{to})) { 70 | die "missing zone speification (from, to)\n"; 71 | } elsif (!defined($test->{to})) { 72 | foreach my $zone (@test_zones) { 73 | next if $zone eq $test->{from}; 74 | $test->{to} = $zone; 75 | PVE::FirewallSimulator::add_trace("Set Zone: to => '$zone'\n"); 76 | PVE::FirewallSimulator::simulate_firewall( 77 | $ruleset->{filter}, $ipset_ruleset, $host_ip, $vmdata, $test, 78 | ); 79 | } 80 | } elsif (!defined($test->{from})) { 81 | foreach my $zone (@test_zones) { 82 | next if $zone eq $test->{to}; 83 | $test->{from} = $zone; 84 | PVE::FirewallSimulator::add_trace("Set Zone: from => '$zone'\n"); 85 | PVE::FirewallSimulator::simulate_firewall( 86 | $ruleset->{filter}, $ipset_ruleset, $host_ip, $vmdata, $test, 87 | ); 88 | } 89 | } else { 90 | PVE::FirewallSimulator::simulate_firewall( 91 | $ruleset->{filter}, $ipset_ruleset, $host_ip, $vmdata, $test, 92 | ); 93 | } 94 | }; 95 | if (my $err = $@) { 96 | print Dumper($ruleset->{filter}) if !$debug; 97 | print PVE::FirewallSimulator::get_trace() . "\n" if !$debug; 98 | print "$filename line $.: $line"; 99 | print "test failed: $err\n"; 100 | exit(-1); 101 | } 102 | } else { 103 | die "parse error"; 104 | } 105 | } 106 | 107 | die "no tests found\n" if $testcount <= 0; 108 | 109 | print "PASS: $filename\n"; 110 | 111 | return undef; 112 | } 113 | 114 | my $vmdata = { 115 | qemu => { 116 | 100 => { 117 | net0 => "e1000=0E:0B:38:B8:B3:21,bridge=vmbr0,firewall=1", 118 | net1 => "e1000=0E:0B:38:B9:B4:21,bridge=vmbr1,firewall=1", 119 | net2 => "e1000=0E:0B:38:BA:B4:21,bridge=vmbr2,firewall=1", 120 | }, 121 | 101 => { 122 | net0 => "e1000=0E:0B:38:B8:B3:22,bridge=vmbr0,firewall=1", 123 | }, 124 | # on bridge vmbr1 125 | 110 => { 126 | net0 => "e1000=0E:0B:38:B8:B4:21,bridge=vmbr1,firewall=1", 127 | }, 128 | }, 129 | lxc => { 130 | 200 => { 131 | net0 => 132 | "name=eth0,hwaddr=0E:18:24:41:2C:43,bridge=vmbr0,firewall=1,ip=10.0.200.1/24", 133 | }, 134 | 201 => { 135 | net0 => 136 | "name=eth0,hwaddr=0E:18:24:41:2C:44,bridge=vmbr0,firewall=1,ip=10.0.200.2/24", 137 | }, 138 | }, 139 | }; 140 | 141 | if ($testfilename) { 142 | my $testfile; 143 | my $dir; 144 | 145 | if (-d $testfilename) { 146 | $dir = $testfilename; 147 | } elsif (-f $testfilename) { 148 | $dir = dirname($testfilename); 149 | $testfile = basename($testfilename); 150 | } else { 151 | die "no such file/dir '$testfilename'\n"; 152 | } 153 | 154 | run_tests($vmdata, $dir, $testfile, $testid); 155 | 156 | } else { 157 | foreach my $dir () { 158 | next if !-d $dir; 159 | run_tests($vmdata, $dir); 160 | } 161 | } 162 | 163 | print "OK - all tests passed\n"; 164 | 165 | exit(0); 166 | -------------------------------------------------------------------------------- /src/PVE/Firewall/Helpers.pm: -------------------------------------------------------------------------------- 1 | package PVE::Firewall::Helpers; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Date::Parse qw(str2time); 7 | use Errno qw(ENOENT); 8 | use File::Basename qw(fileparse); 9 | use IO::Zlib; 10 | use PVE::Cluster; 11 | use PVE::Network; 12 | use PVE::Tools qw(file_get_contents file_set_contents); 13 | 14 | use base 'Exporter'; 15 | our @EXPORT_OK = qw( 16 | lock_vmfw_conf 17 | remove_vmfw_conf 18 | clone_vmfw_conf 19 | collect_refs 20 | flush_fw_ct_entries_by_mark 21 | ); 22 | 23 | my $pvefw_conf_dir = "/etc/pve/firewall"; 24 | 25 | sub lock_vmfw_conf { 26 | my ($vmid, $timeout, $code, @param) = @_; 27 | 28 | die "can't lock VM firewall config for undefined VMID\n" 29 | if !defined($vmid); 30 | 31 | my $res = PVE::Cluster::cfs_lock_firewall("vm-$vmid", $timeout, $code, @param); 32 | die $@ if $@; 33 | 34 | return $res; 35 | } 36 | 37 | sub lock_vnetfw_conf { 38 | my ($vnet, $timeout, $code, @param) = @_; 39 | 40 | die "can't lock vnet firewall config for undefined vnet\n" 41 | if !defined($vnet); 42 | 43 | my $res = PVE::Cluster::cfs_lock_firewall("vnet-$vnet", $timeout, $code, @param); 44 | die $@ if $@; 45 | 46 | return $res; 47 | } 48 | 49 | sub remove_vmfw_conf { 50 | my ($vmid) = @_; 51 | 52 | my $vmfw_conffile = "$pvefw_conf_dir/$vmid.fw"; 53 | 54 | unlink $vmfw_conffile; 55 | } 56 | 57 | sub clone_vmfw_conf { 58 | my ($vmid, $newid) = @_; 59 | 60 | my $sourcevm_conffile = "$pvefw_conf_dir/$vmid.fw"; 61 | my $clonevm_conffile = "$pvefw_conf_dir/$newid.fw"; 62 | 63 | lock_vmfw_conf( 64 | $newid, 65 | 10, 66 | sub { 67 | if (-f $clonevm_conffile) { 68 | unlink $clonevm_conffile; 69 | } 70 | if (-f $sourcevm_conffile) { 71 | my $data = file_get_contents($sourcevm_conffile); 72 | file_set_contents($clonevm_conffile, $data); 73 | } 74 | }, 75 | ); 76 | } 77 | 78 | sub dump_fw_logfile { 79 | my ($filename, $param, $callback) = @_; 80 | my ($start, $limit, $since, $until) = $param->@{qw(start limit since until)}; 81 | 82 | my $filter = sub { 83 | my ($line) = @_; 84 | 85 | if (defined($callback)) { 86 | return undef if !$callback->($line); 87 | } 88 | 89 | if ($since || $until) { 90 | my @words = split / /, $line; 91 | my $timestamp = str2time($words[3], $words[4]); 92 | return undef if $since && $timestamp < $since; 93 | return undef if $until && $timestamp > $until; 94 | } 95 | 96 | return $line; 97 | }; 98 | 99 | if (!defined($since) && !defined($until)) { 100 | return PVE::Tools::dump_logfile($filename, $start, $limit, $filter); 101 | } 102 | 103 | my %state = ( 104 | 'count' => 0, 105 | 'lines' => [], 106 | 'start' => $start, 107 | 'limit' => $limit, 108 | ); 109 | 110 | # Take into consideration also rotated logs 111 | my ($basename, $logdir, $type) = fileparse($filename); 112 | my $regex = qr/^\Q$basename\E(\.[\d]+(\.gz)?)?$/; 113 | my @files = (); 114 | 115 | PVE::Tools::dir_glob_foreach( 116 | $logdir, 117 | $regex, 118 | sub { 119 | my ($file) = @_; 120 | push @files, $file; 121 | }, 122 | ); 123 | 124 | @files = reverse sort @files; 125 | 126 | my $filecount = 0; 127 | for my $filename (@files) { 128 | $state{'final'} = $filecount == $#files; 129 | $filecount++; 130 | 131 | my $fh; 132 | if ($filename =~ /\.gz$/) { 133 | $fh = IO::Zlib->new($logdir . $filename, "r"); 134 | } else { 135 | $fh = IO::File->new($logdir . $filename, "r"); 136 | } 137 | 138 | if (!$fh) { 139 | # If file vanished since reading dir entries, ignore 140 | next if $!{ENOENT}; 141 | 142 | my $lines = $state{'lines'}; 143 | my $count = ++$state{'count'}; 144 | push @$lines, ($count, { n => $count, t => "unable to open file - $!" }); 145 | last; 146 | } 147 | 148 | PVE::Tools::dump_logfile_by_filehandle($fh, $filter, \%state); 149 | 150 | close($fh); 151 | } 152 | 153 | return ($state{'count'}, $state{'lines'}); 154 | } 155 | 156 | sub collect_refs { 157 | my ($conf, $type, $scope) = @_; 158 | 159 | my $res = []; 160 | 161 | if (!$type || $type eq 'ipset') { 162 | foreach my $name (keys %{ $conf->{ipset} }) { 163 | my $data = { 164 | type => 'ipset', 165 | name => $name, 166 | ref => "+$name", 167 | scope => $scope, 168 | }; 169 | if (my $comment = $conf->{ipset_comments}->{$name}) { 170 | $data->{comment} = $comment; 171 | } 172 | push @$res, $data; 173 | } 174 | } 175 | 176 | if (!$type || $type eq 'alias') { 177 | foreach my $name (keys %{ $conf->{aliases} }) { 178 | my $e = $conf->{aliases}->{$name}; 179 | my $data = { 180 | type => 'alias', 181 | name => $name, 182 | ref => $name, 183 | scope => $scope, 184 | }; 185 | $data->{comment} = $e->{comment} if $e->{comment}; 186 | push @$res, $data; 187 | } 188 | } 189 | 190 | return $res; 191 | } 192 | 193 | # This is checked in proxmox-firewall to avoid log-spam due to failing to parse the config 194 | our $FORCE_NFT_DISABLE_FLAG_FILE = "/run/proxmox-nftables-firewall-force-disable"; 195 | 196 | =head3 is_nftables() 197 | 198 | Checks whether nftables is active via checking for the existence of the file 199 | C<$FORCE_NFT_DISABLE_FLAG_FILE> 200 | 201 | =cut 202 | 203 | sub is_nftables { 204 | return !-e $FORCE_NFT_DISABLE_FLAG_FILE; 205 | } 206 | 207 | =head3 needs_fwbr($bridge_name) 208 | 209 | Returns whether a given bridge with interface name C<$bridge_name> requires a 210 | firewall bridge in order for the current firewall configuration to work. This is 211 | the case when using pve-firewall (iptables) or bridges that use OVS. 212 | 213 | =cut 214 | 215 | sub needs_fwbr { 216 | my ($bridge_name) = @_; 217 | return !is_nftables() || PVE::Network::is_ovs_bridge($bridge_name); 218 | } 219 | 220 | =head3 flush_fw_ct_entries_by_mark($mark) 221 | 222 | Flushes all conntrack table entries which are CONNMARK'd with the given 223 | value in C<$mark>. 224 | 225 | =cut 226 | 227 | sub flush_fw_ct_entries_by_mark { 228 | my ($mark) = @_; 229 | 230 | PVE::Tools::run_command( 231 | ['conntrack', '--delete', '--mark', $mark], 232 | noerr => 1, 233 | quiet => 1, 234 | ); 235 | } 236 | 237 | 1; 238 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/Host.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::Host; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use PVE::Exception qw(raise_param_exc); 7 | use PVE::JSONSchema qw(get_standard_option); 8 | use PVE::RPCEnvironment; 9 | 10 | use PVE::Firewall; 11 | use PVE::API2::Firewall::Rules; 12 | 13 | use base qw(PVE::RESTHandler); 14 | 15 | __PACKAGE__->register_method({ 16 | subclass => "PVE::API2::Firewall::HostRules", 17 | path => 'rules', 18 | }); 19 | 20 | __PACKAGE__->register_method({ 21 | name => 'index', 22 | path => '', 23 | method => 'GET', 24 | permissions => { user => 'all' }, 25 | description => "Directory index.", 26 | parameters => { 27 | additionalProperties => 0, 28 | properties => { 29 | node => get_standard_option('pve-node'), 30 | }, 31 | }, 32 | returns => { 33 | type => 'array', 34 | items => { 35 | type => "object", 36 | properties => {}, 37 | }, 38 | links => [{ rel => 'child', href => "{name}" }], 39 | }, 40 | code => sub { 41 | my ($param) = @_; 42 | 43 | my $result = [ 44 | { name => 'rules' }, { name => 'options' }, { name => 'log' }, 45 | ]; 46 | 47 | return $result; 48 | }, 49 | }); 50 | 51 | my $option_properties = $PVE::Firewall::host_option_properties; 52 | 53 | my $add_option_properties = sub { 54 | my ($properties) = @_; 55 | 56 | foreach my $k (keys %$option_properties) { 57 | $properties->{$k} = $option_properties->{$k}; 58 | } 59 | 60 | return $properties; 61 | }; 62 | 63 | __PACKAGE__->register_method({ 64 | name => 'get_options', 65 | path => 'options', 66 | method => 'GET', 67 | description => "Get host firewall options.", 68 | proxyto => 'node', 69 | permissions => { 70 | check => ['perm', '/nodes/{node}', ['Sys.Audit']], 71 | }, 72 | parameters => { 73 | additionalProperties => 0, 74 | properties => { 75 | node => get_standard_option('pve-node'), 76 | }, 77 | }, 78 | returns => { 79 | type => "object", 80 | #additionalProperties => 1, 81 | properties => $option_properties, 82 | }, 83 | code => sub { 84 | my ($param) = @_; 85 | 86 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 87 | my $hostfw_conf = PVE::Firewall::load_hostfw_conf($cluster_conf); 88 | 89 | return PVE::Firewall::copy_opject_with_digest($hostfw_conf->{options}); 90 | }, 91 | }); 92 | 93 | __PACKAGE__->register_method({ 94 | name => 'set_options', 95 | path => 'options', 96 | method => 'PUT', 97 | description => "Set Firewall options.", 98 | protected => 1, 99 | proxyto => 'node', 100 | permissions => { 101 | check => ['perm', '/nodes/{node}', ['Sys.Modify']], 102 | }, 103 | parameters => { 104 | additionalProperties => 0, 105 | properties => &$add_option_properties({ 106 | node => get_standard_option('pve-node'), 107 | delete => { 108 | type => 'string', 109 | format => 'pve-configid-list', 110 | description => "A list of settings you want to delete.", 111 | optional => 1, 112 | }, 113 | digest => get_standard_option('pve-config-digest'), 114 | }), 115 | }, 116 | returns => { type => "null" }, 117 | code => sub { 118 | my ($param) = @_; 119 | 120 | PVE::Firewall::lock_hostfw_conf( 121 | undef, 122 | 10, 123 | sub { 124 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 125 | my $hostfw_conf = PVE::Firewall::load_hostfw_conf($cluster_conf); 126 | 127 | my (undef, $digest) = 128 | PVE::Firewall::copy_opject_with_digest($hostfw_conf->{options}); 129 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 130 | 131 | if ($param->{delete}) { 132 | foreach my $opt (PVE::Tools::split_list($param->{delete})) { 133 | raise_param_exc({ delete => "no such option '$opt'" }) 134 | if !$option_properties->{$opt}; 135 | delete $hostfw_conf->{options}->{$opt}; 136 | } 137 | } 138 | 139 | if (defined($param->{enable})) { 140 | $param->{enable} = $param->{enable} ? 1 : 0; 141 | } 142 | 143 | foreach my $k (keys %$option_properties) { 144 | next if !defined($param->{$k}); 145 | $hostfw_conf->{options}->{$k} = $param->{$k}; 146 | } 147 | 148 | PVE::Firewall::save_hostfw_conf($hostfw_conf); 149 | }, 150 | ); 151 | 152 | return undef; 153 | }, 154 | }); 155 | 156 | __PACKAGE__->register_method({ 157 | name => 'log', 158 | path => 'log', 159 | method => 'GET', 160 | description => "Read firewall log", 161 | proxyto => 'node', 162 | permissions => { 163 | check => ['perm', '/nodes/{node}', ['Sys.Syslog']], 164 | }, 165 | protected => 1, 166 | parameters => { 167 | additionalProperties => 0, 168 | properties => { 169 | node => get_standard_option('pve-node'), 170 | start => { 171 | type => 'integer', 172 | minimum => 0, 173 | optional => 1, 174 | }, 175 | limit => { 176 | type => 'integer', 177 | minimum => 0, 178 | optional => 1, 179 | }, 180 | since => { 181 | type => 'integer', 182 | minimum => 0, 183 | description => "Display log since this UNIX epoch.", 184 | optional => 1, 185 | }, 186 | until => { 187 | type => 'integer', 188 | minimum => 0, 189 | description => "Display log until this UNIX epoch.", 190 | optional => 1, 191 | }, 192 | }, 193 | }, 194 | returns => { 195 | type => 'array', 196 | items => { 197 | type => "object", 198 | properties => { 199 | n => { 200 | description => "Line number", 201 | type => 'integer', 202 | }, 203 | t => { 204 | description => "Line text", 205 | type => 'string', 206 | }, 207 | }, 208 | }, 209 | }, 210 | code => sub { 211 | my ($param) = @_; 212 | 213 | my $rpcenv = PVE::RPCEnvironment::get(); 214 | my $user = $rpcenv->get_user(); 215 | my $node = $param->{node}; 216 | my $filename = "/var/log/pve-firewall.log"; 217 | 218 | my ($count, $lines) = PVE::Firewall::Helpers::dump_fw_logfile($filename, $param, undef); 219 | 220 | $rpcenv->set_result_attrib('total', $count); 221 | 222 | return $lines; 223 | }, 224 | }); 225 | 226 | 1; 227 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/Groups.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::Groups; 2 | 3 | use strict; 4 | use warnings; 5 | use PVE::JSONSchema qw(get_standard_option); 6 | use PVE::Exception qw(raise raise_param_exc); 7 | 8 | use PVE::Firewall; 9 | use PVE::API2::Firewall::Rules; 10 | 11 | use base qw(PVE::RESTHandler); 12 | 13 | my $get_security_group_list = sub { 14 | my ($cluster_conf) = @_; 15 | 16 | my $res = []; 17 | foreach my $group (sort keys %{ $cluster_conf->{groups} }) { 18 | my $data = { 19 | group => $group, 20 | }; 21 | if (my $comment = $cluster_conf->{group_comments}->{$group}) { 22 | $data->{comment} = $comment; 23 | } 24 | push @$res, $data; 25 | } 26 | 27 | my ($list, $digest) = PVE::Firewall::copy_list_with_digest($res); 28 | 29 | return wantarray ? ($list, $digest) : $list; 30 | }; 31 | 32 | my $rename_fw_rules = sub { 33 | my ($old, $new, $rules) = @_; 34 | 35 | for my $rule (@{$rules}) { 36 | next if ($rule->{type} ne "group" || $rule->{action} ne $old); 37 | $rule->{action} = $new; 38 | } 39 | }; 40 | 41 | __PACKAGE__->register_method({ 42 | name => 'list_security_groups', 43 | path => '', 44 | method => 'GET', 45 | description => "List security groups.", 46 | permissions => { user => 'all' }, 47 | parameters => { 48 | additionalProperties => 0, 49 | properties => {}, 50 | }, 51 | returns => { 52 | type => 'array', 53 | items => { 54 | type => "object", 55 | properties => { 56 | group => get_standard_option('pve-security-group-name'), 57 | digest => get_standard_option('pve-config-digest', { optional => 0 }), 58 | comment => { 59 | type => 'string', 60 | optional => 1, 61 | }, 62 | }, 63 | }, 64 | links => [{ rel => 'child', href => "{group}" }], 65 | }, 66 | code => sub { 67 | my ($param) = @_; 68 | 69 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 70 | 71 | return &$get_security_group_list($cluster_conf); 72 | }, 73 | }); 74 | 75 | __PACKAGE__->register_method({ 76 | name => 'create_security_group', 77 | path => '', 78 | method => 'POST', 79 | description => "Create new security group.", 80 | protected => 1, 81 | permissions => { 82 | check => ['perm', '/', ['Sys.Modify']], 83 | }, 84 | parameters => { 85 | additionalProperties => 0, 86 | properties => { 87 | group => get_standard_option('pve-security-group-name'), 88 | comment => { 89 | type => 'string', 90 | optional => 1, 91 | }, 92 | rename => get_standard_option( 93 | 'pve-security-group-name', 94 | { 95 | description => 96 | "Rename/update an existing security group. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing group.", 97 | optional => 1, 98 | }, 99 | ), 100 | digest => get_standard_option('pve-config-digest'), 101 | }, 102 | }, 103 | returns => { type => 'null' }, 104 | code => sub { 105 | my ($param) = @_; 106 | 107 | my $group = $param->{group}; 108 | my $rename = $param->{rename}; 109 | my $comment = $param->{comment}; 110 | 111 | PVE::Firewall::lock_clusterfw_conf( 112 | 10, 113 | sub { 114 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 115 | 116 | if ($rename) { 117 | my (undef, $digest) = &$get_security_group_list($cluster_conf); 118 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 119 | 120 | raise_param_exc({ group => "Security group '$rename' does not exist" }) 121 | if !$cluster_conf->{groups}->{$rename}; 122 | 123 | # prevent overwriting an existing group 124 | raise_param_exc({ group => "Security group '$group' does already exist" }) 125 | if $cluster_conf->{groups}->{$group} 126 | && $group ne $rename; 127 | 128 | if ($rename eq $group) { 129 | $cluster_conf->{group_comments}->{$rename} = $comment 130 | if defined($comment); 131 | PVE::Firewall::save_clusterfw_conf($cluster_conf); 132 | return; 133 | } 134 | 135 | # Create an exact copy of the old security group 136 | $cluster_conf->{groups}->{$group} = $cluster_conf->{groups}->{$rename}; 137 | $cluster_conf->{group_comments}->{$group} = 138 | $cluster_conf->{group_comments}->{$rename}; 139 | 140 | # Update comment if provided 141 | $cluster_conf->{group_comments}->{$group} = $comment if defined($comment); 142 | 143 | # Write the copy to the cluster config, so that if something fails inbetween, the new firewall 144 | # rules won't be broken when the new name is referenced 145 | PVE::Firewall::save_clusterfw_conf($cluster_conf); 146 | 147 | # Update all the host configs to the new copy 148 | my $hosts = PVE::Cluster::get_nodelist(); 149 | foreach my $host (@$hosts) { 150 | PVE::Firewall::lock_hostfw_conf( 151 | $host, 152 | 10, 153 | sub { 154 | my $host_conf_path = "/etc/pve/nodes/$host/host.fw"; 155 | my $host_conf = 156 | PVE::Firewall::load_hostfw_conf($cluster_conf, $host_conf_path); 157 | 158 | if (defined($host_conf)) { 159 | &$rename_fw_rules($rename, $group, $host_conf->{rules}); 160 | PVE::Firewall::save_hostfw_conf($host_conf, 161 | $host_conf_path); 162 | } 163 | }, 164 | ); 165 | } 166 | 167 | # Update all the VM configs 168 | my $vms = PVE::Cluster::get_vmlist(); 169 | foreach my $vm (keys %{ $vms->{ids} }) { 170 | PVE::Firewall::lock_vmfw_conf( 171 | $vm, 172 | 10, 173 | sub { 174 | my $vm_type = $vms->{ids}->{$vm}->{type} eq "lxc" ? "ct" : "vm"; 175 | my $vm_conf = PVE::Firewall::load_vmfw_conf( 176 | $cluster_conf, $vm_type, $vm, "/etc/pve/firewall", 177 | ); 178 | 179 | if (defined($vm_conf)) { 180 | &$rename_fw_rules($rename, $group, $vm_conf->{rules}); 181 | PVE::Firewall::save_vmfw_conf($vm, $vm_conf); 182 | } 183 | }, 184 | ); 185 | } 186 | 187 | # And also update the cluster itself 188 | &$rename_fw_rules($rename, $group, $cluster_conf->{rules}); 189 | 190 | # Now that everything has been updated, the old rule can be deleted 191 | delete $cluster_conf->{groups}->{$rename}; 192 | delete $cluster_conf->{group_comments}->{$rename}; 193 | } else { 194 | foreach my $name (keys %{ $cluster_conf->{groups} }) { 195 | raise_param_exc({ group => "Security group '$name' already exists" }) 196 | if $name eq $group; 197 | } 198 | 199 | $cluster_conf->{groups}->{$group} = []; 200 | $cluster_conf->{group_comments}->{$group} = $comment if defined($comment); 201 | } 202 | 203 | PVE::Firewall::save_clusterfw_conf($cluster_conf); 204 | }, 205 | ); 206 | 207 | return undef; 208 | }, 209 | }); 210 | 211 | __PACKAGE__->register_method({ 212 | subclass => "PVE::API2::Firewall::GroupRules", 213 | path => '{group}', 214 | }); 215 | 216 | 1; 217 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/Cluster.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::Cluster; 2 | 3 | use strict; 4 | use warnings; 5 | use PVE::Exception qw(raise raise_param_exc raise_perm_exc); 6 | use PVE::JSONSchema qw(get_standard_option); 7 | 8 | use PVE::Firewall; 9 | use PVE::API2::Firewall::Aliases; 10 | use PVE::API2::Firewall::Rules; 11 | use PVE::API2::Firewall::Groups; 12 | use PVE::API2::Firewall::Helpers; 13 | use PVE::API2::Firewall::IPSet; 14 | 15 | #fixme: locking? 16 | 17 | use base qw(PVE::RESTHandler); 18 | 19 | __PACKAGE__->register_method({ 20 | subclass => "PVE::API2::Firewall::Groups", 21 | path => 'groups', 22 | }); 23 | 24 | __PACKAGE__->register_method({ 25 | subclass => "PVE::API2::Firewall::ClusterRules", 26 | path => 'rules', 27 | }); 28 | 29 | __PACKAGE__->register_method({ 30 | subclass => "PVE::API2::Firewall::ClusterIPSetList", 31 | path => 'ipset', 32 | }); 33 | 34 | __PACKAGE__->register_method({ 35 | subclass => "PVE::API2::Firewall::ClusterAliases", 36 | path => 'aliases', 37 | }); 38 | 39 | __PACKAGE__->register_method({ 40 | name => 'index', 41 | path => '', 42 | method => 'GET', 43 | permissions => { user => 'all' }, 44 | description => "Directory index.", 45 | parameters => { 46 | additionalProperties => 0, 47 | }, 48 | returns => { 49 | type => 'array', 50 | items => { 51 | type => "object", 52 | properties => {}, 53 | }, 54 | links => [{ rel => 'child', href => "{name}" }], 55 | }, 56 | code => sub { 57 | my ($param) = @_; 58 | 59 | my $result = [ 60 | { name => 'aliases' }, 61 | { name => 'rules' }, 62 | { name => 'options' }, 63 | { name => 'groups' }, 64 | { name => 'ipset' }, 65 | { name => 'macros' }, 66 | { name => 'refs' }, 67 | ]; 68 | 69 | return $result; 70 | }, 71 | }); 72 | 73 | my $option_properties = $PVE::Firewall::cluster_option_properties; 74 | 75 | my $add_option_properties = sub { 76 | my ($properties) = @_; 77 | 78 | foreach my $k (keys %$option_properties) { 79 | $properties->{$k} = $option_properties->{$k}; 80 | } 81 | 82 | return $properties; 83 | }; 84 | 85 | __PACKAGE__->register_method({ 86 | name => 'get_options', 87 | path => 'options', 88 | method => 'GET', 89 | description => "Get Firewall options.", 90 | permissions => { 91 | check => ['perm', '/', ['Sys.Audit']], 92 | }, 93 | parameters => { 94 | additionalProperties => 0, 95 | }, 96 | returns => { 97 | type => "object", 98 | #additionalProperties => 1, 99 | properties => $option_properties, 100 | }, 101 | code => sub { 102 | my ($param) = @_; 103 | 104 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 105 | 106 | return PVE::Firewall::copy_opject_with_digest($cluster_conf->{options}); 107 | }, 108 | }); 109 | 110 | __PACKAGE__->register_method({ 111 | name => 'set_options', 112 | path => 'options', 113 | method => 'PUT', 114 | description => "Set Firewall options.", 115 | protected => 1, 116 | permissions => { 117 | check => ['perm', '/', ['Sys.Modify']], 118 | }, 119 | parameters => { 120 | additionalProperties => 0, 121 | properties => &$add_option_properties({ 122 | delete => { 123 | type => 'string', 124 | format => 'pve-configid-list', 125 | description => "A list of settings you want to delete.", 126 | optional => 1, 127 | }, 128 | digest => get_standard_option('pve-config-digest'), 129 | }), 130 | }, 131 | returns => { type => "null" }, 132 | code => sub { 133 | my ($param) = @_; 134 | 135 | PVE::Firewall::lock_clusterfw_conf( 136 | 10, 137 | sub { 138 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 139 | 140 | my (undef, $digest) = 141 | PVE::Firewall::copy_opject_with_digest($cluster_conf->{options}); 142 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 143 | 144 | if ($param->{delete}) { 145 | foreach my $opt (PVE::Tools::split_list($param->{delete})) { 146 | raise_param_exc({ delete => "no such option '$opt'" }) 147 | if !$option_properties->{$opt}; 148 | delete $cluster_conf->{options}->{$opt}; 149 | } 150 | } 151 | 152 | if (defined($param->{enable}) && ($param->{enable} > 1)) { 153 | $param->{enable} = time(); 154 | } 155 | 156 | foreach my $k (keys %$option_properties) { 157 | next if !defined($param->{$k}); 158 | $cluster_conf->{options}->{$k} = $param->{$k}; 159 | } 160 | 161 | PVE::Firewall::save_clusterfw_conf($cluster_conf); 162 | }, 163 | ); 164 | 165 | # instant firewall update when using double (anti-lockout) API call 166 | # -> not waiting for a firewall update at the first (timestamp enable) set 167 | if (defined($param->{enable}) && ($param->{enable} > 1)) { 168 | PVE::Firewall::update(); 169 | } 170 | 171 | return undef; 172 | }, 173 | }); 174 | 175 | __PACKAGE__->register_method({ 176 | name => 'get_macros', 177 | path => 'macros', 178 | method => 'GET', 179 | description => "List available macros", 180 | permissions => { user => 'all' }, 181 | parameters => { 182 | additionalProperties => 0, 183 | }, 184 | returns => { 185 | type => 'array', 186 | items => { 187 | type => "object", 188 | properties => { 189 | macro => { 190 | description => "Macro name.", 191 | type => 'string', 192 | }, 193 | descr => { 194 | description => "More verbose description (if available).", 195 | type => 'string', 196 | }, 197 | }, 198 | }, 199 | }, 200 | code => sub { 201 | my ($param) = @_; 202 | 203 | my $res = []; 204 | 205 | my ($macros, $descr) = PVE::Firewall::get_macros(); 206 | 207 | foreach my $macro (keys %$macros) { 208 | push @$res, { macro => $macro, descr => $descr->{$macro} || $macro }; 209 | } 210 | 211 | return $res; 212 | }, 213 | }); 214 | 215 | __PACKAGE__->register_method({ 216 | name => 'refs', 217 | path => 'refs', 218 | method => 'GET', 219 | description => 220 | "Lists possible IPSet/Alias reference which are allowed in source/dest properties.", 221 | permissions => { 222 | check => ['perm', '/', ['Sys.Audit']], 223 | }, 224 | parameters => { 225 | additionalProperties => 0, 226 | properties => { 227 | type => { 228 | description => "Only list references of specified type.", 229 | type => 'string', 230 | enum => ['alias', 'ipset'], 231 | optional => 1, 232 | }, 233 | }, 234 | }, 235 | returns => { 236 | type => 'array', 237 | items => { 238 | type => "object", 239 | properties => { 240 | type => { 241 | type => 'string', 242 | enum => ['alias', 'ipset'], 243 | }, 244 | name => { 245 | type => 'string', 246 | }, 247 | ref => { 248 | type => 'string', 249 | }, 250 | scope => { 251 | type => 'string', 252 | }, 253 | comment => { 254 | type => 'string', 255 | optional => 1, 256 | }, 257 | }, 258 | }, 259 | }, 260 | code => sub { 261 | my ($param) = @_; 262 | 263 | my $conf = PVE::Firewall::load_clusterfw_conf(); 264 | 265 | # we are explicitly loading the SDN config here with the scope of the current 266 | # API user, so we only return the IPSets that the user can actually use 267 | my $allowed_vms = PVE::API2::Firewall::Helpers::get_allowed_vms(); 268 | my $allowed_vnets = PVE::API2::Firewall::Helpers::get_allowed_vnets(); 269 | my $sdn_conf = PVE::Firewall::load_sdn_conf($allowed_vms, $allowed_vnets); 270 | 271 | my $cluster_refs = PVE::Firewall::Helpers::collect_refs($conf, $param->{type}, "dc"); 272 | my $sdn_refs = PVE::Firewall::Helpers::collect_refs($sdn_conf, $param->{type}, "sdn"); 273 | 274 | return [@$sdn_refs, @$cluster_refs]; 275 | }, 276 | }); 277 | 278 | 1; 279 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/VM.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::VMBase; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use PVE::Exception qw(raise_param_exc); 7 | use PVE::JSONSchema qw(get_standard_option); 8 | use PVE::Cluster; 9 | use PVE::Firewall; 10 | use PVE::API2::Firewall::Rules; 11 | use PVE::API2::Firewall::Helpers; 12 | use PVE::API2::Firewall::Aliases; 13 | 14 | use base qw(PVE::RESTHandler); 15 | 16 | my $option_properties = $PVE::Firewall::vm_option_properties; 17 | 18 | my $add_option_properties = sub { 19 | my ($properties) = @_; 20 | 21 | foreach my $k (keys %$option_properties) { 22 | $properties->{$k} = $option_properties->{$k}; 23 | } 24 | 25 | return $properties; 26 | }; 27 | 28 | sub register_handlers { 29 | my ($class, $rule_env) = @_; 30 | 31 | $class->register_method({ 32 | name => 'index', 33 | path => '', 34 | method => 'GET', 35 | permissions => { user => 'all' }, 36 | description => "Directory index.", 37 | parameters => { 38 | additionalProperties => 0, 39 | properties => { 40 | node => get_standard_option('pve-node'), 41 | vmid => get_standard_option('pve-vmid'), 42 | }, 43 | }, 44 | returns => { 45 | type => 'array', 46 | items => { 47 | type => "object", 48 | properties => {}, 49 | }, 50 | links => [{ rel => 'child', href => "{name}" }], 51 | }, 52 | code => sub { 53 | my ($param) = @_; 54 | 55 | my $result = [ 56 | { name => 'rules' }, 57 | { name => 'aliases' }, 58 | { name => 'ipset' }, 59 | { name => 'refs' }, 60 | { name => 'options' }, 61 | ]; 62 | 63 | return $result; 64 | }, 65 | }); 66 | 67 | $class->register_method({ 68 | name => 'get_options', 69 | path => 'options', 70 | method => 'GET', 71 | description => "Get VM firewall options.", 72 | proxyto => 'node', 73 | permissions => { 74 | check => ['perm', '/vms/{vmid}', ['VM.Audit']], 75 | }, 76 | parameters => { 77 | additionalProperties => 0, 78 | properties => { 79 | node => get_standard_option('pve-node'), 80 | vmid => get_standard_option('pve-vmid'), 81 | }, 82 | }, 83 | returns => { 84 | type => "object", 85 | #additionalProperties => 1, 86 | properties => $option_properties, 87 | }, 88 | code => sub { 89 | my ($param) = @_; 90 | 91 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 92 | my $vmfw_conf = 93 | PVE::Firewall::load_vmfw_conf($cluster_conf, $rule_env, $param->{vmid}); 94 | 95 | return PVE::Firewall::copy_opject_with_digest($vmfw_conf->{options}); 96 | }, 97 | }); 98 | 99 | $class->register_method({ 100 | name => 'set_options', 101 | path => 'options', 102 | method => 'PUT', 103 | description => "Set Firewall options.", 104 | protected => 1, 105 | proxyto => 'node', 106 | permissions => { 107 | check => ['perm', '/vms/{vmid}', ['VM.Config.Network']], 108 | }, 109 | parameters => { 110 | additionalProperties => 0, 111 | properties => &$add_option_properties({ 112 | node => get_standard_option('pve-node'), 113 | vmid => get_standard_option('pve-vmid'), 114 | delete => { 115 | type => 'string', 116 | format => 'pve-configid-list', 117 | description => "A list of settings you want to delete.", 118 | optional => 1, 119 | }, 120 | digest => get_standard_option('pve-config-digest'), 121 | }), 122 | }, 123 | returns => { type => "null" }, 124 | code => sub { 125 | my ($param) = @_; 126 | 127 | PVE::Firewall::lock_vmfw_conf( 128 | $param->{vmid}, 129 | 10, 130 | sub { 131 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 132 | my $vmfw_conf = 133 | PVE::Firewall::load_vmfw_conf($cluster_conf, $rule_env, $param->{vmid}); 134 | 135 | my (undef, $digest) = 136 | PVE::Firewall::copy_opject_with_digest($vmfw_conf->{options}); 137 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 138 | 139 | if ($param->{delete}) { 140 | foreach my $opt (PVE::Tools::split_list($param->{delete})) { 141 | raise_param_exc({ delete => "no such option '$opt'" }) 142 | if !$option_properties->{$opt}; 143 | delete $vmfw_conf->{options}->{$opt}; 144 | } 145 | } 146 | 147 | if (defined($param->{enable})) { 148 | $param->{enable} = $param->{enable} ? 1 : 0; 149 | } 150 | 151 | foreach my $k (keys %$option_properties) { 152 | next if !defined($param->{$k}); 153 | $vmfw_conf->{options}->{$k} = $param->{$k}; 154 | } 155 | 156 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $vmfw_conf); 157 | }, 158 | ); 159 | 160 | return undef; 161 | }, 162 | }); 163 | 164 | $class->register_method({ 165 | name => 'log', 166 | path => 'log', 167 | method => 'GET', 168 | description => "Read firewall log", 169 | proxyto => 'node', 170 | permissions => { 171 | check => ['perm', '/vms/{vmid}', ['VM.Console']], 172 | }, 173 | protected => 1, 174 | parameters => { 175 | additionalProperties => 0, 176 | properties => { 177 | node => get_standard_option('pve-node'), 178 | vmid => get_standard_option('pve-vmid'), 179 | start => { 180 | type => 'integer', 181 | minimum => 0, 182 | optional => 1, 183 | }, 184 | limit => { 185 | type => 'integer', 186 | minimum => 0, 187 | optional => 1, 188 | }, 189 | since => { 190 | type => 'integer', 191 | minimum => 0, 192 | description => "Display log since this UNIX epoch.", 193 | optional => 1, 194 | }, 195 | until => { 196 | type => 'integer', 197 | minimum => 0, 198 | description => "Display log until this UNIX epoch.", 199 | optional => 1, 200 | }, 201 | }, 202 | }, 203 | returns => { 204 | type => 'array', 205 | items => { 206 | type => "object", 207 | properties => { 208 | n => { 209 | description => "Line number", 210 | type => 'integer', 211 | }, 212 | t => { 213 | description => "Line text", 214 | type => 'string', 215 | }, 216 | }, 217 | }, 218 | }, 219 | code => sub { 220 | my ($param) = @_; 221 | 222 | my $rpcenv = PVE::RPCEnvironment::get(); 223 | my $user = $rpcenv->get_user(); 224 | my $filename = "/var/log/pve-firewall.log"; 225 | my $vmid = $param->{'vmid'}; 226 | 227 | my $callback = sub { 228 | my ($line) = @_; 229 | my $reg = "^$vmid "; 230 | return $line =~ m/$reg/; 231 | }; 232 | 233 | my ($count, $lines) = 234 | PVE::Firewall::Helpers::dump_fw_logfile($filename, $param, $callback); 235 | 236 | $rpcenv->set_result_attrib('total', $count); 237 | 238 | return $lines; 239 | }, 240 | }); 241 | 242 | $class->register_method({ 243 | name => 'refs', 244 | path => 'refs', 245 | method => 'GET', 246 | description => 247 | "Lists possible IPSet/Alias reference which are allowed in source/dest properties.", 248 | permissions => { 249 | check => ['perm', '/vms/{vmid}', ['VM.Audit']], 250 | }, 251 | parameters => { 252 | additionalProperties => 0, 253 | properties => { 254 | node => get_standard_option('pve-node'), 255 | vmid => get_standard_option('pve-vmid'), 256 | type => { 257 | description => "Only list references of specified type.", 258 | type => 'string', 259 | enum => ['alias', 'ipset'], 260 | optional => 1, 261 | }, 262 | }, 263 | }, 264 | returns => { 265 | type => 'array', 266 | items => { 267 | type => "object", 268 | properties => { 269 | type => { 270 | type => 'string', 271 | enum => ['alias', 'ipset'], 272 | }, 273 | name => { 274 | type => 'string', 275 | }, 276 | ref => { 277 | type => 'string', 278 | }, 279 | scope => { 280 | type => 'string', 281 | }, 282 | comment => { 283 | type => 'string', 284 | optional => 1, 285 | }, 286 | }, 287 | }, 288 | }, 289 | code => sub { 290 | my ($param) = @_; 291 | 292 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 293 | my $fw_conf = 294 | PVE::Firewall::load_vmfw_conf($cluster_conf, $rule_env, $param->{vmid}); 295 | 296 | # we are explicitly loading the SDN config here with the scope of the current 297 | # API user, so we only return the IPSets that the user can actually use 298 | my $allowed_vms = PVE::API2::Firewall::Helpers::get_allowed_vms(); 299 | my $allowed_vnets = PVE::API2::Firewall::Helpers::get_allowed_vnets(); 300 | my $sdn_conf = PVE::Firewall::load_sdn_conf($allowed_vms, $allowed_vnets); 301 | 302 | my $dc_refs = 303 | PVE::Firewall::Helpers::collect_refs($cluster_conf, $param->{type}, 'dc'); 304 | my $sdn_refs = 305 | PVE::Firewall::Helpers::collect_refs($sdn_conf, $param->{type}, "sdn"); 306 | my $vm_refs = 307 | PVE::Firewall::Helpers::collect_refs($fw_conf, $param->{type}, 'guest'); 308 | 309 | return [@$dc_refs, @$sdn_refs, @$vm_refs]; 310 | }, 311 | }); 312 | } 313 | 314 | package PVE::API2::Firewall::VM; 315 | 316 | use strict; 317 | use warnings; 318 | 319 | use base qw(PVE::API2::Firewall::VMBase); 320 | 321 | __PACKAGE__->register_method({ 322 | subclass => "PVE::API2::Firewall::VMRules", 323 | path => 'rules', 324 | }); 325 | 326 | __PACKAGE__->register_method({ 327 | subclass => "PVE::API2::Firewall::VMAliases", 328 | path => 'aliases', 329 | }); 330 | 331 | __PACKAGE__->register_method({ 332 | subclass => "PVE::API2::Firewall::VMIPSetList", 333 | path => 'ipset', 334 | }); 335 | 336 | __PACKAGE__->register_handlers('vm'); 337 | 338 | package PVE::API2::Firewall::CT; 339 | 340 | use strict; 341 | use warnings; 342 | 343 | use base qw(PVE::API2::Firewall::VMBase); 344 | 345 | __PACKAGE__->register_method({ 346 | subclass => "PVE::API2::Firewall::CTRules", 347 | path => 'rules', 348 | }); 349 | 350 | __PACKAGE__->register_method({ 351 | subclass => "PVE::API2::Firewall::CTAliases", 352 | path => 'aliases', 353 | }); 354 | 355 | __PACKAGE__->register_method({ 356 | subclass => "PVE::API2::Firewall::CTIPSetList", 357 | path => 'ipset', 358 | }); 359 | 360 | __PACKAGE__->register_handlers('vm'); 361 | 362 | 1; 363 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/Aliases.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::AliasesBase; 2 | 3 | use strict; 4 | use warnings; 5 | use PVE::Exception qw(raise raise_param_exc); 6 | use PVE::JSONSchema qw(get_standard_option); 7 | 8 | use PVE::Firewall; 9 | 10 | use base qw(PVE::RESTHandler); 11 | 12 | my $api_properties = { 13 | cidr => { 14 | description => "Network/IP specification in CIDR format.", 15 | type => 'string', 16 | format => 'IPorCIDR', 17 | }, 18 | name => get_standard_option('pve-fw-alias'), 19 | rename => get_standard_option( 20 | 'pve-fw-alias', 21 | { 22 | description => "Rename an existing alias.", 23 | optional => 1, 24 | }, 25 | ), 26 | comment => { 27 | type => 'string', 28 | optional => 1, 29 | }, 30 | }; 31 | 32 | sub lock_config { 33 | my ($class, $param, $code) = @_; 34 | 35 | die "implement this in subclass"; 36 | } 37 | 38 | sub load_config { 39 | my ($class, $param) = @_; 40 | 41 | die "implement this in subclass"; 42 | 43 | #return ($fw_conf, $rules); 44 | } 45 | 46 | sub save_aliases { 47 | my ($class, $param, $fw_conf, $aliases) = @_; 48 | 49 | die "implement this in subclass"; 50 | } 51 | 52 | sub rule_env { 53 | my ($class, $param) = @_; 54 | 55 | die "implement this in subclass"; 56 | } 57 | 58 | my $additional_param_hash = {}; 59 | 60 | sub additional_parameters { 61 | my ($class, $new_value) = @_; 62 | 63 | if (defined($new_value)) { 64 | $additional_param_hash->{$class} = $new_value; 65 | } 66 | 67 | # return a copy 68 | my $copy = {}; 69 | my $org = $additional_param_hash->{$class} || {}; 70 | foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; } 71 | return $copy; 72 | } 73 | 74 | my $aliases_to_list = sub { 75 | my ($aliases) = @_; 76 | 77 | my $list = []; 78 | foreach my $k (sort keys %$aliases) { 79 | push @$list, $aliases->{$k}; 80 | } 81 | return $list; 82 | }; 83 | 84 | sub register_get_aliases { 85 | my ($class) = @_; 86 | 87 | my $properties = $class->additional_parameters(); 88 | 89 | $class->register_method({ 90 | name => 'get_aliases', 91 | path => '', 92 | method => 'GET', 93 | description => "List aliases", 94 | permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()), 95 | parameters => { 96 | additionalProperties => 0, 97 | properties => $properties, 98 | }, 99 | returns => { 100 | type => 'array', 101 | items => { 102 | type => "object", 103 | properties => { 104 | name => { type => 'string' }, 105 | cidr => { type => 'string' }, 106 | comment => { 107 | type => 'string', 108 | optional => 1, 109 | }, 110 | digest => get_standard_option('pve-config-digest', { optional => 0 }), 111 | }, 112 | }, 113 | links => [{ rel => 'child', href => "{name}" }], 114 | }, 115 | code => sub { 116 | my ($param) = @_; 117 | 118 | my ($fw_conf, $aliases) = $class->load_config($param); 119 | 120 | my $list = &$aliases_to_list($aliases); 121 | 122 | return PVE::Firewall::copy_list_with_digest($list); 123 | }, 124 | }); 125 | } 126 | 127 | sub register_create_alias { 128 | my ($class) = @_; 129 | 130 | my $properties = $class->additional_parameters(); 131 | 132 | $properties->{name} = $api_properties->{name}; 133 | $properties->{cidr} = $api_properties->{cidr}; 134 | $properties->{comment} = $api_properties->{comment}; 135 | 136 | $class->register_method({ 137 | name => 'create_alias', 138 | path => '', 139 | method => 'POST', 140 | description => "Create IP or Network Alias.", 141 | permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), 142 | protected => 1, 143 | parameters => { 144 | additionalProperties => 0, 145 | properties => $properties, 146 | }, 147 | returns => { type => "null" }, 148 | code => sub { 149 | my ($param) = @_; 150 | 151 | $class->lock_config( 152 | $param, 153 | sub { 154 | my ($param) = @_; 155 | 156 | my ($fw_conf, $aliases) = $class->load_config($param); 157 | 158 | my $name = lc($param->{name}); 159 | 160 | raise_param_exc({ name => "alias '$param->{name}' already exists" }) 161 | if defined($aliases->{$name}); 162 | 163 | my $data = { name => $param->{name}, cidr => $param->{cidr} }; 164 | $data->{comment} = $param->{comment} if $param->{comment}; 165 | 166 | $aliases->{$name} = $data; 167 | 168 | $class->save_aliases($param, $fw_conf, $aliases); 169 | }, 170 | ); 171 | 172 | return undef; 173 | }, 174 | }); 175 | } 176 | 177 | sub register_read_alias { 178 | my ($class) = @_; 179 | 180 | my $properties = $class->additional_parameters(); 181 | 182 | $properties->{name} = $api_properties->{name}; 183 | 184 | $class->register_method({ 185 | name => 'read_alias', 186 | path => '{name}', 187 | method => 'GET', 188 | description => "Read alias.", 189 | permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()), 190 | parameters => { 191 | additionalProperties => 0, 192 | properties => $properties, 193 | }, 194 | returns => { type => "object" }, 195 | code => sub { 196 | my ($param) = @_; 197 | 198 | my ($fw_conf, $aliases) = $class->load_config($param); 199 | 200 | my $name = lc($param->{name}); 201 | 202 | raise_param_exc({ name => "no such alias" }) 203 | if !defined($aliases->{$name}); 204 | 205 | return $aliases->{$name}; 206 | }, 207 | }); 208 | } 209 | 210 | sub register_update_alias { 211 | my ($class) = @_; 212 | 213 | my $properties = $class->additional_parameters(); 214 | 215 | $properties->{name} = $api_properties->{name}; 216 | $properties->{rename} = $api_properties->{rename}; 217 | $properties->{cidr} = $api_properties->{cidr}; 218 | $properties->{comment} = $api_properties->{comment}; 219 | $properties->{digest} = get_standard_option('pve-config-digest'); 220 | 221 | $class->register_method({ 222 | name => 'update_alias', 223 | path => '{name}', 224 | method => 'PUT', 225 | description => "Update IP or Network alias.", 226 | permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), 227 | protected => 1, 228 | parameters => { 229 | additionalProperties => 0, 230 | properties => $properties, 231 | }, 232 | returns => { type => "null" }, 233 | code => sub { 234 | my ($param) = @_; 235 | 236 | $class->lock_config( 237 | $param, 238 | sub { 239 | my ($param) = @_; 240 | 241 | my ($fw_conf, $aliases) = $class->load_config($param); 242 | 243 | my $list = &$aliases_to_list($aliases); 244 | 245 | my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list); 246 | 247 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 248 | 249 | my $name = lc($param->{name}); 250 | 251 | raise_param_exc({ name => "no such alias" }) if !$aliases->{$name}; 252 | 253 | my $data = { name => $param->{name}, cidr => $param->{cidr} }; 254 | $data->{comment} = $param->{comment} if $param->{comment}; 255 | 256 | $aliases->{$name} = $data; 257 | 258 | my $rename = $param->{rename}; 259 | $rename = lc($rename) if $rename; 260 | 261 | if ($rename && ($name ne $rename)) { 262 | raise_param_exc({ name => "alias '$param->{rename}' already exists" }) 263 | if defined($aliases->{$rename}); 264 | $aliases->{$name}->{name} = $param->{rename}; 265 | $aliases->{$rename} = $aliases->{$name}; 266 | delete $aliases->{$name}; 267 | } 268 | 269 | $class->save_aliases($param, $fw_conf, $aliases); 270 | }, 271 | ); 272 | 273 | return undef; 274 | }, 275 | }); 276 | } 277 | 278 | sub register_delete_alias { 279 | my ($class) = @_; 280 | 281 | my $properties = $class->additional_parameters(); 282 | 283 | $properties->{name} = $api_properties->{name}; 284 | $properties->{digest} = get_standard_option('pve-config-digest'); 285 | 286 | $class->register_method({ 287 | name => 'remove_alias', 288 | path => '{name}', 289 | method => 'DELETE', 290 | description => "Remove IP or Network alias.", 291 | permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), 292 | protected => 1, 293 | parameters => { 294 | additionalProperties => 0, 295 | properties => $properties, 296 | }, 297 | returns => { type => "null" }, 298 | code => sub { 299 | my ($param) = @_; 300 | 301 | $class->lock_config( 302 | $param, 303 | sub { 304 | my ($param) = @_; 305 | 306 | my ($fw_conf, $aliases) = $class->load_config($param); 307 | 308 | my $list = &$aliases_to_list($aliases); 309 | my (undef, $digest) = PVE::Firewall::copy_list_with_digest($list); 310 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 311 | 312 | my $name = lc($param->{name}); 313 | delete $aliases->{$name}; 314 | 315 | $class->save_aliases($param, $fw_conf, $aliases); 316 | }, 317 | ); 318 | 319 | return undef; 320 | }, 321 | }); 322 | } 323 | 324 | sub register_handlers { 325 | my ($class) = @_; 326 | 327 | $class->register_get_aliases(); 328 | $class->register_create_alias(); 329 | $class->register_read_alias(); 330 | $class->register_update_alias(); 331 | $class->register_delete_alias(); 332 | } 333 | 334 | package PVE::API2::Firewall::ClusterAliases; 335 | 336 | use strict; 337 | use warnings; 338 | 339 | use base qw(PVE::API2::Firewall::AliasesBase); 340 | 341 | sub rule_env { 342 | my ($class, $param) = @_; 343 | 344 | return 'cluster'; 345 | } 346 | 347 | sub lock_config { 348 | my ($class, $param, $code) = @_; 349 | 350 | PVE::Firewall::lock_clusterfw_conf(10, $code, $param); 351 | } 352 | 353 | sub load_config { 354 | my ($class, $param) = @_; 355 | 356 | my $fw_conf = PVE::Firewall::load_clusterfw_conf(); 357 | my $aliases = $fw_conf->{aliases}; 358 | 359 | return ($fw_conf, $aliases); 360 | } 361 | 362 | sub save_aliases { 363 | my ($class, $param, $fw_conf, $aliases) = @_; 364 | 365 | $fw_conf->{aliases} = $aliases; 366 | PVE::Firewall::save_clusterfw_conf($fw_conf); 367 | } 368 | 369 | __PACKAGE__->register_handlers(); 370 | 371 | package PVE::API2::Firewall::VMAliases; 372 | 373 | use strict; 374 | use warnings; 375 | use PVE::JSONSchema qw(get_standard_option); 376 | 377 | use base qw(PVE::API2::Firewall::AliasesBase); 378 | 379 | sub rule_env { 380 | my ($class, $param) = @_; 381 | 382 | return 'vm'; 383 | } 384 | 385 | __PACKAGE__->additional_parameters({ 386 | node => get_standard_option('pve-node'), 387 | vmid => get_standard_option('pve-vmid'), 388 | }); 389 | 390 | sub lock_config { 391 | my ($class, $param, $code) = @_; 392 | 393 | PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param); 394 | } 395 | 396 | sub load_config { 397 | my ($class, $param) = @_; 398 | 399 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 400 | my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid}); 401 | my $aliases = $fw_conf->{aliases}; 402 | 403 | return ($fw_conf, $aliases); 404 | } 405 | 406 | sub save_aliases { 407 | my ($class, $param, $fw_conf, $aliases) = @_; 408 | 409 | $fw_conf->{aliases} = $aliases; 410 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); 411 | } 412 | 413 | __PACKAGE__->register_handlers(); 414 | 415 | package PVE::API2::Firewall::CTAliases; 416 | 417 | use strict; 418 | use warnings; 419 | use PVE::JSONSchema qw(get_standard_option); 420 | 421 | use base qw(PVE::API2::Firewall::AliasesBase); 422 | 423 | sub rule_env { 424 | my ($class, $param) = @_; 425 | 426 | return 'ct'; 427 | } 428 | 429 | __PACKAGE__->additional_parameters({ 430 | node => get_standard_option('pve-node'), 431 | vmid => get_standard_option('pve-vmid'), 432 | }); 433 | 434 | sub lock_config { 435 | my ($class, $param, $code) = @_; 436 | 437 | PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param); 438 | } 439 | 440 | sub load_config { 441 | my ($class, $param) = @_; 442 | 443 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 444 | my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid}); 445 | my $aliases = $fw_conf->{aliases}; 446 | 447 | return ($fw_conf, $aliases); 448 | } 449 | 450 | sub save_aliases { 451 | my ($class, $param, $fw_conf, $aliases) = @_; 452 | 453 | $fw_conf->{aliases} = $aliases; 454 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); 455 | } 456 | 457 | __PACKAGE__->register_handlers(); 458 | 459 | 1; 460 | -------------------------------------------------------------------------------- /src/PVE/Service/pve_firewall.pm: -------------------------------------------------------------------------------- 1 | package PVE::Service::pve_firewall; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Data::Dumper; 7 | use Time::HiRes qw (gettimeofday usleep); 8 | 9 | use PVE::CLIHandler; 10 | use PVE::Cluster qw(cfs_read_file); 11 | use PVE::Corosync; 12 | use PVE::Daemon; 13 | use PVE::INotify; 14 | use PVE::ProcFSTools; 15 | use PVE::RPCEnvironment; 16 | use PVE::SafeSyslog; 17 | use PVE::Tools qw(dir_glob_foreach file_read_firstline); 18 | 19 | use PVE::Firewall; 20 | use PVE::FirewallSimulator; 21 | use PVE::FirewallSimulator qw($bridge_interface_pattern); 22 | 23 | use base qw(PVE::Daemon); 24 | 25 | my $cmdline = [$0, @ARGV]; 26 | 27 | my %daemon_options = (restart_on_error => 5, stop_wait_time => 5); 28 | 29 | my $daemon = __PACKAGE__->new('pve-firewall', $cmdline, %daemon_options); 30 | 31 | my $nodename = PVE::INotify::nodename(); 32 | 33 | sub init { 34 | PVE::Cluster::cfs_update(); 35 | 36 | PVE::Firewall::init(); 37 | } 38 | 39 | my ($next_update, $cycle, $restart_request) = (0, 0, 0); 40 | my $updatetime = 10; 41 | 42 | my $initial_memory_usage; 43 | 44 | sub shutdown { 45 | my ($self) = @_; 46 | 47 | syslog('info', "server shutting down"); 48 | 49 | # wait for children 50 | 1 while (waitpid(-1, POSIX::WNOHANG()) > 0); 51 | 52 | syslog('info', "clear PVE-generated firewall rules"); 53 | 54 | eval { PVE::Firewall::remove_pvefw_chains(); }; 55 | warn $@ if $@; 56 | 57 | $self->exit_daemon(0); 58 | } 59 | 60 | sub hup { 61 | my ($self) = @_; 62 | 63 | $restart_request = 1; 64 | } 65 | 66 | sub run { 67 | my ($self) = @_; 68 | 69 | local $SIG{'__WARN__'} = 'IGNORE'; # do not fill up logs 70 | 71 | for (;;) { # forever 72 | $next_update = time() + $updatetime; 73 | 74 | my ($ccsec, $cusec) = gettimeofday(); 75 | eval { 76 | PVE::Cluster::cfs_update(); 77 | PVE::Firewall::update(); 78 | }; 79 | if (my $err = $@) { 80 | syslog('err', "status update error: $err"); 81 | } 82 | 83 | my ($ccsec_end, $cusec_end) = gettimeofday(); 84 | my $cptime = ($ccsec_end - $ccsec) + ($cusec_end - $cusec) / 1000000; 85 | 86 | syslog('info', sprintf("firewall update time (%.3f seconds)", $cptime)) 87 | if ($cptime > 5); 88 | 89 | $cycle++; 90 | 91 | my $mem = PVE::ProcFSTools::read_memory_usage(); 92 | 93 | if (!defined($initial_memory_usage) || ($cycle < 10)) { 94 | $initial_memory_usage = $mem->{resident}; 95 | } else { 96 | my $diff = $mem->{resident} - $initial_memory_usage; 97 | if ($diff > 5 * 1024 * 1024) { 98 | syslog( 99 | 'info', 100 | "restarting server after $cycle cycles to " 101 | . "reduce memory usage (free $mem->{resident} ($diff) bytes)", 102 | ); 103 | $self->restart_daemon(); 104 | } 105 | } 106 | 107 | my $wcount = 0; 108 | while ( 109 | (time() < $next_update) 110 | && ($wcount < $updatetime) 111 | && # protect against time wrap 112 | !$restart_request 113 | ) { 114 | $wcount++; 115 | sleep(1); 116 | } 117 | 118 | $self->restart_daemon() if $restart_request; 119 | } 120 | } 121 | 122 | $daemon->register_start_command("Start the Proxmox VE firewall service."); 123 | $daemon->register_restart_command(1, "Restart the Proxmox VE firewall service."); 124 | $daemon->register_stop_command( 125 | "Stop the Proxmox VE firewall service. Note, stopping actively removes all Proxmox VE related" 126 | . " iptable rules rendering the host potentially unprotected."); 127 | 128 | __PACKAGE__->register_method({ 129 | name => 'status', 130 | path => 'status', 131 | method => 'GET', 132 | description => "Get firewall status.", 133 | parameters => { 134 | additionalProperties => 0, 135 | properties => {}, 136 | }, 137 | returns => { 138 | type => 'object', 139 | additionalProperties => 0, 140 | properties => { 141 | status => { 142 | type => 'string', 143 | enum => ['unknown', 'stopped', 'running'], 144 | }, 145 | enable => { 146 | description => "Firewall is enabled (in 'cluster.fw')", 147 | type => 'boolean', 148 | }, 149 | changes => { 150 | description => "Set when there are pending changes.", 151 | type => 'boolean', 152 | optional => 1, 153 | }, 154 | }, 155 | }, 156 | code => sub { 157 | my ($param) = @_; 158 | 159 | local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog 160 | 161 | my $code = sub { 162 | 163 | my $status = $daemon->running() ? 'running' : 'stopped'; 164 | 165 | my $res = { status => $status }; 166 | 167 | PVE::Firewall::set_verbose(1); # show syntax errors 168 | 169 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 170 | $res->{enable} = $cluster_conf->{options}->{enable} ? 1 : 0; 171 | 172 | if ($status eq 'running') { 173 | 174 | my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = 175 | PVE::Firewall::compile($cluster_conf, undef, undef); 176 | 177 | PVE::Firewall::set_verbose(0); # do not show iptables details 178 | my (undef, undef, $ipset_changes) = 179 | PVE::Firewall::get_ipset_cmdlist($ipset_ruleset); 180 | my ($test, $ruleset_changes) = 181 | PVE::Firewall::get_ruleset_cmdlist($ruleset->{filter}); 182 | my (undef, $ruleset_changesv6) = 183 | PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables"); 184 | my (undef, $ruleset_changes_raw) = 185 | PVE::Firewall::get_ruleset_cmdlist($ruleset->{raw}, undef, 'raw'); 186 | my (undef, $ruleset_changesv6_raw) = 187 | PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{raw}, "ip6tables", 'raw'); 188 | my (undef, $ebtables_changes) = 189 | PVE::Firewall::get_ebtables_cmdlist($ebtables_ruleset); 190 | 191 | $res->{changes} = 192 | ($ipset_changes 193 | || $ruleset_changes 194 | || $ruleset_changesv6 195 | || $ebtables_changes 196 | || $ruleset_changes_raw 197 | || $ruleset_changesv6_raw) ? 1 : 0; 198 | } 199 | 200 | return $res; 201 | }; 202 | 203 | return PVE::Firewall::run_locked($code); 204 | }, 205 | }); 206 | 207 | __PACKAGE__->register_method({ 208 | name => 'compile', 209 | path => 'compile', 210 | method => 'GET', 211 | description => "Compile and print firewall rules. This is useful for testing.", 212 | parameters => { 213 | additionalProperties => 0, 214 | properties => {}, 215 | }, 216 | returns => { type => 'null' }, 217 | 218 | code => sub { 219 | my ($param) = @_; 220 | 221 | local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog 222 | 223 | my $code = sub { 224 | 225 | PVE::Firewall::set_verbose(1); 226 | 227 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 228 | my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = 229 | PVE::Firewall::compile($cluster_conf, undef, undef); 230 | 231 | print "ipset cmdlist:\n"; 232 | my (undef, undef, $ipset_changes) = 233 | PVE::Firewall::get_ipset_cmdlist($ipset_ruleset); 234 | 235 | print "\niptables cmdlist:\n"; 236 | my (undef, $ruleset_changes) = 237 | PVE::Firewall::get_ruleset_cmdlist($ruleset->{filter}); 238 | 239 | print "\nip6tables cmdlist:\n"; 240 | my (undef, $ruleset_changesv6) = 241 | PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{filter}, "ip6tables"); 242 | 243 | print "\nebtables cmdlist:\n"; 244 | my (undef, $ebtables_changes) = 245 | PVE::Firewall::get_ebtables_cmdlist($ebtables_ruleset); 246 | 247 | print "\niptables table raw cmdlist:\n"; 248 | my (undef, $ruleset_changes_raw) = 249 | PVE::Firewall::get_ruleset_cmdlist($ruleset->{raw}, undef, 'raw'); 250 | 251 | print "\nip6tables table raw cmdlist:\n"; 252 | my (undef, $ruleset_changesv6_raw) = 253 | PVE::Firewall::get_ruleset_cmdlist($rulesetv6->{raw}, "ip6tables", 'raw'); 254 | 255 | if ( 256 | $ipset_changes 257 | || $ruleset_changes 258 | || $ruleset_changesv6 259 | || $ebtables_changes 260 | || $ruleset_changes_raw 261 | || $ruleset_changesv6_raw 262 | ) { 263 | print "detected changes\n"; 264 | } else { 265 | print "no changes\n"; 266 | } 267 | if (!$cluster_conf->{options}->{enable}) { 268 | print "firewall disabled\n"; 269 | } 270 | }; 271 | 272 | PVE::Firewall::run_locked($code); 273 | 274 | return undef; 275 | }, 276 | }); 277 | 278 | __PACKAGE__->register_method({ 279 | name => 'localnet', 280 | path => 'localnet', 281 | method => 'GET', 282 | description => "Print information about local network.", 283 | parameters => { 284 | additionalProperties => 0, 285 | properties => {}, 286 | }, 287 | returns => { type => 'null' }, 288 | code => sub { 289 | my ($param) = @_; 290 | 291 | local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog 292 | 293 | my $nodename = PVE::INotify::nodename(); 294 | print "local hostname: $nodename\n"; 295 | 296 | my $ip = PVE::Cluster::remote_node_ip($nodename); 297 | print "local IP address: $ip\n"; 298 | 299 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 300 | 301 | my $localnet = PVE::Firewall::local_network() || '127.0.0.0/8'; 302 | print "network auto detect: $localnet\n"; 303 | if (my $local_network = $cluster_conf->{aliases}->{local_network}) { 304 | print "using user defined local_network: $local_network->{cidr}\n"; 305 | } else { 306 | print "using detected local_network: $localnet\n"; 307 | } 308 | 309 | if (PVE::Corosync::check_conf_exists(1)) { 310 | my $corosync_conf = PVE::Cluster::cfs_read_file("corosync.conf"); 311 | my $corosync_node_found = 0; 312 | 313 | print "\naccepting corosync traffic from/to:\n"; 314 | 315 | PVE::Corosync::for_all_corosync_addresses( 316 | $corosync_conf, 317 | undef, 318 | sub { 319 | my ($curr_node_name, $curr_node_ip, undef, $key) = @_; 320 | 321 | return if $curr_node_name eq $nodename; 322 | 323 | $corosync_node_found = 1; 324 | 325 | $key =~ m/(?:ring|link)(\d+)_addr/; 326 | print " - $curr_node_name: $curr_node_ip (link: $1)\n"; 327 | }, 328 | ); 329 | 330 | if (!$corosync_node_found) { 331 | print " - no nodes found\n"; 332 | } 333 | } 334 | 335 | return undef; 336 | }, 337 | }); 338 | 339 | __PACKAGE__->register_method({ 340 | name => 'simulate', 341 | path => 'simulate', 342 | method => 'GET', 343 | description => 344 | "Simulate firewall rules. This does not simulates the kernel 'routing' table," 345 | . " but simply assumes that routing from source zone to destination zone is possible.", 346 | parameters => { 347 | additionalProperties => 0, 348 | properties => { 349 | verbose => { 350 | description => "Verbose output.", 351 | type => 'boolean', 352 | optional => 1, 353 | default => 0, 354 | }, 355 | from => { 356 | description => "Source zone.", 357 | type => 'string', 358 | pattern => "(host|outside|vm\\d+|ct\\d+|$bridge_interface_pattern)", 359 | optional => 1, 360 | default => 'outside', 361 | }, 362 | to => { 363 | description => "Destination zone.", 364 | type => 'string', 365 | pattern => "(host|outside|vm\\d+|ct\\d+|$bridge_interface_pattern)", 366 | optional => 1, 367 | default => 'host', 368 | }, 369 | protocol => { 370 | description => "Protocol.", 371 | type => 'string', 372 | pattern => '(tcp|udp)', 373 | optional => 1, 374 | default => 'tcp', 375 | }, 376 | dport => { 377 | description => "Destination port.", 378 | type => 'integer', 379 | minValue => 1, 380 | maxValue => 65535, 381 | optional => 1, 382 | }, 383 | sport => { 384 | description => "Source port.", 385 | type => 'integer', 386 | minValue => 1, 387 | maxValue => 65535, 388 | optional => 1, 389 | }, 390 | source => { 391 | description => "Source IP address.", 392 | type => 'string', 393 | format => 'ipv4', 394 | optional => 1, 395 | }, 396 | dest => { 397 | description => "Destination IP address.", 398 | type => 'string', 399 | format => 'ipv4', 400 | optional => 1, 401 | }, 402 | }, 403 | }, 404 | returns => { type => 'null' }, 405 | code => sub { 406 | my ($param) = @_; 407 | 408 | local $SIG{'__WARN__'} = 'DEFAULT'; # do not fill up syslog 409 | 410 | PVE::Firewall::set_verbose($param->{verbose}); 411 | 412 | my ($ruleset, $ipset_ruleset, $rulesetv6, $ebtables_ruleset) = PVE::Firewall::compile(); 413 | 414 | PVE::FirewallSimulator::debug(); 415 | 416 | my $host_ip = PVE::Cluster::remote_node_ip($nodename); 417 | 418 | PVE::FirewallSimulator::reset_trace(); 419 | print Dumper($ruleset->{filter}) if $param->{verbose}; 420 | print Dumper($ruleset->{raw}) if $param->{verbose}; 421 | 422 | my $test = { 423 | from => $param->{from}, 424 | to => $param->{to}, 425 | proto => $param->{protocol} || 'tcp', 426 | source => $param->{source}, 427 | dest => $param->{dest}, 428 | dport => $param->{dport}, 429 | sport => $param->{sport}, 430 | }; 431 | 432 | if (!defined($test->{to})) { 433 | $test->{to} = 'host'; 434 | PVE::FirewallSimulator::add_trace("Set Zone: to => '$test->{to}'\n"); 435 | } 436 | if (!defined($test->{from})) { 437 | $test->{from} = 'outside', 438 | PVE::FirewallSimulator::add_trace("Set Zone: from => '$test->{from}'\n"); 439 | } 440 | 441 | my $vmdata = PVE::Firewall::read_local_vm_config(); 442 | 443 | print "Test packet:\n"; 444 | 445 | foreach my $k (qw(from to proto source dest dport sport)) { 446 | printf(" %-8s: %s\n", $k, $test->{$k}) if defined($test->{$k}); 447 | } 448 | 449 | $test->{action} = 'QUERY'; 450 | 451 | my $res = PVE::FirewallSimulator::simulate_firewall( 452 | $ruleset->{filter}, $ipset_ruleset, $host_ip, $vmdata, $test, 453 | ); 454 | 455 | print "ACTION: $res\n"; 456 | 457 | return undef; 458 | }, 459 | }); 460 | 461 | our $cmddef = { 462 | start => [__PACKAGE__, 'start', []], 463 | restart => [__PACKAGE__, 'restart', []], 464 | stop => [__PACKAGE__, 'stop', []], 465 | compile => [__PACKAGE__, 'compile', []], 466 | simulate => [__PACKAGE__, 'simulate', []], 467 | localnet => [__PACKAGE__, 'localnet', []], 468 | status => [ 469 | __PACKAGE__, 470 | 'status', 471 | [], 472 | undef, 473 | sub { 474 | my $res = shift; 475 | my $status = ($res->{enable} ? "enabled" : "disabled") . '/' . $res->{status}; 476 | 477 | if ($res->{changes}) { 478 | print "Status: $status (pending changes)\n"; 479 | } else { 480 | print "Status: $status\n"; 481 | } 482 | }, 483 | ], 484 | }; 485 | 486 | 1; 487 | -------------------------------------------------------------------------------- /src/PVE/FirewallSimulator.pm: -------------------------------------------------------------------------------- 1 | package PVE::FirewallSimulator; 2 | 3 | use strict; 4 | use warnings; 5 | use Data::Dumper; 6 | use PVE::Firewall; 7 | use File::Basename; 8 | use Net::IP; 9 | 10 | use base 'Exporter'; 11 | our @EXPORT_OK = qw( 12 | $bridge_name_pattern 13 | $bridge_interface_pattern 14 | ); 15 | 16 | # dynamically include PVE::QemuServer and PVE::LXC 17 | # to avoid dependency problems 18 | my $have_qemu_server; 19 | eval { 20 | require PVE::QemuServer; 21 | $have_qemu_server = 1; 22 | }; 23 | 24 | my $have_lxc; 25 | eval { 26 | require PVE::LXC; 27 | $have_lxc = 1; 28 | }; 29 | 30 | my $mark = 0; 31 | my $trace; 32 | my $debug = 0; 33 | 34 | my $NUMBER_RE = qr/0x[0-9a-fA-F]+|\d+/; 35 | 36 | our $bridge_name_pattern = '[a-zA-Z][a-zA-Z0-9]{0,9}'; 37 | our $bridge_interface_pattern = "($bridge_name_pattern)/(\\S+)"; 38 | 39 | sub debug { 40 | my $new_value = shift; 41 | $debug = $new_value if defined($new_value); 42 | return $debug; 43 | } 44 | 45 | sub reset_trace { 46 | $trace = ''; 47 | } 48 | 49 | sub get_trace { 50 | return $trace; 51 | } 52 | 53 | sub add_trace { 54 | my ($text) = @_; 55 | 56 | if ($debug) { 57 | print $text; 58 | } else { 59 | $trace .= $text; 60 | } 61 | } 62 | 63 | $SIG{'__WARN__'} = sub { 64 | my $err = $@; 65 | my $t = $_[0]; 66 | chomp $t; 67 | add_trace("$t\n"); 68 | $@ = $err; 69 | }; 70 | 71 | sub nf_dev_match { 72 | my ($devre, $dev) = @_; 73 | 74 | $devre =~ s/\+$/\.\*/; 75 | return ($dev =~ m/^${devre}$/) ? 1 : 0; 76 | } 77 | 78 | sub ipset_match { 79 | my ($ipset_ruleset, $ipsetname, $ipaddr) = @_; 80 | 81 | my $ipset = $ipset_ruleset->{$ipsetname}; 82 | die "no such ipset '$ipsetname'" if !$ipset; 83 | 84 | my $ip = Net::IP->new($ipaddr); 85 | 86 | my $first = $ipset->[0]; 87 | if ($first =~ m/^create\s+\S+\s+list:/) { 88 | foreach my $entry (@$ipset) { 89 | next if $entry =~ m/^create/; # simply ignore 90 | if ($entry =~ m/add \S+ (\S+)$/) { 91 | return 1 if ipset_match($ipset_ruleset, $1, $ipaddr); 92 | } else { 93 | die "implement me"; 94 | } 95 | } 96 | return 0; 97 | } elsif ($first =~ m/^create\s+\S+\s+hash:net/) { 98 | foreach my $entry (@$ipset) { 99 | next if $entry =~ m/^create/; # simply ignore 100 | if ($entry =~ m/add \S+ (\S+)$/) { 101 | my $test = Net::IP->new($1); 102 | if ($test->overlaps($ip)) { 103 | add_trace("IPSET $ipsetname match $ipaddr\n"); 104 | return 1; 105 | } 106 | } else { 107 | die "implement me"; 108 | } 109 | } 110 | return 0; 111 | } else { 112 | die "unknown ipset type '$first' - not implemented\n"; 113 | } 114 | 115 | return 0; 116 | } 117 | 118 | sub rule_match { 119 | my ($ipset_ruleset, $chain, $rule, $pkg) = @_; 120 | 121 | $rule =~ s/^-A $chain +// || die "got strange rule: $rule"; 122 | 123 | while (length($rule)) { 124 | 125 | if ($rule =~ s/^-m conntrack --ctstate (\S+)\s*//) { 126 | my $cstate = $1; 127 | 128 | return undef if $cstate eq 'INVALID'; # no match 129 | return undef if $cstate eq 'RELATED,ESTABLISHED'; # no match 130 | 131 | next if $cstate =~ m/NEW/; 132 | 133 | die "cstate test '$cstate' not implemented\n"; 134 | } 135 | 136 | if ($rule =~ s/^-m addrtype --src-type (\S+)\s*//) { 137 | my $atype = $1; 138 | die "missing source address type (srctype)\n" 139 | if !$pkg->{srctype}; 140 | return undef if $atype ne $pkg->{srctype}; 141 | } 142 | 143 | if ($rule =~ s/^-m addrtype --dst-type (\S+)\s*//) { 144 | my $atype = $1; 145 | die "missing destination address type (dsttype)\n" 146 | if !$pkg->{dsttype}; 147 | return undef if $atype ne $pkg->{dsttype}; 148 | } 149 | 150 | if ($rule =~ s/^-m icmp(v6)? --icmp-type (\S+)\s*//) { 151 | my $icmpv6 = !!$1; 152 | my $icmptype = $2; 153 | die "missing destination address type (dsttype)\n" if !defined($pkg->{dport}); 154 | return undef if $icmptype ne $pkg->{dport}; 155 | } 156 | 157 | if ($rule =~ s/^-i (\S+)\s*//) { 158 | my $devre = $1; 159 | die "missing interface (iface_in)\n" if !$pkg->{iface_in}; 160 | return undef if !nf_dev_match($devre, $pkg->{iface_in}); 161 | next; 162 | } 163 | 164 | if ($rule =~ s/^-o (\S+)\s*//) { 165 | my $devre = $1; 166 | die "missing interface (iface_out)\n" if !$pkg->{iface_out}; 167 | return undef if !nf_dev_match($devre, $pkg->{iface_out}); 168 | next; 169 | } 170 | 171 | if ($rule =~ s/^-p (tcp|udp|igmp|icmp)\s*//) { 172 | die "missing proto" if !$pkg->{proto}; 173 | return undef if $pkg->{proto} ne $1; # no match 174 | next; 175 | } 176 | 177 | if ($rule =~ s/^--dport (\d+):(\d+)\s*//) { 178 | die "missing dport" if !$pkg->{dport}; 179 | return undef if ($pkg->{dport} < $1) || ($pkg->{dport} > $2); # no match 180 | next; 181 | } 182 | 183 | if ($rule =~ s/^--dport (\d+)\s*//) { 184 | die "missing dport" if !$pkg->{dport}; 185 | return undef if $pkg->{dport} != $1; # no match 186 | next; 187 | } 188 | 189 | if ($rule =~ s/^-s (\S+)\s*//) { 190 | die "missing source" if !$pkg->{source}; 191 | my $ip = Net::IP->new($1); 192 | return undef if !$ip->overlaps(Net::IP->new($pkg->{source})); # no match 193 | next; 194 | } 195 | 196 | if ($rule =~ s/^-d (\S+)\s*//) { 197 | die "missing destination" if !$pkg->{dest}; 198 | my $ip = Net::IP->new($1); 199 | return undef if !$ip->overlaps(Net::IP->new($pkg->{dest})); # no match 200 | next; 201 | } 202 | 203 | if ($rule =~ s/^-m set (!\s+)?--match-set (\S+) src\s*//) { 204 | die "missing source" if !$pkg->{source}; 205 | my $neg = $1; 206 | my $ipset_name = $2; 207 | if ($neg) { 208 | return undef if ipset_match($ipset_ruleset, $ipset_name, $pkg->{source}); 209 | } else { 210 | return undef if !ipset_match($ipset_ruleset, $ipset_name, $pkg->{source}); 211 | } 212 | next; 213 | } 214 | 215 | if ($rule =~ s/^-m set --match-set (\S+) dst\s*//) { 216 | die "missing destination" if !$pkg->{dest}; 217 | my $ipset_name = $1; 218 | return undef if !ipset_match($ipset_ruleset, $ipset_name, $pkg->{dest}); 219 | next; 220 | } 221 | 222 | if ($rule =~ s/^-m mac ! --mac-source (\S+)\s*//) { 223 | die "missing source mac" if !$pkg->{mac_source}; 224 | return undef if $pkg->{mac_source} eq $1; # no match 225 | next; 226 | } 227 | 228 | if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-in (\S+)\s*//) { 229 | my $devre = $1; 230 | return undef if !$pkg->{physdev_in}; 231 | return undef if !nf_dev_match($devre, $pkg->{physdev_in}); 232 | next; 233 | } 234 | 235 | if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-out (\S+)\s*//) { 236 | my $devre = $1; 237 | return undef if !$pkg->{physdev_out}; 238 | return undef if !nf_dev_match($devre, $pkg->{physdev_out}); 239 | next; 240 | } 241 | 242 | if ($rule =~ s@^-m mark --mark ($NUMBER_RE)(?:/($NUMBER_RE))?\s*@@) { 243 | my ($value, $mask) = PVE::Firewall::get_mark_values($1, $2); 244 | return undef if ($mark & $mask) != $value; 245 | next; 246 | } 247 | 248 | # final actions 249 | 250 | if ($rule =~ s@^-j MARK --set-mark ($NUMBER_RE)(?:/($NUMBER_RE))?\s*$@@) { 251 | my ($value, $mask) = PVE::Firewall::get_mark_values($1, $2); 252 | $mark = ($mark & ~$mask) | $value; 253 | return undef; 254 | } 255 | 256 | if ($rule =~ s/^-j (\S+)\s*$//) { 257 | return (0, $1); 258 | } 259 | 260 | if ($rule =~ s/^-g (\S+)\s*$//) { 261 | return (1, $1); 262 | } 263 | 264 | if ($rule =~ s/^-j NFLOG --nflog-prefix \"[^\"]+\"$//) { 265 | return undef; 266 | } 267 | 268 | last; 269 | } 270 | 271 | die "unable to parse rule: $rule"; 272 | } 273 | 274 | sub ruleset_simulate_chain { 275 | my ($ruleset, $ipset_ruleset, $chain, $pkg) = @_; 276 | 277 | add_trace("ENTER chain $chain\n"); 278 | 279 | my $counter = 0; 280 | 281 | if ($chain eq 'PVEFW-Drop') { 282 | add_trace("LEAVE chain $chain\n"); 283 | return ('DROP', $counter); 284 | } 285 | if ($chain eq 'PVEFW-reject') { 286 | add_trace("LEAVE chain $chain\n"); 287 | return ('REJECT', $counter); 288 | } 289 | 290 | if ($chain eq 'PVEFW-tcpflags') { 291 | add_trace("LEAVE chain $chain\n"); 292 | return (undef, $counter); 293 | } 294 | 295 | my $rules = $ruleset->{$chain} 296 | || die "no such chain '$chain'"; 297 | 298 | foreach my $rule (@$rules) { 299 | $counter++; 300 | my ($goto, $action) = rule_match($ipset_ruleset, $chain, $rule, $pkg); 301 | if (!defined($action)) { 302 | add_trace("SKIP: $rule\n"); 303 | next; 304 | } 305 | add_trace("MATCH: $rule\n"); 306 | 307 | if ($action eq 'ACCEPT' || $action eq 'DROP' || $action eq 'REJECT') { 308 | add_trace("TERMINATE chain $chain: $action\n"); 309 | return ($action, $counter); 310 | } elsif ($action eq 'RETURN') { 311 | add_trace("RETURN FROM chain $chain\n"); 312 | last; 313 | } else { 314 | if ($goto) { 315 | add_trace("LEAVE chain $chain - goto $action\n"); 316 | return ruleset_simulate_chain($ruleset, $ipset_ruleset, $action, $pkg) 317 | #$chain = $action; 318 | #$rules = $ruleset->{$chain} || die "no such chain '$chain'"; 319 | } else { 320 | my ($act, $ctr) = ruleset_simulate_chain($ruleset, $ipset_ruleset, $action, $pkg); 321 | $counter += $ctr; 322 | return ($act, $counter) if $act; 323 | add_trace("CONTINUE chain $chain\n"); 324 | } 325 | } 326 | } 327 | 328 | add_trace("LEAVE chain $chain\n"); 329 | if ($chain =~ m/^PVEFW-(INPUT|OUTPUT|FORWARD)$/) { 330 | return ('ACCEPT', $counter); # default policy 331 | } 332 | 333 | return (undef, $counter); 334 | } 335 | 336 | sub copy_packet { 337 | my ($pkg) = @_; 338 | 339 | my $res = {}; 340 | 341 | while (my ($k, $v) = each %$pkg) { 342 | $res->{$k} = $v; 343 | } 344 | 345 | return $res; 346 | } 347 | 348 | # Try to simulate packet traversal inside kernel. This invokes iptable 349 | # checks several times. 350 | sub route_packet { 351 | my ($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state) = @_; 352 | 353 | $pkg->{ipversion} = 4; # fixme: allow ipv6 354 | 355 | my $route_state = $start_state; 356 | 357 | my $physdev_in; 358 | 359 | my $ipt_invocation_counter = 0; 360 | my $rule_check_counter = 0; 361 | 362 | while ($route_state ne $target->{iface}) { 363 | 364 | my $chain; 365 | my $next_route_state; 366 | my $next_physdev_in; 367 | 368 | $pkg->{iface_in} = $pkg->{iface_out} = undef; 369 | $pkg->{physdev_in} = $pkg->{physdev_out} = undef; 370 | 371 | if ($route_state eq 'from-bport') { 372 | $next_route_state = $from_info->{bridge} || die 'internal error'; 373 | $next_physdev_in = $from_info->{iface} || die 'internal error'; 374 | } elsif ($route_state eq 'host') { 375 | 376 | if ($target->{type} eq 'bport') { 377 | $pkg->{iface_in} = 'lo'; 378 | $pkg->{iface_out} = $target->{bridge} || die 'internal error'; 379 | $chain = 'PVEFW-OUTPUT'; 380 | $next_route_state = $target->{iface} || die 'internal error'; 381 | } elsif ($target->{type} eq 'vm' || $target->{type} eq 'ct') { 382 | $pkg->{iface_in} = 'lo'; 383 | $pkg->{iface_out} = $target->{bridge} || die 'internal error'; 384 | $chain = 'PVEFW-OUTPUT'; 385 | $next_route_state = 'fwbr-in'; 386 | } else { 387 | die "implement me"; 388 | } 389 | 390 | } elsif ($route_state eq 'fwbr-out') { 391 | 392 | $chain = 'PVEFW-FORWARD'; 393 | $next_route_state = $from_info->{bridge} || die 'internal error'; 394 | $next_physdev_in = $from_info->{fwpr} || die 'internal error'; 395 | $pkg->{iface_in} = $from_info->{fwbr} || die 'internal error'; 396 | $pkg->{iface_out} = $from_info->{fwbr} || die 'internal error'; 397 | $pkg->{physdev_in} = $from_info->{tapdev} || die 'internal error'; 398 | $pkg->{physdev_out} = $from_info->{fwln} || die 'internal error'; 399 | 400 | } elsif ($route_state eq 'fwbr-in') { 401 | 402 | $chain = 'PVEFW-FORWARD'; 403 | $next_route_state = $target->{tapdev}; 404 | $pkg->{iface_in} = $target->{fwbr} || die 'internal error'; 405 | $pkg->{iface_out} = $target->{fwbr} || die 'internal error'; 406 | $pkg->{physdev_in} = $target->{fwln} || die 'internal error'; 407 | $pkg->{physdev_out} = $target->{tapdev} || die 'internal error'; 408 | 409 | } elsif ($route_state =~ m/^$bridge_name_pattern$/) { 410 | 411 | die "missing physdev_in - internal error?" if !$physdev_in; 412 | $pkg->{physdev_in} = $physdev_in; 413 | 414 | if ($target->{type} eq 'host') { 415 | 416 | $chain = 'PVEFW-INPUT'; 417 | $pkg->{iface_in} = $route_state; 418 | $pkg->{iface_out} = 'lo'; 419 | $next_route_state = 'host'; 420 | 421 | } elsif ($target->{type} eq 'bport') { 422 | 423 | $chain = 'PVEFW-FORWARD'; 424 | $pkg->{iface_in} = $route_state; 425 | $pkg->{iface_out} = $target->{bridge} || die 'internal error'; 426 | # conditionally set physdev_out (same behavior as kernel) 427 | if ($route_state eq $target->{bridge}) { 428 | $pkg->{physdev_out} = $target->{iface} || die 'internal error'; 429 | } 430 | $next_route_state = $target->{iface}; 431 | 432 | } elsif ($target->{type} eq 'vm' || $target->{type} eq 'ct') { 433 | 434 | $chain = 'PVEFW-FORWARD'; 435 | $pkg->{iface_in} = $route_state; 436 | $pkg->{iface_out} = $target->{bridge}; 437 | # conditionally set physdev_out (same behavior as kernel) 438 | if ($route_state eq $target->{bridge}) { 439 | $pkg->{physdev_out} = $target->{fwpr} || die 'internal error'; 440 | } 441 | $next_route_state = 'fwbr-in'; 442 | 443 | } else { 444 | die "implement me"; 445 | } 446 | 447 | } else { 448 | die "implement me $route_state"; 449 | } 450 | 451 | die "internal error" if !defined($next_route_state); 452 | 453 | if ($chain) { 454 | add_trace("IPT check at $route_state (chain $chain)\n"); 455 | add_trace(Dumper($pkg)); 456 | $ipt_invocation_counter++; 457 | my ($res, $ctr) = ruleset_simulate_chain($ruleset, $ipset_ruleset, $chain, $pkg); 458 | $rule_check_counter += $ctr; 459 | return ($res, $ipt_invocation_counter, $rule_check_counter) if $res ne 'ACCEPT'; 460 | } 461 | 462 | $route_state = $next_route_state; 463 | 464 | $physdev_in = $next_physdev_in; 465 | } 466 | 467 | return ('ACCEPT', $ipt_invocation_counter, $rule_check_counter); 468 | } 469 | 470 | sub extract_ct_info { 471 | my ($vmdata, $vmid, $netnum) = @_; 472 | 473 | my $info = { type => 'ct', vmid => $vmid }; 474 | 475 | my $conf = $vmdata->{lxc}->{$vmid} || die "no such CT '$vmid'"; 476 | my $net = PVE::LXC::Config->parse_lxc_network($conf->{"net$netnum"}); 477 | $info->{macaddr} = $net->{hwaddr} || die "unable to get mac address"; 478 | $info->{bridge} = $net->{bridge} || die "unable to get bridge"; 479 | $info->{fwbr} = "fwbr${vmid}i$netnum"; 480 | $info->{tapdev} = "veth${vmid}i$netnum"; 481 | $info->{fwln} = "fwln${vmid}i$netnum"; 482 | $info->{fwpr} = "fwpr${vmid}p$netnum"; 483 | $info->{ip_address} = $net->{ip} || die "unable to get ip address"; 484 | 485 | return $info; 486 | } 487 | 488 | sub extract_vm_info { 489 | my ($vmdata, $vmid, $netnum) = @_; 490 | 491 | my $info = { type => 'vm', vmid => $vmid }; 492 | 493 | my $conf = $vmdata->{qemu}->{$vmid} || die "no such VM '$vmid'"; 494 | my $net = PVE::QemuServer::Network::parse_net($conf->{"net$netnum"}); 495 | $info->{macaddr} = $net->{macaddr} || die "unable to get mac address"; 496 | $info->{bridge} = $net->{bridge} || die "unable to get bridge"; 497 | $info->{fwbr} = "fwbr${vmid}i$netnum"; 498 | $info->{tapdev} = "tap${vmid}i$netnum"; 499 | $info->{fwln} = "fwln${vmid}i$netnum"; 500 | $info->{fwpr} = "fwpr${vmid}p$netnum"; 501 | 502 | return $info; 503 | } 504 | 505 | sub simulate_firewall { 506 | my ($ruleset, $ipset_ruleset, $host_ip, $vmdata, $test) = @_; 507 | 508 | my $from = $test->{from} || die "missing 'from' field"; 509 | my $to = $test->{to} || die "missing 'to' field"; 510 | my $action = $test->{action} || die "missing 'action'"; 511 | 512 | my $testid = $test->{id}; 513 | 514 | die "from/to needs to be different" if $from eq $to; 515 | 516 | my $pkg = { 517 | proto => 'tcp', 518 | sport => undef, 519 | dport => undef, 520 | source => undef, 521 | dest => undef, 522 | srctype => 'UNICAST', 523 | dsttype => 'UNICAST', 524 | }; 525 | 526 | while (my ($k, $v) = each %$test) { 527 | next if $k eq 'from'; 528 | next if $k eq 'to'; 529 | next if $k eq 'action'; 530 | next if $k eq 'id'; 531 | die "unknown attribute '$k'\n" if !exists($pkg->{$k}); 532 | $pkg->{$k} = $v; 533 | } 534 | 535 | my $from_info = {}; 536 | 537 | my $start_state; 538 | 539 | if ($from eq 'host') { 540 | $from_info->{type} = 'host'; 541 | $start_state = 'host'; 542 | $pkg->{source} = $host_ip if !defined($pkg->{source}); 543 | } elsif ($from eq 'outside') { 544 | $from_info->{type} = 'bport'; 545 | $from_info->{bridge} = 'vmbr0'; 546 | $from_info->{iface} = 'eth0'; 547 | $start_state = 'from-bport'; 548 | } elsif ($from eq 'nfvm') { 549 | $from_info->{type} = 'bport'; 550 | $from_info->{bridge} = 'vmbr0'; 551 | $from_info->{iface} = 'tapXYZ'; 552 | $start_state = 'from-bport'; 553 | } elsif ($from =~ m/^ct(\d+)$/) { 554 | return 'SKIPPED' if !$have_lxc; 555 | my $vmid = $1; 556 | $from_info = extract_ct_info($vmdata, $vmid, 0); 557 | $start_state = 'fwbr-out'; 558 | $pkg->{mac_source} = $from_info->{macaddr}; 559 | } elsif ($from =~ m/^vm(\d+)(i(\d))?$/) { 560 | return 'SKIPPED' if !$have_qemu_server; 561 | my $vmid = $1; 562 | my $netnum = $3 || 0; 563 | $from_info = extract_vm_info($vmdata, $vmid, $netnum); 564 | $start_state = 'fwbr-out'; 565 | $pkg->{mac_source} = $from_info->{macaddr}; 566 | } elsif ($from =~ m|^$bridge_interface_pattern$|) { 567 | $from_info->{type} = 'bport'; 568 | $from_info->{bridge} = $1; 569 | $from_info->{iface} = $2; 570 | $start_state = 'from-bport'; 571 | } else { 572 | die "unable to parse \"from => '$from'\"\n"; 573 | } 574 | 575 | my $target; 576 | 577 | if ($to eq 'host') { 578 | $target->{type} = 'host'; 579 | $target->{iface} = 'host'; 580 | $pkg->{dest} = $host_ip if !defined($pkg->{dest}); 581 | } elsif ($to eq 'outside') { 582 | $target->{type} = 'bport'; 583 | $target->{bridge} = 'vmbr0'; 584 | $target->{iface} = 'eth0'; 585 | } elsif ($to eq 'nfvm') { 586 | $target->{type} = 'bport'; 587 | $target->{bridge} = 'vmbr0'; 588 | $target->{iface} = 'tapXYZ'; 589 | } elsif ($to =~ m/^ct(\d+)$/) { 590 | return 'SKIPPED' if !$have_lxc; 591 | my $vmid = $1; 592 | $target = extract_ct_info($vmdata, $vmid, 0); 593 | $target->{iface} = $target->{tapdev}; 594 | } elsif ($to =~ m/^vm(\d+)$/) { 595 | return 'SKIPPED' if !$have_qemu_server; 596 | my $vmid = $1; 597 | $target = extract_vm_info($vmdata, $vmid, 0); 598 | $target->{iface} = $target->{tapdev}; 599 | } elsif ($to =~ m|^$bridge_interface_pattern$|) { 600 | $target->{type} = 'bport'; 601 | $target->{bridge} = $1; 602 | $target->{iface} = $2; 603 | } else { 604 | die "unable to parse \"to => '$to'\"\n"; 605 | } 606 | 607 | $pkg->{source} = '100.100.1.2' if !defined($pkg->{source}); 608 | $pkg->{dest} = '100.200.3.4' if !defined($pkg->{dest}); 609 | 610 | my ($res, $ic, $rc) = 611 | route_packet($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state); 612 | 613 | add_trace("IPT statistics: invocation = $ic, checks = $rc\n"); 614 | 615 | return $res if $action eq 'QUERY'; 616 | 617 | die "test failed ($res != $action)\n" if $action ne $res; 618 | 619 | return undef; 620 | } 621 | 622 | 1; 623 | 624 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/Rules.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::RulesBase; 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use PVE::JSONSchema qw(get_standard_option); 7 | use PVE::Exception qw(raise raise_param_exc); 8 | 9 | use PVE::Firewall; 10 | use PVE::API2::Firewall::Helpers; 11 | 12 | use base qw(PVE::RESTHandler); 13 | 14 | my $api_properties = { 15 | pos => { 16 | description => "Rule position.", 17 | type => 'integer', 18 | minimum => 0, 19 | }, 20 | }; 21 | 22 | my $rule_return_properties = { 23 | action => { 24 | description => "Rule action ('ACCEPT', 'DROP', 'REJECT') or security group name", 25 | type => 'string', 26 | }, 27 | comment => { 28 | description => 'Descriptive comment', 29 | type => 'string', 30 | optional => 1, 31 | }, 32 | dest => { 33 | description => 'Restrict packet destination address', 34 | type => 'string', 35 | optional => 1, 36 | }, 37 | dport => { 38 | description => 'Restrict TCP/UDP destination port', 39 | type => 'string', 40 | optional => 1, 41 | }, 42 | enable => { 43 | description => 'Flag to enable/disable a rule', 44 | type => 'integer', 45 | optional => 1, 46 | }, 47 | log => PVE::Firewall::get_standard_option( 48 | 'pve-fw-loglevel', 49 | { 50 | description => 'Log level for firewall rule', 51 | }, 52 | ), 53 | 'icmp-type' => { 54 | description => 55 | "Specify icmp-type. Only valid if proto equals 'icmp' or 'icmpv6'/'ipv6-icmp'", 56 | type => 'string', 57 | optional => 1, 58 | }, 59 | iface => { 60 | description => 61 | 'Network interface name. You have to use network configuration key names for VMs and containers', 62 | type => 'string', 63 | optional => 1, 64 | }, 65 | ipversion => { 66 | description => 67 | 'IP version (4 or 6) - automatically determined from source/dest addresses', 68 | type => 'integer', 69 | optional => 1, 70 | }, 71 | macro => { 72 | description => 'Use predefined standard macro', 73 | type => 'string', 74 | optional => 1, 75 | }, 76 | pos => { 77 | description => 'Rule position in the ruleset', 78 | type => 'integer', 79 | }, 80 | proto => { 81 | description => 82 | "IP protocol. You can use protocol names ('tcp'/'udp') or simple numbers, as defined in '/etc/protocols'", 83 | type => 'string', 84 | optional => 1, 85 | }, 86 | source => { 87 | description => 'Restrict packet source address', 88 | type => 'string', 89 | optional => 1, 90 | }, 91 | sport => { 92 | description => 'Restrict TCP/UDP source port', 93 | type => 'string', 94 | optional => 1, 95 | }, 96 | type => { 97 | description => 'Rule type', 98 | type => 'string', 99 | }, 100 | }; 101 | 102 | =head3 check_privileges_for_method($class, $method_name, $param) 103 | 104 | If the permission checks from the register_method() call are not sufficient, 105 | this function can be overriden for performing additional permission checks 106 | before API methods are executed. If the permission check fails, this function 107 | should die with an appropriate error message. The name of the method calling 108 | this function is provided by C<$method_name> and the parameters of the API 109 | method are provided by C<$param> 110 | 111 | Default implementation is a no-op to preserve backwards compatibility with 112 | existing subclasses, since this got added later on. It preserves existing 113 | behavior without having to change every subclass. 114 | 115 | =cut 116 | 117 | sub check_privileges_for_method { 118 | my ($class, $method_name, $param) = @_; 119 | } 120 | 121 | sub lock_config { 122 | my ($class, $param, $code) = @_; 123 | 124 | die "implement this in subclass"; 125 | } 126 | 127 | sub load_config { 128 | my ($class, $param) = @_; 129 | 130 | die "implement this in subclass"; 131 | 132 | #return ($cluster_conf, $fw_conf, $rules); 133 | } 134 | 135 | sub save_rules { 136 | my ($class, $param, $fw_conf, $rules) = @_; 137 | 138 | die "implement this in subclass"; 139 | } 140 | 141 | my $additional_param_hash = {}; 142 | 143 | sub rule_env { 144 | my ($class, $param) = @_; 145 | 146 | die "implement this in subclass"; 147 | } 148 | 149 | sub additional_parameters { 150 | my ($class, $new_value) = @_; 151 | 152 | if (defined($new_value)) { 153 | $additional_param_hash->{$class} = $new_value; 154 | } 155 | 156 | # return a copy 157 | my $copy = {}; 158 | my $org = $additional_param_hash->{$class} || {}; 159 | foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; } 160 | return $copy; 161 | } 162 | 163 | sub register_get_rules { 164 | my ($class) = @_; 165 | 166 | my $properties = $class->additional_parameters(); 167 | 168 | my $rule_env = $class->rule_env(); 169 | 170 | $class->register_method({ 171 | name => 'get_rules', 172 | path => '', 173 | method => 'GET', 174 | description => "List rules.", 175 | permissions => PVE::Firewall::rules_audit_permissions($rule_env), 176 | parameters => { 177 | additionalProperties => 0, 178 | properties => $properties, 179 | }, 180 | proxyto => $rule_env eq 'host' ? 'node' : undef, 181 | returns => { 182 | type => 'array', 183 | items => { 184 | type => "object", 185 | properties => $rule_return_properties, 186 | }, 187 | links => [{ rel => 'child', href => "{pos}" }], 188 | }, 189 | code => sub { 190 | my ($param) = @_; 191 | 192 | $class->check_privileges_for_method('get_rules', $param); 193 | 194 | my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param); 195 | 196 | my ($list, $digest) = PVE::Firewall::copy_list_with_digest($rules); 197 | 198 | my $ind = 0; 199 | foreach my $rule (@$list) { 200 | $rule->{pos} = $ind++; 201 | } 202 | 203 | return $list; 204 | }, 205 | }); 206 | } 207 | 208 | sub register_get_rule { 209 | my ($class) = @_; 210 | 211 | my $properties = $class->additional_parameters(); 212 | 213 | $properties->{pos} = $api_properties->{pos}; 214 | 215 | my $rule_env = $class->rule_env(); 216 | 217 | $class->register_method({ 218 | name => 'get_rule', 219 | path => '{pos}', 220 | method => 'GET', 221 | description => "Get single rule data.", 222 | permissions => PVE::Firewall::rules_audit_permissions($rule_env), 223 | parameters => { 224 | additionalProperties => 0, 225 | properties => $properties, 226 | }, 227 | proxyto => $rule_env eq 'host' ? 'node' : undef, 228 | returns => { 229 | type => "object", 230 | properties => $rule_return_properties, 231 | }, 232 | code => sub { 233 | my ($param) = @_; 234 | 235 | $class->check_privileges_for_method('get_rule', $param); 236 | 237 | my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param); 238 | 239 | my ($list, $digest) = PVE::Firewall::copy_list_with_digest($rules); 240 | 241 | die "no rule at position $param->{pos}\n" if $param->{pos} >= scalar(@$list); 242 | 243 | my $rule = $list->[$param->{pos}]; 244 | $rule->{pos} = $param->{pos}; 245 | 246 | return $rule; 247 | }, 248 | }); 249 | } 250 | 251 | sub register_create_rule { 252 | my ($class) = @_; 253 | 254 | my $properties = $class->additional_parameters(); 255 | 256 | my $create_rule_properties = PVE::Firewall::add_rule_properties($properties); 257 | $create_rule_properties->{action}->{optional} = 0; 258 | $create_rule_properties->{type}->{optional} = 0; 259 | 260 | my $rule_env = $class->rule_env(); 261 | 262 | $class->register_method({ 263 | name => 'create_rule', 264 | path => '', 265 | method => 'POST', 266 | description => "Create new rule.", 267 | protected => 1, 268 | permissions => PVE::Firewall::rules_modify_permissions($rule_env), 269 | parameters => { 270 | additionalProperties => 0, 271 | properties => $create_rule_properties, 272 | }, 273 | proxyto => $rule_env eq 'host' ? 'node' : undef, 274 | returns => { type => "null" }, 275 | code => sub { 276 | my ($param) = @_; 277 | 278 | $class->check_privileges_for_method('create_rule', $param); 279 | 280 | $class->lock_config( 281 | $param, 282 | sub { 283 | my ($param) = @_; 284 | 285 | my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param); 286 | 287 | my $rule = {}; 288 | 289 | # reloading the scoped SDN config for verification, so users can 290 | # only use IPSets they have permissions for 291 | my $allowed_vms = PVE::API2::Firewall::Helpers::get_allowed_vms(); 292 | my $allowed_vnets = PVE::API2::Firewall::Helpers::get_allowed_vnets(); 293 | my $sdn_conf = PVE::Firewall::load_sdn_conf($allowed_vms, $allowed_vnets); 294 | 295 | if ($cluster_conf) { 296 | $cluster_conf->{sdn} = $sdn_conf; 297 | } else { 298 | $fw_conf->{sdn} = $sdn_conf; 299 | } 300 | 301 | PVE::Firewall::copy_rule_data($rule, $param); 302 | PVE::Firewall::verify_rule( 303 | $rule, $cluster_conf, $fw_conf, $class->rule_env(), 304 | ); 305 | 306 | $rule->{enable} = 0 if !defined($param->{enable}); 307 | 308 | unshift @$rules, $rule; 309 | 310 | $class->save_rules($param, $fw_conf, $rules); 311 | }, 312 | ); 313 | 314 | return undef; 315 | }, 316 | }); 317 | } 318 | 319 | sub register_update_rule { 320 | my ($class) = @_; 321 | 322 | my $properties = $class->additional_parameters(); 323 | 324 | $properties->{pos} = $api_properties->{pos}; 325 | 326 | my $rule_env = $class->rule_env(); 327 | 328 | $properties->{moveto} = { 329 | description => "Move rule to new position . Other arguments are ignored.", 330 | type => 'integer', 331 | minimum => 0, 332 | optional => 1, 333 | }; 334 | 335 | $properties->{delete} = { 336 | type => 'string', 337 | format => 'pve-configid-list', 338 | description => "A list of settings you want to delete.", 339 | optional => 1, 340 | }; 341 | 342 | my $update_rule_properties = PVE::Firewall::add_rule_properties($properties); 343 | 344 | $class->register_method({ 345 | name => 'update_rule', 346 | path => '{pos}', 347 | method => 'PUT', 348 | description => "Modify rule data.", 349 | protected => 1, 350 | permissions => PVE::Firewall::rules_modify_permissions($rule_env), 351 | parameters => { 352 | additionalProperties => 0, 353 | properties => $update_rule_properties, 354 | }, 355 | proxyto => $rule_env eq 'host' ? 'node' : undef, 356 | returns => { type => "null" }, 357 | code => sub { 358 | my ($param) = @_; 359 | 360 | $class->check_privileges_for_method('update_rule', $param); 361 | 362 | $class->lock_config( 363 | $param, 364 | sub { 365 | my ($param) = @_; 366 | 367 | my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param); 368 | 369 | my (undef, $digest) = PVE::Firewall::copy_list_with_digest($rules); 370 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 371 | 372 | die "no rule at position $param->{pos}\n" 373 | if $param->{pos} >= scalar(@$rules); 374 | 375 | my $rule = $rules->[$param->{pos}]; 376 | 377 | my $moveto = $param->{moveto}; 378 | if (defined($moveto) && $moveto != $param->{pos}) { 379 | my $newrules = []; 380 | for (my $i = 0; $i < scalar(@$rules); $i++) { 381 | next if $i == $param->{pos}; 382 | if ($i == $moveto) { 383 | push @$newrules, $rule; 384 | } 385 | push @$newrules, $rules->[$i]; 386 | } 387 | push @$newrules, $rule if $moveto >= scalar(@$rules); 388 | $rules = $newrules; 389 | } else { 390 | PVE::Firewall::copy_rule_data($rule, $param); 391 | 392 | PVE::Firewall::delete_rule_properties($rule, $param->{'delete'}) 393 | if $param->{'delete'}; 394 | 395 | # reloading the scoped SDN config for verification, so users can 396 | # only use IPSets they have permissions for 397 | my $allowed_vms = PVE::API2::Firewall::Helpers::get_allowed_vms(); 398 | my $allowed_vnets = PVE::API2::Firewall::Helpers::get_allowed_vnets(); 399 | my $sdn_conf = 400 | PVE::Firewall::load_sdn_conf($allowed_vms, $allowed_vnets); 401 | 402 | if ($cluster_conf) { 403 | $cluster_conf->{sdn} = $sdn_conf; 404 | } else { 405 | $fw_conf->{sdn} = $sdn_conf; 406 | } 407 | 408 | PVE::Firewall::verify_rule( 409 | $rule, $cluster_conf, $fw_conf, $class->rule_env(), 410 | ); 411 | } 412 | 413 | $class->save_rules($param, $fw_conf, $rules); 414 | }, 415 | ); 416 | 417 | return undef; 418 | }, 419 | }); 420 | } 421 | 422 | sub register_delete_rule { 423 | my ($class) = @_; 424 | 425 | my $properties = $class->additional_parameters(); 426 | 427 | $properties->{pos} = $api_properties->{pos}; 428 | 429 | $properties->{digest} = get_standard_option('pve-config-digest'); 430 | 431 | my $rule_env = $class->rule_env(); 432 | 433 | $class->register_method({ 434 | name => 'delete_rule', 435 | path => '{pos}', 436 | method => 'DELETE', 437 | description => "Delete rule.", 438 | protected => 1, 439 | permissions => PVE::Firewall::rules_modify_permissions($rule_env), 440 | parameters => { 441 | additionalProperties => 0, 442 | properties => $properties, 443 | }, 444 | proxyto => $rule_env eq 'host' ? 'node' : undef, 445 | returns => { type => "null" }, 446 | code => sub { 447 | my ($param) = @_; 448 | 449 | $class->check_privileges_for_method('delete_rule', $param); 450 | 451 | $class->lock_config( 452 | $param, 453 | sub { 454 | my ($param) = @_; 455 | 456 | my ($cluster_conf, $fw_conf, $rules) = $class->load_config($param); 457 | 458 | my (undef, $digest) = PVE::Firewall::copy_list_with_digest($rules); 459 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 460 | 461 | die "no rule at position $param->{pos}\n" 462 | if $param->{pos} >= scalar(@$rules); 463 | 464 | splice(@$rules, $param->{pos}, 1); 465 | 466 | $class->save_rules($param, $fw_conf, $rules); 467 | }, 468 | ); 469 | 470 | return undef; 471 | }, 472 | }); 473 | } 474 | 475 | sub register_handlers { 476 | my ($class) = @_; 477 | 478 | $class->register_get_rules(); 479 | $class->register_get_rule(); 480 | $class->register_create_rule(); 481 | $class->register_update_rule(); 482 | $class->register_delete_rule(); 483 | } 484 | 485 | package PVE::API2::Firewall::GroupRules; 486 | 487 | use strict; 488 | use warnings; 489 | use PVE::JSONSchema qw(get_standard_option); 490 | 491 | use base qw(PVE::API2::Firewall::RulesBase); 492 | 493 | __PACKAGE__->additional_parameters({ group => get_standard_option('pve-security-group-name') }); 494 | 495 | sub rule_env { 496 | my ($class, $param) = @_; 497 | 498 | return 'group'; 499 | } 500 | 501 | sub lock_config { 502 | my ($class, $param, $code) = @_; 503 | 504 | PVE::Firewall::lock_clusterfw_conf(10, $code, $param); 505 | } 506 | 507 | sub load_config { 508 | my ($class, $param) = @_; 509 | 510 | my $fw_conf = PVE::Firewall::load_clusterfw_conf(); 511 | my $rules = $fw_conf->{groups}->{ $param->{group} }; 512 | die "no such security group '$param->{group}'\n" if !defined($rules); 513 | 514 | return (undef, $fw_conf, $rules); 515 | } 516 | 517 | sub save_rules { 518 | my ($class, $param, $fw_conf, $rules) = @_; 519 | 520 | if (!defined($rules)) { 521 | delete $fw_conf->{groups}->{ $param->{group} }; 522 | } else { 523 | $fw_conf->{groups}->{ $param->{group} } = $rules; 524 | } 525 | 526 | PVE::Firewall::save_clusterfw_conf($fw_conf); 527 | } 528 | 529 | __PACKAGE__->register_method({ 530 | name => 'delete_security_group', 531 | path => '', 532 | method => 'DELETE', 533 | description => "Delete security group.", 534 | protected => 1, 535 | permissions => { 536 | check => ['perm', '/', ['Sys.Modify']], 537 | }, 538 | parameters => { 539 | additionalProperties => 0, 540 | properties => { 541 | group => get_standard_option('pve-security-group-name'), 542 | }, 543 | }, 544 | returns => { type => 'null' }, 545 | code => sub { 546 | my ($param) = @_; 547 | 548 | __PACKAGE__->lock_config( 549 | $param, 550 | sub { 551 | my ($param) = @_; 552 | 553 | my (undef, $cluster_conf, $rules) = __PACKAGE__->load_config($param); 554 | 555 | die "Security group '$param->{group}' is not empty\n" 556 | if scalar(@$rules); 557 | 558 | __PACKAGE__->save_rules($param, $cluster_conf, undef); 559 | }, 560 | ); 561 | 562 | return undef; 563 | }, 564 | }); 565 | 566 | __PACKAGE__->register_handlers(); 567 | 568 | package PVE::API2::Firewall::ClusterRules; 569 | 570 | use strict; 571 | use warnings; 572 | 573 | use base qw(PVE::API2::Firewall::RulesBase); 574 | 575 | sub rule_env { 576 | my ($class, $param) = @_; 577 | 578 | return 'cluster'; 579 | } 580 | 581 | sub lock_config { 582 | my ($class, $param, $code) = @_; 583 | 584 | PVE::Firewall::lock_clusterfw_conf(10, $code, $param); 585 | } 586 | 587 | sub load_config { 588 | my ($class, $param) = @_; 589 | 590 | my $fw_conf = PVE::Firewall::load_clusterfw_conf(); 591 | my $rules = $fw_conf->{rules}; 592 | 593 | return (undef, $fw_conf, $rules); 594 | } 595 | 596 | sub save_rules { 597 | my ($class, $param, $fw_conf, $rules) = @_; 598 | 599 | $fw_conf->{rules} = $rules; 600 | PVE::Firewall::save_clusterfw_conf($fw_conf); 601 | } 602 | 603 | __PACKAGE__->register_handlers(); 604 | 605 | package PVE::API2::Firewall::HostRules; 606 | 607 | use strict; 608 | use warnings; 609 | use PVE::JSONSchema qw(get_standard_option); 610 | 611 | use base qw(PVE::API2::Firewall::RulesBase); 612 | 613 | __PACKAGE__->additional_parameters({ node => get_standard_option('pve-node') }); 614 | 615 | sub rule_env { 616 | my ($class, $param) = @_; 617 | 618 | return 'host'; 619 | } 620 | 621 | sub lock_config { 622 | my ($class, $param, $code) = @_; 623 | 624 | PVE::Firewall::lock_hostfw_conf(undef, 10, $code, $param); 625 | } 626 | 627 | sub load_config { 628 | my ($class, $param) = @_; 629 | 630 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 631 | my $fw_conf = PVE::Firewall::load_hostfw_conf($cluster_conf); 632 | my $rules = $fw_conf->{rules}; 633 | 634 | return ($cluster_conf, $fw_conf, $rules); 635 | } 636 | 637 | sub save_rules { 638 | my ($class, $param, $fw_conf, $rules) = @_; 639 | 640 | $fw_conf->{rules} = $rules; 641 | PVE::Firewall::save_hostfw_conf($fw_conf); 642 | } 643 | 644 | __PACKAGE__->register_handlers(); 645 | 646 | package PVE::API2::Firewall::VMRules; 647 | 648 | use strict; 649 | use warnings; 650 | use PVE::JSONSchema qw(get_standard_option); 651 | 652 | use base qw(PVE::API2::Firewall::RulesBase); 653 | 654 | __PACKAGE__->additional_parameters({ 655 | node => get_standard_option('pve-node'), 656 | vmid => get_standard_option('pve-vmid'), 657 | }); 658 | 659 | sub rule_env { 660 | my ($class, $param) = @_; 661 | 662 | return 'vm'; 663 | } 664 | 665 | sub lock_config { 666 | my ($class, $param, $code) = @_; 667 | 668 | PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param); 669 | } 670 | 671 | sub load_config { 672 | my ($class, $param) = @_; 673 | 674 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 675 | my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid}); 676 | my $rules = $fw_conf->{rules}; 677 | 678 | return ($cluster_conf, $fw_conf, $rules); 679 | } 680 | 681 | sub save_rules { 682 | my ($class, $param, $fw_conf, $rules) = @_; 683 | 684 | $fw_conf->{rules} = $rules; 685 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); 686 | } 687 | 688 | __PACKAGE__->register_handlers(); 689 | 690 | package PVE::API2::Firewall::CTRules; 691 | 692 | use strict; 693 | use warnings; 694 | use PVE::JSONSchema qw(get_standard_option); 695 | 696 | use base qw(PVE::API2::Firewall::RulesBase); 697 | 698 | __PACKAGE__->additional_parameters({ 699 | node => get_standard_option('pve-node'), 700 | vmid => get_standard_option('pve-vmid'), 701 | }); 702 | 703 | sub rule_env { 704 | my ($class, $param) = @_; 705 | 706 | return 'ct'; 707 | } 708 | 709 | sub lock_config { 710 | my ($class, $param, $code) = @_; 711 | 712 | PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param); 713 | } 714 | 715 | sub load_config { 716 | my ($class, $param) = @_; 717 | 718 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 719 | my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid}); 720 | my $rules = $fw_conf->{rules}; 721 | 722 | return ($cluster_conf, $fw_conf, $rules); 723 | } 724 | 725 | sub save_rules { 726 | my ($class, $param, $fw_conf, $rules) = @_; 727 | 728 | $fw_conf->{rules} = $rules; 729 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); 730 | } 731 | 732 | __PACKAGE__->register_handlers(); 733 | 734 | package PVE::API2::Firewall::VnetRules; 735 | 736 | use strict; 737 | use warnings; 738 | use PVE::JSONSchema qw(get_standard_option); 739 | 740 | use base qw(PVE::API2::Firewall::RulesBase); 741 | 742 | __PACKAGE__->additional_parameters({ 743 | vnet => get_standard_option('pve-sdn-vnet-id'), 744 | }); 745 | 746 | sub check_privileges_for_method { 747 | my ($class, $method_name, $param) = @_; 748 | 749 | if ($method_name eq 'get_rule' || $method_name eq 'get_rules') { 750 | PVE::API2::Firewall::Helpers::check_vnet_access( 751 | $param->{vnet}, 752 | ['SDN.Audit', 'SDN.Allocate'], 753 | ); 754 | } elsif ($method_name =~ '(update|create|delete)_rule') { 755 | PVE::API2::Firewall::Helpers::check_vnet_access($param->{vnet}, ['SDN.Allocate']); 756 | } else { 757 | die "unknown method: $method_name"; 758 | } 759 | } 760 | 761 | sub rule_env { 762 | my ($class, $param) = @_; 763 | 764 | return 'vnet'; 765 | } 766 | 767 | sub lock_config { 768 | my ($class, $param, $code) = @_; 769 | 770 | PVE::Firewall::lock_vnetfw_conf($param->{vnet}, 10, $code, $param); 771 | } 772 | 773 | sub load_config { 774 | my ($class, $param) = @_; 775 | 776 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 777 | my $fw_conf = PVE::Firewall::load_vnetfw_conf($cluster_conf, 'vnet', $param->{vnet}); 778 | my $rules = $fw_conf->{rules}; 779 | 780 | return ($cluster_conf, $fw_conf, $rules); 781 | } 782 | 783 | sub save_rules { 784 | my ($class, $param, $fw_conf, $rules) = @_; 785 | 786 | $fw_conf->{rules} = $rules; 787 | PVE::Firewall::save_vnetfw_conf($param->{vnet}, $fw_conf); 788 | } 789 | 790 | __PACKAGE__->register_handlers(); 791 | 792 | 1; 793 | -------------------------------------------------------------------------------- /src/PVE/API2/Firewall/IPSet.pm: -------------------------------------------------------------------------------- 1 | package PVE::API2::Firewall::IPSetBase; 2 | 3 | use strict; 4 | use warnings; 5 | use PVE::Exception qw(raise raise_param_exc); 6 | use PVE::JSONSchema qw(get_standard_option); 7 | 8 | use PVE::Firewall; 9 | 10 | use base qw(PVE::RESTHandler); 11 | 12 | my $api_properties = { 13 | cidr => { 14 | description => "Network/IP specification in CIDR format.", 15 | type => 'string', 16 | format => 'IPorCIDRorAlias', 17 | }, 18 | name => get_standard_option('ipset-name'), 19 | comment => { 20 | type => 'string', 21 | optional => 1, 22 | }, 23 | nomatch => { 24 | type => 'boolean', 25 | optional => 1, 26 | }, 27 | }; 28 | 29 | sub lock_config { 30 | my ($class, $param, $code) = @_; 31 | 32 | die "implement this in subclass"; 33 | } 34 | 35 | sub load_config { 36 | my ($class, $param) = @_; 37 | 38 | die "implement this in subclass"; 39 | 40 | #return ($cluster_conf, $fw_conf, $ipset); 41 | } 42 | 43 | sub save_config { 44 | my ($class, $param, $fw_conf) = @_; 45 | 46 | die "implement this in subclass"; 47 | } 48 | 49 | sub rule_env { 50 | my ($class, $param) = @_; 51 | 52 | die "implement this in subclass"; 53 | } 54 | 55 | sub save_ipset { 56 | my ($class, $param, $fw_conf, $ipset) = @_; 57 | 58 | if (!defined($ipset)) { 59 | delete $fw_conf->{ipset}->{ $param->{name} }; 60 | } else { 61 | $fw_conf->{ipset}->{ $param->{name} } = $ipset; 62 | } 63 | 64 | $class->save_config($param, $fw_conf); 65 | } 66 | 67 | my $additional_param_hash = {}; 68 | 69 | sub additional_parameters { 70 | my ($class, $new_value) = @_; 71 | 72 | if (defined($new_value)) { 73 | $additional_param_hash->{$class} = $new_value; 74 | } 75 | 76 | # return a copy 77 | my $copy = {}; 78 | my $org = $additional_param_hash->{$class} || {}; 79 | foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; } 80 | return $copy; 81 | } 82 | 83 | sub register_get_ipset { 84 | my ($class) = @_; 85 | 86 | my $properties = $class->additional_parameters(); 87 | 88 | $properties->{name} = $api_properties->{name}; 89 | 90 | $class->register_method({ 91 | name => 'get_ipset', 92 | path => '', 93 | method => 'GET', 94 | description => "List IPSet content", 95 | permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()), 96 | parameters => { 97 | additionalProperties => 0, 98 | properties => $properties, 99 | }, 100 | returns => { 101 | type => 'array', 102 | items => { 103 | type => "object", 104 | properties => { 105 | cidr => { 106 | type => 'string', 107 | }, 108 | comment => { 109 | type => 'string', 110 | optional => 1, 111 | }, 112 | nomatch => { 113 | type => 'boolean', 114 | optional => 1, 115 | }, 116 | digest => get_standard_option('pve-config-digest', { optional => 0 }), 117 | }, 118 | }, 119 | links => [{ rel => 'child', href => "{cidr}" }], 120 | }, 121 | code => sub { 122 | my ($param) = @_; 123 | 124 | my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); 125 | 126 | return PVE::Firewall::copy_list_with_digest($ipset); 127 | }, 128 | }); 129 | } 130 | 131 | sub register_delete_ipset { 132 | my ($class) = @_; 133 | 134 | my $properties = $class->additional_parameters(); 135 | 136 | $properties->{name} = get_standard_option('ipset-name'); 137 | $properties->{force} = { 138 | type => 'boolean', 139 | optional => 1, 140 | description => 'Delete all members of the IPSet, if there are any.', 141 | }; 142 | 143 | $class->register_method({ 144 | name => 'delete_ipset', 145 | path => '', 146 | method => 'DELETE', 147 | description => "Delete IPSet", 148 | protected => 1, 149 | permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), 150 | parameters => { 151 | additionalProperties => 0, 152 | properties => $properties, 153 | }, 154 | returns => { type => 'null' }, 155 | code => sub { 156 | my ($param) = @_; 157 | 158 | $class->lock_config( 159 | $param, 160 | sub { 161 | my ($param) = @_; 162 | 163 | my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); 164 | 165 | die "IPSet '$param->{name}' is not empty\n" 166 | if scalar(@$ipset) && !$param->{force}; 167 | 168 | $class->save_ipset($param, $fw_conf, undef); 169 | 170 | }, 171 | ); 172 | 173 | return undef; 174 | }, 175 | }); 176 | } 177 | 178 | sub register_create_ip { 179 | my ($class) = @_; 180 | 181 | my $properties = $class->additional_parameters(); 182 | 183 | $properties->{name} = $api_properties->{name}; 184 | $properties->{cidr} = $api_properties->{cidr}; 185 | $properties->{nomatch} = $api_properties->{nomatch}; 186 | $properties->{comment} = $api_properties->{comment}; 187 | 188 | $class->register_method({ 189 | name => 'create_ip', 190 | path => '', 191 | method => 'POST', 192 | description => "Add IP or Network to IPSet.", 193 | protected => 1, 194 | permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), 195 | parameters => { 196 | additionalProperties => 0, 197 | properties => $properties, 198 | }, 199 | returns => { type => "null" }, 200 | code => sub { 201 | my ($param) = @_; 202 | 203 | $class->lock_config( 204 | $param, 205 | sub { 206 | my ($param) = @_; 207 | 208 | my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); 209 | 210 | my $cidr = $param->{cidr}; 211 | if ($cidr =~ m@^(dc/|guest/)?(${PVE::Firewall::ip_alias_pattern})$@) { 212 | my $scope = $1 // ""; 213 | my $alias = $2; 214 | # make sure alias exists (if $cidr is an alias) 215 | PVE::Firewall::resolve_alias($cluster_conf, $fw_conf, $alias, $scope); 216 | } else { 217 | $cidr = PVE::Firewall::clean_cidr($cidr); 218 | # normalize like config parser, otherwise duplicates might slip through 219 | $cidr = PVE::Firewall::parse_ip_or_cidr($cidr); 220 | } 221 | 222 | foreach my $entry (@$ipset) { 223 | raise_param_exc({ cidr => "address '$cidr' already exists" }) 224 | if $entry->{cidr} eq $cidr; 225 | } 226 | 227 | raise_param_exc({ cidr => "a zero prefix is not allowed in ipset entries" }) 228 | if $cidr =~ m!/0+$!; 229 | 230 | my $data = { cidr => $cidr }; 231 | 232 | $data->{nomatch} = 1 if $param->{nomatch}; 233 | $data->{comment} = $param->{comment} if $param->{comment}; 234 | 235 | unshift @$ipset, $data; 236 | 237 | $class->save_ipset($param, $fw_conf, $ipset); 238 | 239 | }, 240 | ); 241 | 242 | return undef; 243 | }, 244 | }); 245 | } 246 | 247 | sub register_read_ip { 248 | my ($class) = @_; 249 | 250 | my $properties = $class->additional_parameters(); 251 | 252 | $properties->{name} = $api_properties->{name}; 253 | $properties->{cidr} = $api_properties->{cidr}; 254 | 255 | $class->register_method({ 256 | name => 'read_ip', 257 | path => '{cidr}', 258 | method => 'GET', 259 | description => "Read IP or Network settings from IPSet.", 260 | permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()), 261 | protected => 1, 262 | parameters => { 263 | additionalProperties => 0, 264 | properties => $properties, 265 | }, 266 | returns => { type => "object" }, 267 | code => sub { 268 | my ($param) = @_; 269 | 270 | my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); 271 | 272 | my $list = PVE::Firewall::copy_list_with_digest($ipset); 273 | 274 | foreach my $entry (@$list) { 275 | if ($entry->{cidr} eq $param->{cidr}) { 276 | return $entry; 277 | } 278 | } 279 | 280 | raise_param_exc({ cidr => "no such IP/Network" }); 281 | }, 282 | }); 283 | } 284 | 285 | sub register_update_ip { 286 | my ($class) = @_; 287 | 288 | my $properties = $class->additional_parameters(); 289 | 290 | $properties->{name} = $api_properties->{name}; 291 | $properties->{cidr} = $api_properties->{cidr}; 292 | $properties->{nomatch} = $api_properties->{nomatch}; 293 | $properties->{comment} = $api_properties->{comment}; 294 | $properties->{digest} = get_standard_option('pve-config-digest'); 295 | 296 | $class->register_method({ 297 | name => 'update_ip', 298 | path => '{cidr}', 299 | method => 'PUT', 300 | description => "Update IP or Network settings", 301 | protected => 1, 302 | permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), 303 | parameters => { 304 | additionalProperties => 0, 305 | properties => $properties, 306 | }, 307 | returns => { type => "null" }, 308 | code => sub { 309 | my ($param) = @_; 310 | 311 | my $found = $class->lock_config( 312 | $param, 313 | sub { 314 | my ($param) = @_; 315 | 316 | my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); 317 | 318 | my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset); 319 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 320 | 321 | foreach my $entry (@$ipset) { 322 | if ($entry->{cidr} eq $param->{cidr}) { 323 | $entry->{nomatch} = $param->{nomatch}; 324 | $entry->{comment} = $param->{comment}; 325 | $class->save_ipset($param, $fw_conf, $ipset); 326 | return 1; 327 | } 328 | } 329 | 330 | return 0; 331 | }, 332 | ); 333 | 334 | return if $found; 335 | 336 | raise_param_exc({ cidr => "no such IP/Network" }); 337 | }, 338 | }); 339 | } 340 | 341 | sub register_delete_ip { 342 | my ($class) = @_; 343 | 344 | my $properties = $class->additional_parameters(); 345 | 346 | $properties->{name} = $api_properties->{name}; 347 | $properties->{cidr} = $api_properties->{cidr}; 348 | $properties->{digest} = get_standard_option('pve-config-digest'); 349 | 350 | $class->register_method({ 351 | name => 'remove_ip', 352 | path => '{cidr}', 353 | method => 'DELETE', 354 | description => "Remove IP or Network from IPSet.", 355 | protected => 1, 356 | permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), 357 | parameters => { 358 | additionalProperties => 0, 359 | properties => $properties, 360 | }, 361 | returns => { type => "null" }, 362 | code => sub { 363 | my ($param) = @_; 364 | 365 | $class->lock_config( 366 | $param, 367 | sub { 368 | my ($param) = @_; 369 | 370 | my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param); 371 | 372 | my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset); 373 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 374 | 375 | my $new = []; 376 | 377 | foreach my $entry (@$ipset) { 378 | push @$new, $entry if $entry->{cidr} ne $param->{cidr}; 379 | } 380 | 381 | $class->save_ipset($param, $fw_conf, $new); 382 | }, 383 | ); 384 | 385 | return undef; 386 | }, 387 | }); 388 | } 389 | 390 | sub register_handlers { 391 | my ($class) = @_; 392 | 393 | $class->register_delete_ipset(); 394 | $class->register_get_ipset(); 395 | $class->register_create_ip(); 396 | $class->register_read_ip(); 397 | $class->register_update_ip(); 398 | $class->register_delete_ip(); 399 | } 400 | 401 | package PVE::API2::Firewall::ClusterIPset; 402 | 403 | use strict; 404 | use warnings; 405 | 406 | use base qw(PVE::API2::Firewall::IPSetBase); 407 | 408 | sub rule_env { 409 | my ($class, $param) = @_; 410 | 411 | return 'cluster'; 412 | } 413 | 414 | sub lock_config { 415 | my ($class, $param, $code) = @_; 416 | 417 | PVE::Firewall::lock_clusterfw_conf(10, $code, $param); 418 | } 419 | 420 | sub load_config { 421 | my ($class, $param) = @_; 422 | 423 | my $fw_conf = PVE::Firewall::load_clusterfw_conf(); 424 | my $ipset = $fw_conf->{ipset}->{ $param->{name} }; 425 | die "no such IPSet '$param->{name}'\n" if !defined($ipset); 426 | 427 | return (undef, $fw_conf, $ipset); 428 | } 429 | 430 | sub save_config { 431 | my ($class, $param, $fw_conf) = @_; 432 | 433 | PVE::Firewall::save_clusterfw_conf($fw_conf); 434 | } 435 | 436 | __PACKAGE__->register_handlers(); 437 | 438 | package PVE::API2::Firewall::VMIPset; 439 | 440 | use strict; 441 | use warnings; 442 | use PVE::JSONSchema qw(get_standard_option); 443 | 444 | use base qw(PVE::API2::Firewall::IPSetBase); 445 | 446 | sub rule_env { 447 | my ($class, $param) = @_; 448 | 449 | return 'vm'; 450 | } 451 | 452 | __PACKAGE__->additional_parameters({ 453 | node => get_standard_option('pve-node'), 454 | vmid => get_standard_option('pve-vmid'), 455 | }); 456 | 457 | sub lock_config { 458 | my ($class, $param, $code) = @_; 459 | 460 | PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param); 461 | } 462 | 463 | sub load_config { 464 | my ($class, $param) = @_; 465 | 466 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 467 | my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid}); 468 | my $ipset = $fw_conf->{ipset}->{ $param->{name} }; 469 | die "no such IPSet '$param->{name}'\n" if !defined($ipset); 470 | 471 | return ($cluster_conf, $fw_conf, $ipset); 472 | } 473 | 474 | sub save_config { 475 | my ($class, $param, $fw_conf) = @_; 476 | 477 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); 478 | } 479 | 480 | __PACKAGE__->register_handlers(); 481 | 482 | package PVE::API2::Firewall::CTIPset; 483 | 484 | use strict; 485 | use warnings; 486 | use PVE::JSONSchema qw(get_standard_option); 487 | 488 | use base qw(PVE::API2::Firewall::IPSetBase); 489 | 490 | sub rule_env { 491 | my ($class, $param) = @_; 492 | 493 | return 'ct'; 494 | } 495 | 496 | __PACKAGE__->additional_parameters({ 497 | node => get_standard_option('pve-node'), 498 | vmid => get_standard_option('pve-vmid'), 499 | }); 500 | 501 | sub lock_config { 502 | my ($class, $param, $code) = @_; 503 | 504 | PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param); 505 | } 506 | 507 | sub load_config { 508 | my ($class, $param) = @_; 509 | 510 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 511 | my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid}); 512 | my $ipset = $fw_conf->{ipset}->{ $param->{name} }; 513 | die "no such IPSet '$param->{name}'\n" if !defined($ipset); 514 | 515 | return ($cluster_conf, $fw_conf, $ipset); 516 | } 517 | 518 | sub save_config { 519 | my ($class, $param, $fw_conf) = @_; 520 | 521 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); 522 | } 523 | 524 | __PACKAGE__->register_handlers(); 525 | 526 | package PVE::API2::Firewall::BaseIPSetList; 527 | 528 | use strict; 529 | use warnings; 530 | use PVE::JSONSchema qw(get_standard_option); 531 | use PVE::Exception qw(raise_param_exc); 532 | use PVE::Firewall; 533 | 534 | use base qw(PVE::RESTHandler); 535 | 536 | sub lock_config { 537 | my ($class, $param, $code) = @_; 538 | 539 | die "implement this in subclass"; 540 | } 541 | 542 | sub load_config { 543 | my ($class, $param) = @_; 544 | 545 | die "implement this in subclass"; 546 | 547 | #return ($cluster_conf, $fw_conf); 548 | } 549 | 550 | sub save_config { 551 | my ($class, $param, $fw_conf) = @_; 552 | 553 | die "implement this in subclass"; 554 | } 555 | 556 | sub rule_env { 557 | my ($class, $param) = @_; 558 | 559 | die "implement this in subclass"; 560 | } 561 | 562 | my $additional_param_hash_list = {}; 563 | 564 | sub additional_parameters { 565 | my ($class, $new_value) = @_; 566 | 567 | if (defined($new_value)) { 568 | $additional_param_hash_list->{$class} = $new_value; 569 | } 570 | 571 | # return a copy 572 | my $copy = {}; 573 | my $org = $additional_param_hash_list->{$class} || {}; 574 | foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; } 575 | return $copy; 576 | } 577 | 578 | my $get_ipset_list = sub { 579 | my ($fw_conf) = @_; 580 | 581 | my $res = []; 582 | foreach my $name (sort keys %{ $fw_conf->{ipset} }) { 583 | my $data = { 584 | name => $name, 585 | }; 586 | if (my $comment = $fw_conf->{ipset_comments}->{$name}) { 587 | $data->{comment} = $comment; 588 | } 589 | push @$res, $data; 590 | } 591 | 592 | my ($list, $digest) = PVE::Firewall::copy_list_with_digest($res); 593 | 594 | return wantarray ? ($list, $digest) : $list; 595 | }; 596 | 597 | sub register_index { 598 | my ($class) = @_; 599 | 600 | my $properties = $class->additional_parameters(); 601 | 602 | $class->register_method({ 603 | name => 'ipset_index', 604 | path => '', 605 | method => 'GET', 606 | description => "List IPSets", 607 | permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()), 608 | parameters => { 609 | additionalProperties => 0, 610 | properties => $properties, 611 | }, 612 | returns => { 613 | type => 'array', 614 | items => { 615 | type => "object", 616 | properties => { 617 | name => get_standard_option('ipset-name'), 618 | digest => get_standard_option('pve-config-digest', { optional => 0 }), 619 | comment => { 620 | type => 'string', 621 | optional => 1, 622 | }, 623 | }, 624 | }, 625 | links => [{ rel => 'child', href => "{name}" }], 626 | }, 627 | code => sub { 628 | my ($param) = @_; 629 | 630 | my ($cluster_conf, $fw_conf) = $class->load_config($param); 631 | 632 | return &$get_ipset_list($fw_conf); 633 | }, 634 | }); 635 | } 636 | 637 | sub register_create { 638 | my ($class) = @_; 639 | 640 | my $properties = $class->additional_parameters(); 641 | 642 | $properties->{name} = get_standard_option('ipset-name'); 643 | 644 | $properties->{comment} = { type => 'string', optional => 1 }; 645 | 646 | $properties->{digest} = get_standard_option('pve-config-digest'); 647 | 648 | $properties->{rename} = get_standard_option( 649 | 'ipset-name', 650 | { 651 | description => 652 | "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.", 653 | optional => 1, 654 | }, 655 | ); 656 | 657 | $class->register_method({ 658 | name => 'create_ipset', 659 | path => '', 660 | method => 'POST', 661 | description => "Create new IPSet", 662 | protected => 1, 663 | permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()), 664 | parameters => { 665 | additionalProperties => 0, 666 | properties => $properties, 667 | }, 668 | returns => { type => 'null' }, 669 | code => sub { 670 | my ($param) = @_; 671 | 672 | $class->lock_config( 673 | $param, 674 | sub { 675 | my ($param) = @_; 676 | 677 | my ($cluster_conf, $fw_conf) = $class->load_config($param); 678 | 679 | if ($param->{rename}) { 680 | my (undef, $digest) = &$get_ipset_list($fw_conf); 681 | PVE::Tools::assert_if_modified($digest, $param->{digest}); 682 | 683 | raise_param_exc({ name => "IPSet '$param->{rename}' does not exist" }) 684 | if !$fw_conf->{ipset}->{ $param->{rename} }; 685 | 686 | # prevent overwriting existing ipset 687 | raise_param_exc({ name => "IPSet '$param->{name}' does already exist" }) 688 | if $fw_conf->{ipset}->{ $param->{name} } 689 | && $param->{name} ne $param->{rename}; 690 | 691 | my $data = delete $fw_conf->{ipset}->{ $param->{rename} }; 692 | $fw_conf->{ipset}->{ $param->{name} } = $data; 693 | if ( 694 | my $comment = 695 | delete $fw_conf->{ipset_comments}->{ $param->{rename} } 696 | ) { 697 | $fw_conf->{ipset_comments}->{ $param->{name} } = $comment; 698 | } 699 | $fw_conf->{ipset_comments}->{ $param->{name} } = $param->{comment} 700 | if defined($param->{comment}); 701 | } else { 702 | foreach my $name (keys %{ $fw_conf->{ipset} }) { 703 | raise_param_exc({ name => "IPSet '$name' already exists" }) 704 | if $name eq $param->{name}; 705 | } 706 | 707 | $fw_conf->{ipset}->{ $param->{name} } = []; 708 | $fw_conf->{ipset_comments}->{ $param->{name} } = $param->{comment} 709 | if defined($param->{comment}); 710 | } 711 | 712 | $class->save_config($param, $fw_conf); 713 | }, 714 | ); 715 | 716 | return undef; 717 | }, 718 | }); 719 | } 720 | 721 | sub register_handlers { 722 | my ($class) = @_; 723 | 724 | $class->register_index(); 725 | $class->register_create(); 726 | } 727 | 728 | package PVE::API2::Firewall::ClusterIPSetList; 729 | 730 | use strict; 731 | use warnings; 732 | use PVE::Firewall; 733 | 734 | use base qw(PVE::API2::Firewall::BaseIPSetList); 735 | 736 | sub rule_env { 737 | my ($class, $param) = @_; 738 | 739 | return 'cluster'; 740 | } 741 | 742 | sub lock_config { 743 | my ($class, $param, $code) = @_; 744 | 745 | PVE::Firewall::lock_clusterfw_conf(10, $code, $param); 746 | } 747 | 748 | sub load_config { 749 | my ($class, $param) = @_; 750 | 751 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 752 | return (undef, $cluster_conf); 753 | } 754 | 755 | sub save_config { 756 | my ($class, $param, $fw_conf) = @_; 757 | 758 | PVE::Firewall::save_clusterfw_conf($fw_conf); 759 | } 760 | 761 | __PACKAGE__->register_handlers(); 762 | 763 | __PACKAGE__->register_method({ 764 | subclass => "PVE::API2::Firewall::ClusterIPset", 765 | path => '{name}', 766 | # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' 767 | fragmentDelimiter => '', 768 | }); 769 | 770 | package PVE::API2::Firewall::VMIPSetList; 771 | 772 | use strict; 773 | use warnings; 774 | use PVE::JSONSchema qw(get_standard_option); 775 | use PVE::Firewall; 776 | 777 | use base qw(PVE::API2::Firewall::BaseIPSetList); 778 | 779 | __PACKAGE__->additional_parameters({ 780 | node => get_standard_option('pve-node'), 781 | vmid => get_standard_option('pve-vmid'), 782 | }); 783 | 784 | sub rule_env { 785 | my ($class, $param) = @_; 786 | 787 | return 'vm'; 788 | } 789 | 790 | sub lock_config { 791 | my ($class, $param, $code) = @_; 792 | 793 | PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param); 794 | } 795 | 796 | sub load_config { 797 | my ($class, $param) = @_; 798 | 799 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 800 | my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid}); 801 | return ($cluster_conf, $fw_conf); 802 | } 803 | 804 | sub save_config { 805 | my ($class, $param, $fw_conf) = @_; 806 | 807 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); 808 | } 809 | 810 | __PACKAGE__->register_handlers(); 811 | 812 | __PACKAGE__->register_method({ 813 | subclass => "PVE::API2::Firewall::VMIPset", 814 | path => '{name}', 815 | # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' 816 | fragmentDelimiter => '', 817 | }); 818 | 819 | package PVE::API2::Firewall::CTIPSetList; 820 | 821 | use strict; 822 | use warnings; 823 | use PVE::JSONSchema qw(get_standard_option); 824 | use PVE::Firewall; 825 | 826 | use base qw(PVE::API2::Firewall::BaseIPSetList); 827 | 828 | __PACKAGE__->additional_parameters({ 829 | node => get_standard_option('pve-node'), 830 | vmid => get_standard_option('pve-vmid'), 831 | }); 832 | 833 | sub rule_env { 834 | my ($class, $param) = @_; 835 | 836 | return 'ct'; 837 | } 838 | 839 | sub lock_config { 840 | my ($class, $param, $code) = @_; 841 | 842 | PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param); 843 | } 844 | 845 | sub load_config { 846 | my ($class, $param) = @_; 847 | 848 | my $cluster_conf = PVE::Firewall::load_clusterfw_conf(); 849 | my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid}); 850 | return ($cluster_conf, $fw_conf); 851 | } 852 | 853 | sub save_config { 854 | my ($class, $param, $fw_conf) = @_; 855 | 856 | PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf); 857 | } 858 | 859 | __PACKAGE__->register_handlers(); 860 | 861 | __PACKAGE__->register_method({ 862 | subclass => "PVE::API2::Firewall::CTIPset", 863 | path => '{name}', 864 | # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/' 865 | fragmentDelimiter => '', 866 | }); 867 | 868 | 1; 869 | --------------------------------------------------------------------------------