diff --git a/edit_rule.cgi b/edit_rule.cgi index 1c33919db..c4f3663df 100755 --- a/edit_rule.cgi +++ b/edit_rule.cgi @@ -12,6 +12,10 @@ my $table = $tables[$in{'table'}]; my $rule; my $chain_def; my $chain_hook; +my $action_sel; +my $proto_sel; +my $icmp_type; +my $log_enabled; if ($in{'new'}) { &ui_print_header(undef, $text{'edit_title_new'}, "", "intro", 1, 1); @@ -24,6 +28,26 @@ if ($table && $rule->{'chain'}) { $chain_def = $table->{'chains'}->{$rule->{'chain'}}; $chain_hook = $chain_def ? $chain_def->{'hook'} : undef; } +if ($rule) { + if ($rule->{'jump'}) { + $action_sel = 'jump'; + } + elsif ($rule->{'goto'}) { + $action_sel = 'goto'; + } + else { + $action_sel = $rule->{'action'}; + } + $action_sel ||= 'accept'; + $proto_sel = $rule->{'proto'} || $rule->{'l4proto'}; + if (!$proto_sel) { + $proto_sel = 'icmp' if ($rule->{'icmp_type'}); + $proto_sel = 'icmpv6' if ($rule->{'icmpv6_type'}); + } + $proto_sel ||= 'tcp' if ($in{'new'}); + $icmp_type = $rule->{'icmp_type'} || $rule->{'icmpv6_type'}; + $log_enabled = $rule->{'log'} || $rule->{'log_prefix'} || $rule->{'log_level'}; +} print &ui_form_start("save_rule.cgi"); print &ui_hidden("table", $in{'table'}); @@ -39,26 +63,23 @@ print &ui_table_row($text{'edit_comment'}, # Action print &ui_table_row($text{'edit_action'}, - &ui_select("action", $rule->{'action'}, + &ui_select("action", $action_sel, [ [ "accept", $text{'index_accept'} ], [ "drop", $text{'index_drop'} ], [ "reject", $text{'index_reject'} ], + [ "return", $text{'edit_return'} ], + [ "jump", $text{'edit_jump_action'} ], + [ "goto", $text{'edit_goto_action'} ], ])); -# Protocol -print &ui_table_row($text{'edit_proto'}, - &ui_select("proto", $rule->{'proto'}, - [ - [ "tcp", "TCP" ], - [ "udp", "UDP" ], - [ "icmp", "ICMP" ], - ])); - -# Destination port -print &ui_table_row($text{'edit_dport'}, - &ui_textbox("dport", $rule->{'dport'}, 10)); +# Jump/Goto target chain +print &ui_table_row($text{'edit_jump'}, + &ui_textbox("jump", $rule->{'jump'}, 20)); +print &ui_table_row($text{'edit_goto'}, + &ui_textbox("goto", $rule->{'goto'}, 20)); +# Interfaces if ($chain_hook && $chain_hook eq 'input') { # Incoming interface print &ui_table_row($text{'edit_iif'}, @@ -77,6 +98,64 @@ else { &interface_choice("oif", $rule->{'oif'}, $text{'edit_if_any'})); } +# Addresses +print &ui_table_row($text{'edit_saddr'}, + &ui_textbox("saddr", $rule->{'saddr'}, 30)); +print &ui_table_row($text{'edit_daddr'}, + &ui_textbox("daddr", $rule->{'daddr'}, 30)); + +# Protocol +print &ui_table_row($text{'edit_proto'}, + &ui_select("proto", $proto_sel, + [ + [ "", $text{'edit_proto_any'} ], + [ "tcp", "TCP" ], + [ "udp", "UDP" ], + [ "icmp", "ICMP" ], + [ "icmpv6", "ICMPv6" ], + ])); + +# Ports +print &ui_table_row($text{'edit_sport'}, + &ui_textbox("sport", $rule->{'sport'}, 10)); +print &ui_table_row($text{'edit_dport'}, + &ui_textbox("dport", $rule->{'dport'}, 10)); + +# ICMP type +print &ui_table_row($text{'edit_icmp_type'}, + &ui_textbox("icmp_type", $icmp_type, 20)); + +# Conntrack state +print &ui_table_row($text{'edit_ct_state'}, + &ui_textbox("ct_state", $rule->{'ct_state'}, 30)); + +# TCP flags +print &ui_table_row($text{'edit_tcp_flags'}, + &ui_textbox("tcp_flags", $rule->{'tcp_flags'}, 20)); +print &ui_table_row($text{'edit_tcp_flags_mask'}, + &ui_textbox("tcp_flags_mask", $rule->{'tcp_flags_mask'}, 20)); + +# Limit +print &ui_table_row($text{'edit_limit_rate'}, + &ui_textbox("limit_rate", $rule->{'limit_rate'}, 20)); +print &ui_table_row($text{'edit_limit_burst'}, + &ui_textbox("limit_burst", $rule->{'limit_burst'}, 10)); + +# Log +my $log_row = &ui_checkbox("log", 1, $text{'edit_log_enable'}, $log_enabled); +$log_row .= "
".&text('edit_log_prefix', &ui_textbox("log_prefix", $rule->{'log_prefix'}, 20)); +$log_row .= " ".&text('edit_log_level', &ui_textbox("log_level", $rule->{'log_level'}, 10)); +print &ui_table_row($text{'edit_log'}, $log_row); + +# Counter +print &ui_table_row($text{'edit_counter'}, + &ui_checkbox("counter", 1, $text{'edit_counter_enable'}, $rule->{'counter'})); + +# Raw rule (read-only) +print &ui_table_row($text{'edit_raw_rule'}, + &ui_textarea("raw_rule", $rule->{'text'}, 4, 60, undef, undef, + "readonly='true'")); + print &ui_table_end(); my @buttons; if ($in{'new'}) { diff --git a/lang/en b/lang/en index 3c95a1c00..36c40722f 100644 --- a/lang/en +++ b/lang/en @@ -7,6 +7,7 @@ index_change=Show table: index_table_filter=Packet filtering index_table_nat=Network address translation index_table_mangle=Packet alteration +index_table_ok=Display table index_chain=Chain $1 index_action=Action index_desc=Condition @@ -67,15 +68,58 @@ index_rule_desc=Action $1 for protocol $2 and destination port $3 index_rule_desc2=Action $1 for outgoing interface $2 index_rule_desc3=Action $1 for incoming interface $2 index_rule_desc4=Action $1 for incoming interface $2 and outgoing interface $3 +index_rule_desc_generic=Action $1 for $2 +index_rule_desc_action=Action $1 +index_rule_iif=Incoming interface $1 +index_rule_oif=Outgoing interface $1 +index_rule_saddr=Source $1 +index_rule_daddr=Destination $1 +index_rule_proto=Protocol $1 +index_rule_sport=Source port $1 +index_rule_dport=Destination port $1 +index_rule_icmp=ICMP type $1 +index_rule_icmpv6=ICMPv6 type $1 +index_rule_ct=Conntrack state $1 +index_rule_tcpflags=TCP flags $1 +index_rule_limit=Limit $1 +index_rule_log=Log +index_rule_log_prefix=Log prefix $1 +index_rule_log_level=Log level $1 +index_rule_counter=Counter +index_rule_jump=Jump $1 +index_rule_goto=Goto $1 index_accept=Accept index_drop=Drop index_reject=Reject +index_return_action=Return edit_title_new=Create Rule edit_title_edit=Edit Rule edit_comment=Comment edit_action=Action +edit_return=Return +edit_jump_action=Jump +edit_goto_action=Goto +edit_jump=Jump target chain +edit_goto=Goto target chain edit_proto=Protocol +edit_proto_any=Any +edit_saddr=Source address +edit_daddr=Destination address +edit_sport=Source Port edit_dport=Destination Port +edit_icmp_type=ICMP/ICMPv6 type +edit_ct_state=Conntrack state +edit_tcp_flags=TCP flags +edit_tcp_flags_mask=TCP flags mask +edit_limit_rate=Limit rate +edit_limit_burst=Limit burst +edit_log=Logging +edit_log_enable=Enable logging +edit_log_prefix=Prefix $1 +edit_log_level=Level $1 +edit_counter=Counter +edit_counter_enable=Enable counter +edit_raw_rule=Raw rule edit_oif=Outgoing Interface edit_iif=Incoming Interface edit_if_any=Any diff --git a/nftables-lib.pl b/nftables-lib.pl index e86dbd918..8af072a05 100644 --- a/nftables-lib.pl +++ b/nftables-lib.pl @@ -74,31 +74,11 @@ for(my $i=0; $i<@lines; $i++) { 'index' => scalar(@{$table->{'rules'}}), 'line' => $lnum }; - if ($rule_str =~ /\bcomment\s+"((?:\\.|[^"\\])*)"/) { - my $c = $1; - $c =~ s/\\"/"/g; - $c =~ s/\\\\/\\/g; - $rule->{'comment'} = $c; - } - if ($rule_str =~ /(\S+)\s+dport\s+(\d+)/) { - $rule->{'proto'} = $1; - $rule->{'dport'} = $2; - } - if ($rule_str =~ /\biif\s+"([^"]+)"/) { - $rule->{'iif'} = $1; - } - elsif ($rule_str =~ /\biif\s+(\S+)/) { - $rule->{'iif'} = $1; - } - if ($rule_str =~ /\boif\s+"([^"]+)"/) { - $rule->{'oif'} = $1; - } - elsif ($rule_str =~ /\boif\s+(\S+)/) { - $rule->{'oif'} = $1; - } - my @actions = ($rule_str =~ /\b(accept|drop|reject)\b/g); - if (@actions) { - $rule->{'action'} = $actions[-1]; + my $parsed = &parse_rule_text($rule_str); + if ($parsed) { + foreach my $k (keys %$parsed) { + $rule->{$k} = $parsed->{$k}; + } } push(@{$table->{'rules'}}, $rule); } @@ -108,6 +88,604 @@ for(my $i=0; $i<@lines; $i++) { return @rv; } +sub tokenize_nft_rule +{ +my ($line) = @_; +my @tokens; +my $i = 0; +my $len = length($line); +while ($i < $len) { + my $ch = substr($line, $i, 1); + if ($ch =~ /\s/) { + $i++; + next; + } + if ($ch eq '"' || $ch eq "'") { + my $q = $ch; + my $j = $i + 1; + my $esc = 0; + while ($j < $len) { + my $c = substr($line, $j, 1); + if ($esc) { + $esc = 0; + } + elsif ($c eq "\\") { + $esc = 1; + } + elsif ($c eq $q) { + $j++; + last; + } + $j++; + } + push(@tokens, substr($line, $i, $j-$i)); + $i = $j; + next; + } + if ($ch eq '{') { + my $j = $i + 1; + my $depth = 1; + while ($j < $len && $depth > 0) { + my $c = substr($line, $j, 1); + if ($c eq '{') { + $depth++; + } + elsif ($c eq '}') { + $depth--; + } + $j++; + } + push(@tokens, substr($line, $i, $j-$i)); + $i = $j; + next; + } + my $j = $i; + while ($j < $len && substr($line, $j, 1) !~ /\s/) { + $j++; + } + push(@tokens, substr($line, $i, $j-$i)); + $i = $j; +} +return @tokens; +} + +sub unquote_nft_string +{ +my ($s) = @_; +return $s if (!defined($s)); +if ($s =~ /^"(.*)"$/s) { + $s = $1; + $s =~ s/\\(["\\])/$1/g; +} +elsif ($s =~ /^'(.*)'$/s) { + $s = $1; + $s =~ s/\\(['\\])/$1/g; +} +return $s; +} + +sub escape_nft_string +{ +my ($s) = @_; +return "" if (!defined($s)); +$s =~ s/\\/\\\\/g; +$s =~ s/"/\\"/g; +return $s; +} + +sub guess_addr_family +{ +my ($addr, $fallback) = @_; +return $fallback if ($fallback); +return "ip6" if (defined($addr) && $addr =~ /:/); +return "ip"; +} + +sub format_addr_expr +{ +my ($dir, $rule) = @_; +my $val = $rule->{$dir}; +return undef if (!defined($val) || $val eq ''); +my $fam = &guess_addr_family($val, $rule->{$dir."_family"}); +return $fam." ".$dir." ".$val; +} + +sub format_l4proto_expr +{ +my ($rule) = @_; +my $proto = $rule->{'l4proto'}; +return undef if (!defined($proto) || $proto eq ''); +my $fam = $rule->{'l4proto_family'} || 'meta'; +if ($fam eq 'ip' || $fam eq 'ip6') { + return $fam." protocol ".$proto; +} +return "meta l4proto ".$proto; +} + +sub format_port_expr +{ +my ($dir, $rule) = @_; +my $val = $rule->{$dir}; +return undef if (!defined($val) || $val eq ''); +my $proto; +if ($dir eq 'sport') { + $proto = $rule->{'sport_proto'} || $rule->{'proto'} || $rule->{'l4proto'}; +} +else { + $proto = $rule->{'proto'} || $rule->{'l4proto'}; +} +return undef if (!defined($proto) || $proto eq ''); +return $proto." ".$dir." ".$val; +} + +sub format_tcp_flags_expr +{ +my ($rule) = @_; +return undef if (!defined($rule->{'tcp_flags'}) || $rule->{'tcp_flags'} eq ''); +my $val = $rule->{'tcp_flags'}; +if (defined($rule->{'tcp_flags_mask'}) && $rule->{'tcp_flags_mask'} ne '') { + return "tcp flags & ".$rule->{'tcp_flags_mask'}." == ".$val; +} +return "tcp flags ".$val; +} + +sub format_limit_expr +{ +my ($rule) = @_; +return undef if (!defined($rule->{'limit_rate'}) || $rule->{'limit_rate'} eq ''); +my $out = "limit rate ".$rule->{'limit_rate'}; +if (defined($rule->{'limit_burst'}) && $rule->{'limit_burst'} ne '') { + my $burst = $rule->{'limit_burst'}; + $out .= " burst ".$burst; + $out .= " packets" if ($burst =~ /^\d+$/); +} +return $out; +} + +sub format_log_expr +{ +my ($rule) = @_; +return undef if (!$rule->{'log'} && !$rule->{'log_prefix'} && !$rule->{'log_level'}); +my @p = ("log"); +if (defined($rule->{'log_prefix'}) && $rule->{'log_prefix'} ne '') { + my $pfx = &escape_nft_string($rule->{'log_prefix'}); + push(@p, "prefix", "\"".$pfx."\""); +} +if (defined($rule->{'log_level'}) && $rule->{'log_level'} ne '') { + push(@p, "level", $rule->{'log_level'}); +} +return join(" ", @p); +} + +sub parse_rule_text +{ +my ($line) = @_; +return { } if (!defined($line)); +my %rule; +my @tokens = &tokenize_nft_rule($line); +my @exprs; +my $i = 0; +while ($i < @tokens) { + my $tok = $tokens[$i]; + + if ($tok eq 'comment' && $i+1 < @tokens) { + my $raw = $tokens[$i]." ".$tokens[$i+1]; + $rule{'comment'} = &unquote_nft_string($tokens[$i+1]); + push(@exprs, { 'type' => 'comment', 'text' => $raw }); + $i += 2; + next; + } + if (($tok eq 'iif' || $tok eq 'iifname') && $i+1 < @tokens) { + my $raw = $tok." ".$tokens[$i+1]; + $rule{'iif'} = &unquote_nft_string($tokens[$i+1]); + $rule{'iif_type'} = $tok; + push(@exprs, { 'type' => 'iif', 'text' => $raw }); + $i += 2; + next; + } + if (($tok eq 'oif' || $tok eq 'oifname') && $i+1 < @tokens) { + my $raw = $tok." ".$tokens[$i+1]; + $rule{'oif'} = &unquote_nft_string($tokens[$i+1]); + $rule{'oif_type'} = $tok; + push(@exprs, { 'type' => 'oif', 'text' => $raw }); + $i += 2; + next; + } + if (($tok eq 'ip' || $tok eq 'ip6') && $i+2 < @tokens && + ($tokens[$i+1] eq 'saddr' || $tokens[$i+1] eq 'daddr')) { + my $which = $tokens[$i+1]; + my $val = $tokens[$i+2]; + my $raw = $tok." ".$which." ".$val; + $rule{$which} = $val; + $rule{$which."_family"} = $tok; + push(@exprs, { 'type' => $which, 'text' => $raw }); + $i += 3; + next; + } + if (($tok eq 'ip' || $tok eq 'ip6') && $i+2 < @tokens && + $tokens[$i+1] eq 'protocol') { + my $val = $tokens[$i+2]; + my $raw = $tok." protocol ".$val; + $rule{'l4proto'} = $val; + $rule{'l4proto_family'} = $tok; + push(@exprs, { 'type' => 'l4proto', 'text' => $raw }); + $i += 3; + next; + } + if ($tok eq 'meta' && $i+2 < @tokens && + $tokens[$i+1] eq 'l4proto') { + my $val = $tokens[$i+2]; + my $raw = "meta l4proto ".$val; + $rule{'l4proto'} = $val; + $rule{'l4proto_family'} = 'meta'; + push(@exprs, { 'type' => 'l4proto', 'text' => $raw }); + $i += 3; + next; + } + if ($tok eq 'tcp' && $i+1 < @tokens && $tokens[$i+1] eq 'flags') { + my $j = $i + 2; + my $mask; + my $val; + if ($j < @tokens && $tokens[$j] eq '&' && $j+1 < @tokens) { + $mask = $tokens[$j+1]; + $j += 2; + } + if ($j < @tokens && $tokens[$j] eq '==' && $j+1 < @tokens) { + $val = $tokens[$j+1]; + $j += 2; + } + elsif ($j < @tokens) { + $val = $tokens[$j]; + $j++; + } + my $raw = join(" ", @tokens[$i..($j-1)]); + $rule{'tcp_flags'} = $val if (defined($val)); + $rule{'tcp_flags_mask'} = $mask if (defined($mask)); + push(@exprs, { 'type' => 'tcp_flags', 'text' => $raw }); + $i = $j; + next; + } + if (($tok eq 'tcp' || $tok eq 'udp') && $i+2 < @tokens && + ($tokens[$i+1] eq 'dport' || $tokens[$i+1] eq 'sport')) { + my $dir = $tokens[$i+1]; + my $val = $tokens[$i+2]; + my $raw = $tok." ".$dir." ".$val; + if ($dir eq 'dport') { + $rule{'proto'} = $tok; + $rule{'dport'} = $val; + } + else { + $rule{'sport'} = $val; + $rule{'sport_proto'} = $tok; + } + push(@exprs, { 'type' => $dir, 'text' => $raw, 'proto' => $tok }); + $i += 3; + next; + } + if (($tok eq 'icmp' || $tok eq 'icmpv6') && $i+2 < @tokens && + $tokens[$i+1] eq 'type') { + my $val = $tokens[$i+2]; + my $raw = $tok." type ".$val; + if ($tok eq 'icmp') { + $rule{'icmp_type'} = $val; + } + else { + $rule{'icmpv6_type'} = $val; + } + push(@exprs, { 'type' => $tok, 'text' => $raw }); + $i += 3; + next; + } + if ($tok eq 'ct' && $i+2 < @tokens && $tokens[$i+1] eq 'state') { + my $val = $tokens[$i+2]; + my $raw = "ct state ".$val; + $rule{'ct_state'} = $val; + push(@exprs, { 'type' => 'ct_state', 'text' => $raw }); + $i += 3; + next; + } + if ($tok eq 'limit') { + my $j = $i + 1; + my @lt = ($tok); + if ($j < @tokens && $tokens[$j] eq 'rate' && $j+1 < @tokens) { + push(@lt, $tokens[$j], $tokens[$j+1]); + $rule{'limit_rate'} = $tokens[$j+1]; + $j += 2; + if ($j < @tokens && $tokens[$j] eq 'burst' && $j+1 < @tokens) { + push(@lt, $tokens[$j], $tokens[$j+1]); + $rule{'limit_burst'} = $tokens[$j+1]; + $j += 2; + if ($j < @tokens && $tokens[$j] eq 'packets') { + push(@lt, $tokens[$j]); + $j++; + } + } + } + my $raw = join(" ", @lt); + push(@exprs, { 'type' => 'limit', 'text' => $raw }); + $i = $j; + next; + } + if ($tok eq 'log') { + my $j = $i + 1; + my @lt = ($tok); + while ($j < @tokens) { + if ($tokens[$j] eq 'prefix' && $j+1 < @tokens) { + $rule{'log_prefix'} = &unquote_nft_string($tokens[$j+1]); + push(@lt, $tokens[$j], $tokens[$j+1]); + $j += 2; + next; + } + if ($tokens[$j] eq 'level' && $j+1 < @tokens) { + $rule{'log_level'} = $tokens[$j+1]; + push(@lt, $tokens[$j], $tokens[$j+1]); + $j += 2; + next; + } + last; + } + $rule{'log'} = 1; + my $raw = join(" ", @lt); + push(@exprs, { 'type' => 'log', 'text' => $raw }); + $i = $j; + next; + } + if ($tok eq 'counter') { + $rule{'counter'} = 1; + push(@exprs, { 'type' => 'counter', 'text' => $tok }); + $i++; + next; + } + if ($tok =~ /^(accept|drop|reject|return)$/) { + $rule{'action'} = $tok; + push(@exprs, { 'type' => 'action', 'text' => $tok }); + $i++; + next; + } + if (($tok eq 'jump' || $tok eq 'goto') && $i+1 < @tokens) { + my $raw = $tok." ".$tokens[$i+1]; + $rule{$tok} = $tokens[$i+1]; + push(@exprs, { 'type' => $tok, 'text' => $raw }); + $i += 2; + next; + } + + push(@exprs, { 'type' => 'raw', 'text' => $tok }); + $i++; +} +$rule{'exprs'} = \@exprs; +return \%rule; +} + +sub format_rule_text +{ +my ($rule) = @_; +return "" if (!$rule || ref($rule) ne 'HASH'); +my @parts; +my %used; +my $exprs = $rule->{'exprs'}; +if ($exprs && ref($exprs) eq 'ARRAY' && @$exprs) { + foreach my $e (@$exprs) { + my $type = $e->{'type'} || 'raw'; + if ($type eq 'action' || $type eq 'comment') { + next; + } + if ($type eq 'iif') { + if (!$used{'iif'} && defined($rule->{'iif'}) && $rule->{'iif'} ne '') { + my $iftype = $rule->{'iif_type'} || 'iif'; + my $ival = &escape_nft_string($rule->{'iif'}); + push(@parts, $iftype." \"".$ival."\""); + $used{'iif'} = 1; + } + next; + } + if ($type eq 'oif') { + if (!$used{'oif'} && defined($rule->{'oif'}) && $rule->{'oif'} ne '') { + my $oftype = $rule->{'oif_type'} || 'oif'; + my $oval = &escape_nft_string($rule->{'oif'}); + push(@parts, $oftype." \"".$oval."\""); + $used{'oif'} = 1; + } + next; + } + if ($type eq 'saddr') { + if (!$used{'saddr'}) { + my $addr = &format_addr_expr('saddr', $rule); + if ($addr) { + push(@parts, $addr); + $used{'saddr'} = 1; + } + } + next; + } + if ($type eq 'daddr') { + if (!$used{'daddr'}) { + my $addr = &format_addr_expr('daddr', $rule); + if ($addr) { + push(@parts, $addr); + $used{'daddr'} = 1; + } + } + next; + } + if ($type eq 'l4proto') { + if (!$used{'l4proto'}) { + my $lp = &format_l4proto_expr($rule); + if ($lp) { + push(@parts, $lp); + $used{'l4proto'} = 1; + } + } + next; + } + if ($type eq 'sport') { + if (!$used{'sport'}) { + my $sp = &format_port_expr('sport', $rule); + if ($sp) { + push(@parts, $sp); + $used{'sport'} = 1; + } + } + next; + } + if ($type eq 'dport') { + if (!$used{'dport'} && $rule->{'proto'} && $rule->{'dport'}) { + my $dp = &format_port_expr('dport', $rule); + if ($dp) { + push(@parts, $dp); + $used{'dport'} = 1; + } + } + next; + } + if ($type eq 'icmp') { + if (!$used{'icmp'} && $rule->{'icmp_type'}) { + push(@parts, "icmp type ".$rule->{'icmp_type'}); + $used{'icmp'} = 1; + } + next; + } + if ($type eq 'icmpv6') { + if (!$used{'icmpv6'} && $rule->{'icmpv6_type'}) { + push(@parts, "icmpv6 type ".$rule->{'icmpv6_type'}); + $used{'icmpv6'} = 1; + } + next; + } + if ($type eq 'ct_state') { + if (!$used{'ct_state'} && $rule->{'ct_state'}) { + push(@parts, "ct state ".$rule->{'ct_state'}); + $used{'ct_state'} = 1; + } + next; + } + if ($type eq 'tcp_flags') { + if (!$used{'tcp_flags'}) { + my $tf = &format_tcp_flags_expr($rule); + if ($tf) { + push(@parts, $tf); + $used{'tcp_flags'} = 1; + } + } + next; + } + if ($type eq 'limit') { + if (!$used{'limit'}) { + my $lim = &format_limit_expr($rule); + if ($lim) { + push(@parts, $lim); + $used{'limit'} = 1; + } + } + next; + } + if ($type eq 'log') { + if (!$used{'log'}) { + my $lg = &format_log_expr($rule); + if ($lg) { + push(@parts, $lg); + $used{'log'} = 1; + } + } + next; + } + if ($type eq 'counter') { + if (!$used{'counter'} && $rule->{'counter'}) { + push(@parts, "counter"); + $used{'counter'} = 1; + } + next; + } + if ($type eq 'jump') { + if (!$used{'jump'} && $rule->{'jump'}) { + push(@parts, "jump ".$rule->{'jump'}); + $used{'jump'} = 1; + } + next; + } + if ($type eq 'goto') { + if (!$used{'goto'} && $rule->{'goto'}) { + push(@parts, "goto ".$rule->{'goto'}); + $used{'goto'} = 1; + } + next; + } + push(@parts, $e->{'text'}) if ($e->{'text'}); + } +} +if (!$used{'iif'} && defined($rule->{'iif'}) && $rule->{'iif'} ne '') { + my $iftype = $rule->{'iif_type'} || 'iif'; + my $ival = &escape_nft_string($rule->{'iif'}); + push(@parts, $iftype." \"".$ival."\""); +} +if (!$used{'oif'} && defined($rule->{'oif'}) && $rule->{'oif'} ne '') { + my $oftype = $rule->{'oif_type'} || 'oif'; + my $oval = &escape_nft_string($rule->{'oif'}); + push(@parts, $oftype." \"".$oval."\""); +} +if (!$used{'saddr'}) { + my $addr = &format_addr_expr('saddr', $rule); + push(@parts, $addr) if ($addr); +} +if (!$used{'daddr'}) { + my $addr = &format_addr_expr('daddr', $rule); + push(@parts, $addr) if ($addr); +} +if (!$used{'l4proto'}) { + my $lp = &format_l4proto_expr($rule); + push(@parts, $lp) if ($lp); +} +if (!$used{'sport'}) { + my $sp = &format_port_expr('sport', $rule); + push(@parts, $sp) if ($sp); +} +if (!$used{'dport'}) { + my $dp = &format_port_expr('dport', $rule); + push(@parts, $dp) if ($dp); +} +if (!$used{'icmp'} && $rule->{'icmp_type'}) { + push(@parts, "icmp type ".$rule->{'icmp_type'}); +} +if (!$used{'icmpv6'} && $rule->{'icmpv6_type'}) { + push(@parts, "icmpv6 type ".$rule->{'icmpv6_type'}); +} +if (!$used{'tcp_flags'}) { + my $tf = &format_tcp_flags_expr($rule); + push(@parts, $tf) if ($tf); +} +if (!$used{'ct_state'} && $rule->{'ct_state'}) { + push(@parts, "ct state ".$rule->{'ct_state'}); +} +if (!$used{'limit'}) { + my $lim = &format_limit_expr($rule); + push(@parts, $lim) if ($lim); +} +if (!$used{'log'}) { + my $lg = &format_log_expr($rule); + push(@parts, $lg) if ($lg); +} +if (!$used{'counter'} && $rule->{'counter'}) { + push(@parts, "counter"); +} +if (!$used{'jump'} && $rule->{'jump'}) { + push(@parts, "jump ".$rule->{'jump'}); +} +if (!$used{'goto'} && $rule->{'goto'}) { + push(@parts, "goto ".$rule->{'goto'}); +} +if ($rule->{'action'} && !$rule->{'jump'} && !$rule->{'goto'}) { + push(@parts, $rule->{'action'}); +} +if (defined($rule->{'comment'}) && $rule->{'comment'} ne '') { + my $c = &escape_nft_string($rule->{'comment'}); + push(@parts, "comment \"".$c."\""); +} +my $text = join(" ", grep { defined($_) && $_ ne '' } @parts); +$text =~ s/^\s+//; +$text =~ s/\s+$//; +return $text; +} + # dump_nftables_save(@tables) # Returns a string representation of the firewall rules @@ -191,23 +769,87 @@ return undef; sub describe_rule { my ($r) = @_; -my $desc; -if ($r->{'proto'} && $r->{'dport'} && $r->{'action'}) { - $desc = &text('index_rule_desc', $r->{'action'}, $r->{'proto'}, $r->{'dport'}); +my @conds; +if ($r->{'iif'}) { + push(@conds, &text('index_rule_iif', &html_escape($r->{'iif'}))); } -elsif ($r->{'iif'} && $r->{'oif'} && $r->{'action'}) { - $desc = &text('index_rule_desc4', $r->{'action'}, $r->{'iif'}, $r->{'oif'}); +if ($r->{'oif'}) { + push(@conds, &text('index_rule_oif', &html_escape($r->{'oif'}))); } -elsif ($r->{'iif'} && $r->{'action'}) { - $desc = &text('index_rule_desc3', $r->{'action'}, $r->{'iif'}); +if ($r->{'saddr'}) { + push(@conds, &text('index_rule_saddr', &html_escape($r->{'saddr'}))); } -elsif ($r->{'oif'} && $r->{'action'}) { - $desc = &text('index_rule_desc2', $r->{'action'}, $r->{'oif'}); +if ($r->{'daddr'}) { + push(@conds, &text('index_rule_daddr', &html_escape($r->{'daddr'}))); } -else { - $desc = &html_escape($r->{'text'}); +if ($r->{'l4proto'} || ($r->{'proto'} && !$r->{'dport'} && !$r->{'sport'})) { + my $p = $r->{'l4proto'} || $r->{'proto'}; + push(@conds, &text('index_rule_proto', &html_escape($p))); } -return $desc; +if ($r->{'sport'}) { + push(@conds, &text('index_rule_sport', &html_escape($r->{'sport'}))); +} +if ($r->{'dport'}) { + push(@conds, &text('index_rule_dport', &html_escape($r->{'dport'}))); +} +if ($r->{'icmp_type'}) { + push(@conds, &text('index_rule_icmp', &html_escape($r->{'icmp_type'}))); +} +if ($r->{'icmpv6_type'}) { + push(@conds, &text('index_rule_icmpv6', &html_escape($r->{'icmpv6_type'}))); +} +if ($r->{'ct_state'}) { + push(@conds, &text('index_rule_ct', &html_escape($r->{'ct_state'}))); +} +if ($r->{'tcp_flags'}) { + my $tf = $r->{'tcp_flags'}; + if ($r->{'tcp_flags_mask'}) { + $tf = $r->{'tcp_flags_mask'}."==".$r->{'tcp_flags'}; + } + push(@conds, &text('index_rule_tcpflags', &html_escape($tf))); +} +if ($r->{'limit_rate'}) { + my $lim = $r->{'limit_rate'}; + if ($r->{'limit_burst'}) { + $lim .= " burst ".$r->{'limit_burst'}; + } + push(@conds, &text('index_rule_limit', &html_escape($lim))); +} +if ($r->{'log_prefix'}) { + push(@conds, &text('index_rule_log_prefix', &html_escape($r->{'log_prefix'}))); +} +if ($r->{'log_level'}) { + push(@conds, &text('index_rule_log_level', &html_escape($r->{'log_level'}))); +} +if ($r->{'log'} && !$r->{'log_prefix'} && !$r->{'log_level'}) { + push(@conds, &text('index_rule_log')); +} +if ($r->{'counter'}) { + push(@conds, &text('index_rule_counter')); +} + +my $action_label; +if ($r->{'jump'}) { + $action_label = &text('index_rule_jump', &html_escape($r->{'jump'})); +} +elsif ($r->{'goto'}) { + $action_label = &text('index_rule_goto', &html_escape($r->{'goto'})); +} +elsif ($r->{'action'}) { + if ($r->{'action'} eq 'return') { + $action_label = &text('index_return_action'); + } + else { + $action_label = &text('index_'.lc($r->{'action'})); + } +} +if ($action_label) { + if (@conds) { + return &text('index_rule_desc_generic', $action_label, join(", ", @conds)); + } + return &text('index_rule_desc_action', $action_label); +} +return &html_escape($r->{'text'}); } # interface_choice(name, value, blanktext) diff --git a/save_rule.cgi b/save_rule.cgi index 812ae89c0..49f8be47a 100755 --- a/save_rule.cgi +++ b/save_rule.cgi @@ -26,9 +26,81 @@ if ($in{'delete'}) { } $rule->{'comment'} = $in{'comment'}; - $rule->{'action'} = $in{'action'}; - $rule->{'proto'} = $in{'proto'}; - $rule->{'dport'} = $in{'dport'}; + my $action = $in{'action'} || 'accept'; + $rule->{'action'} = undef; + $rule->{'jump'} = undef; + $rule->{'goto'} = undef; + if ($action eq 'jump') { + $rule->{'jump'} = $in{'jump'}; + } + elsif ($action eq 'goto') { + $rule->{'goto'} = $in{'goto'}; + } + else { + $rule->{'action'} = $action; + } + + $rule->{'saddr'} = (defined($in{'saddr'}) && $in{'saddr'} ne '') ? $in{'saddr'} : undef; + $rule->{'daddr'} = (defined($in{'daddr'}) && $in{'daddr'} ne '') ? $in{'daddr'} : undef; + $rule->{'saddr_family'} = $rule->{'saddr'} ? &guess_addr_family($rule->{'saddr'}) : undef; + $rule->{'daddr_family'} = $rule->{'daddr'} ? &guess_addr_family($rule->{'daddr'}) : undef; + + my $proto = $in{'proto'}; + $proto = undef if (defined($proto) && $proto eq ''); + $rule->{'sport'} = (defined($in{'sport'}) && $in{'sport'} ne '') ? $in{'sport'} : undef; + $rule->{'dport'} = (defined($in{'dport'}) && $in{'dport'} ne '') ? $in{'dport'} : undef; + if (!$proto && ($rule->{'sport'} || $rule->{'dport'})) { + $proto = 'tcp'; + } + $rule->{'l4proto'} = undef; + $rule->{'l4proto_family'} = undef; + $rule->{'proto'} = undef; + $rule->{'sport_proto'} = undef; + if ($proto && ($proto eq 'tcp' || $proto eq 'udp')) { + $rule->{'proto'} = $proto if ($rule->{'sport'} || $rule->{'dport'}); + $rule->{'sport_proto'} = $proto if ($rule->{'sport'}); + } + elsif ($proto && $proto !~ /^(tcp|udp)$/) { + $rule->{'sport'} = undef; + $rule->{'dport'} = undef; + } + if ($proto) { + if (($proto eq 'tcp' || $proto eq 'udp') && ($rule->{'sport'} || $rule->{'dport'})) { + # L4 proto implied by port match + } + else { + $rule->{'l4proto'} = $proto; + $rule->{'l4proto_family'} = 'meta'; + } + } + + my $icmp_type = $in{'icmp_type'}; + $rule->{'icmp_type'} = undef; + $rule->{'icmpv6_type'} = undef; + if ($proto && $proto eq 'icmp') { + $rule->{'icmp_type'} = $icmp_type if (defined($icmp_type) && $icmp_type ne ''); + } + elsif ($proto && $proto eq 'icmpv6') { + $rule->{'icmpv6_type'} = $icmp_type if (defined($icmp_type) && $icmp_type ne ''); + } + elsif (!$proto && defined($icmp_type) && $icmp_type ne '') { + $rule->{'icmp_type'} = $icmp_type; + $rule->{'l4proto'} = 'icmp'; + $rule->{'l4proto_family'} = 'meta'; + } + + $rule->{'ct_state'} = (defined($in{'ct_state'}) && $in{'ct_state'} ne '') ? $in{'ct_state'} : undef; + $rule->{'tcp_flags'} = (defined($in{'tcp_flags'}) && $in{'tcp_flags'} ne '') ? $in{'tcp_flags'} : undef; + $rule->{'tcp_flags_mask'} = (defined($in{'tcp_flags_mask'}) && $in{'tcp_flags_mask'} ne '') ? $in{'tcp_flags_mask'} : undef; + $rule->{'limit_rate'} = (defined($in{'limit_rate'}) && $in{'limit_rate'} ne '') ? $in{'limit_rate'} : undef; + $rule->{'limit_burst'} = (defined($in{'limit_burst'}) && $in{'limit_burst'} ne '') ? $in{'limit_burst'} : undef; + + my $log_enabled = $in{'log'} || $in{'log_prefix'} || $in{'log_level'}; + $rule->{'log'} = $log_enabled ? 1 : undef; + $rule->{'log_prefix'} = $log_enabled && defined($in{'log_prefix'}) && $in{'log_prefix'} ne '' ? $in{'log_prefix'} : undef; + $rule->{'log_level'} = $log_enabled && defined($in{'log_level'}) && $in{'log_level'} ne '' ? $in{'log_level'} : undef; + $rule->{'counter'} = $in{'counter'} ? 1 : undef; + my $iif = $in{'iif'}; my $oif = $in{'oif'}; $iif = $in{'iif_other'} if (defined($iif) && $iif eq 'other'); @@ -36,24 +108,7 @@ if ($in{'delete'}) { $rule->{'iif'} = (defined($iif) && $iif ne '') ? $iif : undef; $rule->{'oif'} = (defined($oif) && $oif ne '') ? $oif : undef; - my $rule_text = ""; - if ($rule->{'proto'} && $rule->{'dport'}) { - $rule_text .= "$rule->{'proto'} dport $rule->{'dport'} "; - } - if ($rule->{'iif'}) { - $rule_text .= "iif \"$rule->{'iif'}\" "; - } - if ($rule->{'oif'}) { - $rule_text .= "oif \"$rule->{'oif'}\" "; - } - $rule_text .= $rule->{'action'}; - if ($rule->{'comment'}) { - my $comment = $rule->{'comment'}; - $comment =~ s/\\/\\\\/g; - $comment =~ s/"/\\"/g; - $rule_text .= " comment \"$comment\""; - } - $rule->{'text'} = $rule_text; + $rule->{'text'} = &format_rule_text($rule); if ($in{'new'}) { push(@{$table->{'rules'}}, $rule); diff --git a/t/rulesets/basic.nft b/t/rulesets/basic.nft new file mode 100644 index 000000000..2d43483b6 --- /dev/null +++ b/t/rulesets/basic.nft @@ -0,0 +1,8 @@ +table inet filter { + chain input { + type filter hook input priority 0; policy drop; + iif "lo" accept + ip saddr 192.168.1.0/24 tcp dport 22 accept comment "ssh" + ct state established,related accept + } +} diff --git a/t/run-tests.t b/t/run-tests.t new file mode 100755 index 000000000..a7c4634f4 --- /dev/null +++ b/t/run-tests.t @@ -0,0 +1,131 @@ +#!/usr/bin/perl +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempdir); + +sub script_dir +{ + my $path = $0; + if ($path =~ m{^/}) { + $path =~ s{/[^/]+$}{}; + return $path; + } + my $cwd = `pwd`; + chomp($cwd); + if ($path =~ m{/}) { + $path =~ s{/[^/]+$}{}; + return $cwd.'/'.$path; + } + return $cwd; +} + +my $bindir = &script_dir(); + +my $confdir = tempdir(CLEANUP => 1); +my $vardir = tempdir(CLEANUP => 1); +open(my $cfh, ">", "$confdir/config") or die "config: $!"; +print $cfh "os_type=linux\nos_version=0\n"; +close($cfh); +open(my $vfh, ">", "$confdir/var-path") or die "var-path: $!"; +print $vfh "$vardir\n"; +close($vfh); +$ENV{'WEBMIN_CONFIG'} = $confdir; +$ENV{'WEBMIN_VAR'} = $vardir; +$ENV{'FOREIGN_MODULE_NAME'} = 'nftables'; +$ENV{'FOREIGN_ROOT_DIRECTORY'} = '/usr/libexec/webmin'; + +chdir("$bindir/..") or die "chdir: $!"; + +require "$bindir/../nftables-lib.pl"; + +sub check_fields +{ + my ($name, $got, $expect) = @_; + foreach my $k (sort keys %$expect) { + is($got->{$k}, $expect->{$k}, "$name $k"); + } +} + +my @cases = ( + { + name => 'tcp dport accept', + line => 'tcp dport 22 accept', + expect => { proto => 'tcp', dport => '22', action => 'accept' }, + }, + { + name => 'iif oif drop', + line => 'iif "eth0" oif "eth1" drop', + expect => { iif => 'eth0', oif => 'eth1', action => 'drop' }, + }, + { + name => 'comment with quotes', + line => 'tcp dport 80 accept comment "a \\"quote\\""', + expect => { proto => 'tcp', dport => '80', action => 'accept', comment => 'a "quote"' }, + }, + { + name => 'ct state', + line => 'ct state established,related accept', + expect => { ct_state => 'established,related', action => 'accept' }, + }, + { + name => 'icmp type', + line => 'icmp type echo-request accept', + expect => { icmp_type => 'echo-request', action => 'accept' }, + }, + { + name => 'limit log counter', + line => 'tcp dport 22 limit rate 10/second burst 20 packets log prefix "ssh" level info counter accept', + expect => { + proto => 'tcp', + dport => '22', + limit_rate => '10/second', + limit_burst => '20', + log_prefix => 'ssh', + log_level => 'info', + counter => 1, + action => 'accept', + }, + }, + { + name => 'unknown tokens preserved', + line => 'tcp dport 22 meta skgid 1000 accept', + expect => { proto => 'tcp', dport => '22', action => 'accept' }, + preserve => 'meta skgid 1000', + }, +); + +foreach my $c (@cases) { + my $r = &parse_rule_text($c->{line}); + ok($r && ref($r) eq 'HASH', "$c->{name} parse hash"); + check_fields($c->{name}, $r, $c->{expect}); + + my $out = &format_rule_text($r); + ok($out =~ /\S/, "$c->{name} formatted non-empty"); + if ($c->{preserve}) { + like($out, qr/\Q$c->{preserve}\E/, "$c->{name} preserves unknowns"); + } + + my $r2 = &parse_rule_text($out); + check_fields($c->{name}.' roundtrip', $r2, $c->{expect}); +} + +my $ruleset = "$bindir/rulesets/basic.nft"; +my @tables = &get_nftables_save($ruleset); +ok(@tables == 1, 'ruleset table count'); +my $t = $tables[0]; +is($t->{family}, 'inet', 'ruleset family'); +is($t->{name}, 'filter', 'ruleset name'); +my $chain = $t->{chains}->{input}; +ok($chain, 'input chain present'); +is($chain->{type}, 'filter', 'chain type'); +is($chain->{hook}, 'input', 'chain hook'); +is($chain->{priority}, '0', 'chain priority'); +is($chain->{policy}, 'drop', 'chain policy'); + +my @rules = @{$t->{rules}}; +check_fields('ruleset r1', $rules[0], { iif => 'lo', action => 'accept' }); +check_fields('ruleset r2', $rules[1], { saddr => '192.168.1.0/24', proto => 'tcp', dport => '22', action => 'accept', comment => 'ssh' }); +check_fields('ruleset r3', $rules[2], { ct_state => 'established,related', action => 'accept' }); + +done_testing();