use Digest::SHA;
use PVE::Tools;
use PVE::QemuServer;
+use File::Basename;
use File::Path;
use IO::File;
use Net::IP;
use Data::Dumper;
my $pve_fw_lock_filename = "/var/lock/pvefw.lck";
+my $pve_fw_status_filename = "/var/lib/pve-firewall/pvefw.status";
+
+my $default_log_level = 'info';
+
+my $log_level_hash = {
+ debug => 7,
+ info => 6,
+ notice => 5,
+ warning => 4,
+ err => 3,
+ crit => 2,
+ alert => 1,
+ emerg => 0,
+};
# imported/converted from: /usr/share/shorewall/macro.*
my $pve_fw_macros = {
{ action => 'PARAM', proto => 'tcp', dport => '1723' },
],
'Ping' => [
- { action => 'PARAM', proto => 'icmp', dport => '8' },
+ { action => 'PARAM', proto => 'icmp', dport => 'echo-request' },
],
'PostgreSQL' => [
{ action => 'PARAM', proto => 'tcp', dport => '5432' },
],
'Trcrt' => [
{ action => 'PARAM', proto => 'udp', dport => '33434:33524' },
- { action => 'PARAM', proto => 'icmp', dport => '8' },
+ { action => 'PARAM', proto => 'icmp', dport => 'echo-request' },
],
'VNC' => [
{ action => 'PARAM', proto => 'tcp', dport => '5900:5909' },
# Drop DNS replies
{ action => 'DROP', proto => 'udp', sport => 53 },
],
- 'PVEFW-logflags' => [
- # same as shorewall logflags action. (fixme: enable/disable logging)
- "-j LOG --log-prefix \"logflags-dropped:\" --log-level 4 --log-ip-options",
- "-j DROP",
- ],
'PVEFW-tcpflags' => [
# same as shorewall tcpflags action.
# Packets arriving on this interface are checked for som illegal combinations of TCP flags
"-p tcp -m tcp --tcp-flags FIN,SYN FIN,SYN -g PVEFW-logflags",
"-p tcp -m tcp --sport 0 --tcp-flags FIN,SYN,RST,ACK SYN -g PVEFW-logflags",
],
- 'PVEFW-smurflog' => [
- # same as shorewall smurflog. (fixme: enable/disable logging)
- "-j LOG --log-prefix \"smurfs-dropped\" --log-level 4",
- "-j DROP",
- ],
'PVEFW-smurfs' => [
# same as shorewall smurfs action
# Filter packets for smurfs (packets with a broadcast address as the source).
}
sub ruleset_generate_rule {
- my ($ruleset, $chain, $rule, $goto) = @_;
+ my ($ruleset, $chain, $rule, $actions, $goto) = @_;
+
+ return if $rule->{disable};
my $cmd = '';
$cmd .= " -s $rule->{source}" if $rule->{source};
$cmd .= " -m iprange --dst-range" if $rule->{nbdest} && $rule->{nbdest} > 1;
$cmd .= " -d $rule->{dest}" if $rule->{dest};
- $cmd .= " -p $rule->{proto}" if $rule->{proto};
- if (($rule->{nbdport} && $rule->{nbdport} > 1) ||
- ($rule->{nbsport} && $rule->{nbsport} > 1)) {
- $cmd .= " --match multiport"
- }
+ if ($rule->{proto}) {
+ $cmd .= " -p $rule->{proto}";
- if ($rule->{dport}) {
- if ($rule->{proto} && $rule->{proto} eq 'icmp') {
- # Note: we use dport to store --icmp-type
- die "unknown icmp-type\n" if !$icmp_type_names->{$rule->{dport}};
- $cmd .= " -m icmp --icmp-type $rule->{dport}";
- } else {
- if ($rule->{nbdport} && $rule->{nbdport} > 1) {
- $cmd .= " --dports $rule->{dport}";
+ my $multiport = 0;
+ $multiport++ if $rule->{nbdport} && ($rule->{nbdport} > 1);
+ $multiport++ if $rule->{nbsport} && ($rule->{nbsport} > 1);
+
+ $cmd .= " --match multiport" if $multiport;
+
+ die "multiport: option '--sports' cannot be used together with '--dports'\n"
+ if ($multiport == 2) && ($rule->{dport} ne $rule->{sport});
+
+ if ($rule->{dport}) {
+ if ($rule->{proto} && $rule->{proto} eq 'icmp') {
+ # Note: we use dport to store --icmp-type
+ die "unknown icmp-type '$rule->{dport}'\n" if !defined($icmp_type_names->{$rule->{dport}});
+ $cmd .= " -m icmp --icmp-type $rule->{dport}";
} else {
- $cmd .= " --dport $rule->{dport}";
+ if ($rule->{nbdport} && $rule->{nbdport} > 1) {
+ if ($multiport == 2) {
+ $cmd .= " --ports $rule->{dport}";
+ } else {
+ $cmd .= " --dports $rule->{dport}";
+ }
+ } else {
+ $cmd .= " --dport $rule->{dport}";
+ }
}
}
- }
- if ($rule->{sport}) {
- if ($rule->{nbsport} && $rule->{nbsport} > 1) {
- $cmd .= " --sports $rule->{sport}";
- } else {
- $cmd .= " --sport $rule->{sport}";
+ if ($rule->{sport}) {
+ if ($rule->{nbsport} && $rule->{nbsport} > 1) {
+ $cmd .= " --sports $rule->{sport}" if $multiport != 2;
+ } else {
+ $cmd .= " --sport $rule->{sport}";
+ }
}
+ } elsif ($rule->{dport} || $rule->{sport}) {
+ warn "ignoring destination port '$rule->{dport}' - no protocol specified\n" if $rule->{dport};
+ warn "ignoring source port '$rule->{sport}' - no protocol specified\n" if $rule->{sport};
}
$cmd .= " -m addrtype --dst-type $rule->{dsttype}" if $rule->{dsttype};
if (my $action = $rule->{action}) {
+ $action = $actions->{$action} if defined($actions->{$action});
$goto = 1 if !defined($goto) && $action eq 'PVEFW-SET-ACCEPT-MARK';
$cmd .= $goto ? " -g $action" : " -j $action";
- };
+ }
ruleset_addrule($ruleset, $chain, $cmd) if $cmd;
}
ruleset_create_chain($ruleset, "$bridge-IN");
ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-bridged --physdev-is-out -j $bridge-IN");
ruleset_addrule($ruleset, "$bridge-FW", "-m mark --mark 1 -j ACCEPT");
+ # accept traffic to unmanaged bridge ports
+ ruleset_addrule($ruleset, "$bridge-FW", "-m physdev --physdev-is-bridged --physdev-is-out -j ACCEPT ");
}
}
sub generate_tap_rules_direction {
my ($ruleset, $group_rules, $iface, $netid, $macaddr, $vmfw_conf, $bridge, $direction) = @_;
- my $rules = $vmfw_conf->{lc($direction)};
+ my $lc_direction = lc($direction);
+ my $rules = $vmfw_conf->{$lc_direction};
+
my $options = $vmfw_conf->{options};
+ my $loglevel = get_option_log_level($options, "log_level_${lc_direction}");
my $tapchain = "$iface-$direction";
ruleset_addrule($ruleset, $tapchain, "-m conntrack --ctstate INVALID,NEW -j PVEFW-smurfs");
}
+ if (!(defined($options->{dhcp}) && $options->{dhcp} == 0)) {
+ ruleset_addrule($ruleset, $tapchain, "-p udp -m udp --dport 67:68 -j ACCEPT");
+ }
+
if ($options->{tcpflags}) {
ruleset_addrule($ruleset, $tapchain, "-p tcp -j PVEFW-tcpflags");
}
ruleset_addrule($ruleset, $tapchain, "-m mac ! --mac-source $macaddr -j DROP");
}
-
if ($rules) {
foreach my $rule (@$rules) {
next if $rule->{iface} && $rule->{iface} ne $netid;
ruleset_addrule($ruleset, $tapchain, "-m mark --mark 1 -j RETURN")
if $direction eq 'OUT';
} else {
- $rule->{action} = "PVEFW-SET-ACCEPT-MARK" if $rule->{action} eq 'ACCEPT' && $direction eq 'OUT';
- ruleset_generate_rule($ruleset, $tapchain, $rule);
+ if ($direction eq 'OUT') {
+ ruleset_generate_rule($ruleset, $tapchain, $rule,
+ { ACCEPT => "PVEFW-SET-ACCEPT-MARK", REJECT => "PVEFW-reject" });
+ } else {
+ ruleset_generate_rule($ruleset, $tapchain, $rule, { REJECT => "PVEFW-reject" });
+ }
}
}
}
ruleset_addrule($ruleset, $tapchain, "-j ACCEPT");
}
} elsif ($policy eq 'DROP') {
+
ruleset_addrule($ruleset, $tapchain, "-j PVEFW-Drop");
- ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-dropped: \" --log-level 4");
+
+ ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-dropped: \" --log-level $loglevel")
+ if defined($loglevel);
+
ruleset_addrule($ruleset, $tapchain, "-j DROP");
} elsif ($policy eq 'REJECT') {
ruleset_addrule($ruleset, $tapchain, "-j PVEFW-Reject");
- ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-reject: \" --log-level 4");
+
+ ruleset_addrule($ruleset, $tapchain, "-j LOG --log-prefix \"$tapchain-reject: \" --log-level $loglevel")
+ if defined($loglevel);
+
ruleset_addrule($ruleset, $tapchain, "-g PVEFW-reject");
} else {
# should not happen
# fixme: allow security groups
+ my $options = $rules->{options};
+
# host inbound firewall
my $chain = "PVEFW-HOST-IN";
ruleset_create_chain($ruleset, $chain);
+ my $loglevel = get_option_log_level($options, "log_level_in");
+
ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
ruleset_addrule($ruleset, $chain, "-i lo -j ACCEPT");
if ($rules->{in}) {
foreach my $rule (@{$rules->{in}}) {
# we use RETURN because we need to check also tap rules
- $rule->{action} = 'RETURN' if $rule->{action} eq 'ACCEPT';
- ruleset_generate_rule($ruleset, $chain, $rule);
+ ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => 'RETURN', REJECT => "PVEFW-reject" });
}
}
- ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-IN dropped: \" --log-level 4");
+ ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-IN dropped: \" --log-level $loglevel")
+ if defined($loglevel);
+
ruleset_addrule($ruleset, $chain, "-j DROP");
# host outbound firewall
$chain = "PVEFW-HOST-OUT";
ruleset_create_chain($ruleset, $chain);
+ $loglevel = get_option_log_level($options, "log_level_out");
+
ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate INVALID -j DROP");
ruleset_addrule($ruleset, $chain, "-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT");
ruleset_addrule($ruleset, $chain, "-o lo -j ACCEPT");
if ($rules->{out}) {
foreach my $rule (@{$rules->{out}}) {
# we use RETURN because we need to check also tap rules
- $rule->{action} = 'RETURN' if $rule->{action} eq 'ACCEPT';
- ruleset_generate_rule($ruleset, $chain, $rule);
+ ruleset_generate_rule($ruleset, $chain, $rule, { ACCEPT => 'RETURN', REJECT => "PVEFW-reject" });
}
}
- ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-OUT dropped: \" --log-level 4");
+ ruleset_addrule($ruleset, $chain, "-j LOG --log-prefix \"kvmhost-OUT dropped: \" --log-level $loglevel")
+ if defined($loglevel);
+
ruleset_addrule($ruleset, $chain, "-j DROP");
ruleset_addrule($ruleset, "PVEFW-OUTPUT", "-j PVEFW-HOST-OUT");
if ($rules->{in}) {
foreach my $rule (@{$rules->{in}}) {
- ruleset_generate_rule($ruleset, $chain, $rule);
+ ruleset_generate_rule($ruleset, $chain, $rule, { REJECT => "PVEFW-reject" });
}
}
if ($rules->{out}) {
foreach my $rule (@{$rules->{out}}) {
- # we go the PVEFW-SET-ACCEPT-MARK Instead of ACCEPT) because we need to
- # check also other tap rules (and group rules can be set on any bridge,
- # so we can't go to VMBRXX-IN)
- $rule->{action} = 'PVEFW-SET-ACCEPT-MARK' if $rule->{action} eq 'ACCEPT';
- ruleset_generate_rule($ruleset, $chain, $rule);
+ # we use PVEFW-SET-ACCEPT-MARK (Instead of ACCEPT) because we need to
+ # check also other tap rules later
+ ruleset_generate_rule($ruleset, $chain, $rule,
+ { ACCEPT => 'PVEFW-SET-ACCEPT-MARK', REJECT => "PVEFW-reject" });
}
}
}
my ($action, $iface, $source, $dest, $proto, $dport, $sport);
- $line =~ s/#.*$//;
+ # we can add single line comments to the end of the rule
+ my $comment = $1 if $line =~ s/#\s*(.*?)\s*$//;
+
+ # we can disable a rule when prefixed with '|'
+ my $disable = 1 if $line =~ s/^\|//;
my @data = split(/\s+/, $line);
my $expected_elements = $need_iface ? 7 : 6;
my $rules = [];
my $param = {
+ disable => $disable,
+ comment => $comment,
action => $action,
iface => $iface,
source => $source,
if ($macro) {
foreach my $templ (@$macro) {
my $rule = {};
+ my $param_used = {};
foreach my $k (keys %$templ) {
my $v = $templ->{$k};
if ($v eq 'PARAM') {
$v = $param->{$k};
+ $param_used->{$k} = 1;
} elsif ($v eq 'DEST') {
$v = $param->{dest};
+ $param_used->{dest} = 1;
} elsif ($v eq 'SOURCE') {
$v = $param->{source};
+ $param_used->{source} = 1;
}
die "missing parameter '$k' in macro '$macro_name'\n" if !defined($v);
$rule->{$k} = $v;
}
+ foreach my $k (keys %$param) {
+ next if !defined($param->{$k});
+ next if $param_used->{$k};
+ if (defined($rule->{$k})) {
+ die "parameter '$k' already define in macro (value = '$rule->{$k}')\n"
+ if $rule->{$k} ne $param->{$k};
+ } else {
+ $rule->{$k} = $param->{$k};
+ }
+ }
push @$rules, $rule;
}
} else {
return $rules;
}
-sub parse_fw_option {
+sub parse_vmfw_option {
+ my ($line) = @_;
+
+ my ($opt, $value);
+
+ my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
+
+ if ($line =~ m/^(enable|dhcp|macfilter|nosmurfs|tcpflags):\s*(0|1)\s*$/i) {
+ $opt = lc($1);
+ $value = int($2);
+ } elsif ($line =~ m/^(log_level_in|log_level_out):\s*(($loglevels)\s*)?$/i) {
+ $opt = lc($1);
+ $value = $2 ? lc($3) : '';
+ } elsif ($line =~ m/^(policy-(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
+ $opt = lc($1);
+ $value = uc($3);
+ } else {
+ chomp $line;
+ die "can't parse option '$line'\n"
+ }
+
+ return ($opt, $value);
+}
+
+sub parse_hostfw_option {
my ($line) = @_;
my ($opt, $value);
- if ($line =~ m/^(enable|macfilter|nosmurfs|tcpflags):\s*(0|1)\s*$/i) {
+ my $loglevels = "emerg|alert|crit|err|warning|notice|info|debug|nolog";
+
+ if ($line =~ m/^(enable|dhcp|nosmurfs|tcpflags):\s*(0|1)\s*$/i) {
$opt = lc($1);
$value = int($2);
+ } elsif ($line =~ m/^(log_level_in|log_level_out|tcp_flags_log_level|smurf_log_level):\s*(($loglevels)\s*)?$/i) {
+ $opt = lc($1);
+ $value = $2 ? lc($3) : '';
} elsif ($line =~ m/^(policy-(in|out)):\s*(ACCEPT|DROP|REJECT)\s*$/i) {
$opt = lc($1);
$value = uc($3);
if ($section eq 'options') {
eval {
- my ($opt, $value) = parse_fw_option($line);
+ my ($opt, $value) = parse_vmfw_option($line);
$res->{options}->{$opt} = $value;
};
warn "$prefix: $@" if $@;
sub parse_host_fw_rules {
my ($filename, $fh) = @_;
- my $res = { in => [], out => [] };
+ my $res = { in => [], out => [], options => {}};
my $section;
my $linenr = $fh->input_line_number();
my $prefix = "$filename (line $linenr)";
- if ($line =~ m/^\[(in|out)\]\s*$/i) {
+ if ($line =~ m/^\[(\S+)\]\s*$/i) {
$section = lc($1);
+ warn "$prefix: ignore unknown section '$section'\n" if !$res->{$section};
next;
}
if (!$section) {
next;
}
+ next if !$res->{$section}; # skip undefined section
+
+ if ($section eq 'options') {
+ eval {
+ print "PARSE:$line\n";
+ my ($opt, $value) = parse_hostfw_option($line);
+ $res->{options}->{$opt} = $value;
+ };
+ warn "$prefix: $@" if $@;
+ next;
+ }
+
my $rules;
eval { $rules = parse_fw_rule($line, 1, 1); };
if (my $err = $@) {
return $rules;
}
+sub get_option_log_level {
+ my ($options, $k) = @_;
+
+ my $v = $options->{$k};
+ $v = $default_log_level if !defined($v);
+
+ return undef if $v eq '' || $v eq 'nolog';
+
+ $v = $log_level_hash->{$v} if defined($log_level_hash->{$v});
+
+ return $v if ($v >= 0) && ($v <= 7);
+
+ warn "unknown log level ($k = '$v')\n";
+
+ return undef;
+}
+
sub generate_std_chains {
- my ($ruleset) = @_;
+ my ($ruleset, $options) = @_;
+
+ my $loglevel = get_option_log_level($options, 'smurf_log_level');
+
+ # same as shorewall smurflog.
+ if (defined($loglevel)) {
+ $pve_std_chains-> {'PVEFW-smurflog'} = [
+ "-j LOG --log-prefix \"smurfs-dropped\" --log-level $loglevel",
+ "-j DROP",
+ ];
+ } else {
+ $pve_std_chains-> {'PVEFW-smurflog'} = [ "-j DROP" ];
+ }
+
+ # same as shorewall logflags action.
+ $loglevel = get_option_log_level($options, 'tcp_flags_log_level');
+ if (defined($loglevel)) {
+ $pve_std_chains-> {'PVEFW-logflags'} = [
+ "-j LOG --log-prefix \"logflags-dropped:\" --log-level $loglevel --log-ip-options",
+ "-j DROP",
+ ];
+ } else {
+ $pve_std_chains-> {'PVEFW-logflags'} = [ "-j DROP" ];
+ }
foreach my $chain (keys %$pve_std_chains) {
ruleset_create_chain($ruleset, $chain);
}
}
+sub save_pvefw_status {
+ my ($status) = @_;
+
+ die "unknown status '$status' - internal error"
+ if $status !~ m/^(stopped|active)$/;
+
+ mkdir dirname($pve_fw_status_filename);
+ PVE::Tools::file_set_contents($pve_fw_status_filename, $status);
+}
+
+sub read_pvefw_status {
+
+ my $status = 'unknown';
+
+ return 'stopped' if ! -f $pve_fw_status_filename;
+
+ eval {
+ $status = PVE::Tools::file_get_contents($pve_fw_status_filename);
+ };
+ warn $@ if $@;
+
+ return $status;
+}
+
sub compile {
my $vmdata = read_local_vm_config();
my $rules = read_vm_firewall_rules($vmdata);
ruleset_create_chain($ruleset, "PVEFW-OUTPUT");
ruleset_create_chain($ruleset, "PVEFW-FORWARD");
- generate_std_chains($ruleset);
+ my $host_options = {};
+ my $host_rules;
- my $enable_hostfw = 0;
$filename = "/etc/pve/local/host.fw";
if (my $fh = IO::File->new($filename, O_RDONLY)) {
- my $host_rules = parse_host_fw_rules($filename, $fh);
+ $host_rules = parse_host_fw_rules($filename, $fh);
+ $host_options = $host_rules->{options};
+ }
- $enable_hostfw = 1;
+ generate_std_chains($ruleset, $host_options);
- enablehostfw($ruleset, $host_rules, $group_rules);
- }
+ my $hotsfw_enable = $host_rules && !(defined($host_options->{enable}) && ($host_options->{enable} == 0));
+
+ enablehostfw($ruleset, $host_rules, $group_rules) if $hotsfw_enable;
# generate firewall rules for QEMU VMs
foreach my $vmid (keys %{$vmdata->{qemu}}) {
}
}
- if ($enable_hostfw) {
+ if ($hotsfw_enable) {
# allow traffic from lo (ourself)
ruleset_addrule($ruleset, "PVEFW-INPUT", "-i lo -j ACCEPT");
}
return "-A $chain -m comment --comment \"PVESIG:$sig\"\n";
}
-sub apply_ruleset {
+sub get_rulset_cmdlist {
my ($ruleset, $verbose) = @_;
- enable_bridge_firewall();
-
my $cmdlist = "*filter\n"; # we pass this to iptables-restore;
my $statushash = get_ruleset_status($ruleset, $verbose);
$cmdlist .= "COMMIT\n";
+ return $cmdlist;
+}
+
+sub apply_ruleset {
+ my ($ruleset, $verbose) = @_;
+
+ enable_bridge_firewall();
+
+ my $cmdlist = get_rulset_cmdlist($ruleset, $verbose);
+
print $cmdlist if $verbose;
iptables_restore_cmdlist($cmdlist);
# test: re-read status and check if everything is up to date
- $statushash = get_ruleset_status($ruleset);
+ my $statushash = get_ruleset_status($ruleset);
my $errors;
foreach my $chain (sort keys %$ruleset) {
die "unable to apply firewall changes\n" if $errors;
}
+sub update {
+ my ($start, $verbose) = @_;
+
+ my $code = sub {
+ my $status = read_pvefw_status();
+
+ my $ruleset = PVE::Firewall::compile();
+
+ if ($start || $status eq 'active') {
+
+ save_pvefw_status('active') if ($status ne 'active');
+
+ PVE::Firewall::apply_ruleset($ruleset, $verbose);
+ } else {
+ print "Firewall not active (status = $status)\n" if $verbose;
+ }
+ };
+
+ run_locked($code);
+}
+
+
1;