Expand parser, cover more kinds of rules

This commit is contained in:
Joe Cooper
2026-01-31 17:00:57 -06:00
parent e92fc730ee
commit 179ddf751b
6 changed files with 1030 additions and 71 deletions

View File

@@ -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 .= "<br>".&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'}) {

44
lang/en
View File

@@ -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

View File

@@ -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)

View File

@@ -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);

8
t/rulesets/basic.nft Normal file
View File

@@ -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
}
}

131
t/run-tests.t Executable file
View File

@@ -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();