mirror of
https://github.com/webmin/webmin.git
synced 2026-05-06 23:30:29 +01:00
881 lines
25 KiB
Perl
881 lines
25 KiB
Perl
# nftables-lib.pl
|
|
# Functions for reading and writing nftables rules
|
|
|
|
BEGIN { push(@INC, ".."); };
|
|
use WebminCore;
|
|
use strict;
|
|
use warnings;
|
|
our (%config, $module_config_directory);
|
|
&init_config();
|
|
|
|
# get_nftables_save([file])
|
|
# Returns a list of tables and their chains/rules
|
|
sub get_nftables_save
|
|
{
|
|
my ($file) = @_;
|
|
my $cmd = $config{'nft_cmd'} || &has_command("nft");
|
|
if (!$file) {
|
|
if ($config{'direct'}) {
|
|
$file = "$cmd list ruleset |";
|
|
} else {
|
|
$file = $config{'save_file'} || "$module_config_directory/nftables.conf";
|
|
}
|
|
}
|
|
return ( ) if (!$file);
|
|
|
|
my @rv;
|
|
my $table;
|
|
my $chain;
|
|
my $lnum = 0;
|
|
my $content;
|
|
open(my $fh, $file);
|
|
$content = do { local $/; <$fh> };
|
|
close($fh);
|
|
|
|
my @lines = split /\r?\n/, $content;
|
|
for(my $i=0; $i<@lines; $i++) {
|
|
my $line = $lines[$i];
|
|
$lnum++;
|
|
$line =~ s/#.*$//; # Ignore comments for now
|
|
|
|
if ($line =~ /^table\s+(\S+)\s+(\S+)\s+\{/) {
|
|
# Start of a table
|
|
$table = { 'name' => $2,
|
|
'family' => $1,
|
|
'line' => $lnum,
|
|
'rules' => [ ],
|
|
'chains' => { } };
|
|
push(@rv, $table);
|
|
$chain = undef;
|
|
}
|
|
elsif ($line =~ /^\s*chain\s+(\S+)\s+\{/) {
|
|
# Start of a chain
|
|
if ($table) {
|
|
$chain = $1;
|
|
$table->{'chains'}->{$chain} = { };
|
|
|
|
# Look at next line for chain definition
|
|
if ($lines[$i+1] =~ /^\s*type\s+(\S+)\s+hook\s+(\S+)\s+priority\s+([a-zA-Z0-9_-]+);\s+policy\s+(\S+);/) {
|
|
$table->{'chains'}->{$chain}->{'type'} = $1;
|
|
$table->{'chains'}->{$chain}->{'hook'} = $2;
|
|
$table->{'chains'}->{$chain}->{'priority'} = $3;
|
|
$table->{'chains'}->{$chain}->{'policy'} = $4;
|
|
$i++; # Skip next line
|
|
}
|
|
}
|
|
}
|
|
elsif ($line =~ /^\s*(.*?)$/ && $table && $chain && $1 ne "}") {
|
|
# A rule
|
|
my $rule_str = $1;
|
|
if ($rule_str =~ /\S/) {
|
|
my $rule = {
|
|
'text' => $rule_str,
|
|
'chain' => $chain,
|
|
'index' => scalar(@{$table->{'rules'}}),
|
|
'line' => $lnum
|
|
};
|
|
my $parsed = &parse_rule_text($rule_str);
|
|
if ($parsed) {
|
|
foreach my $k (keys %$parsed) {
|
|
$rule->{$k} = $parsed->{$k};
|
|
}
|
|
}
|
|
push(@{$table->{'rules'}}, $rule);
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
sub dump_nftables_save
|
|
{
|
|
my (@tables) = @_;
|
|
my $rv;
|
|
foreach my $t (@tables) {
|
|
if ($t->{'family'}) {
|
|
$rv .= "table $t->{'family'} $t->{'name'} {\n";
|
|
} else {
|
|
$rv .= "table $t->{'name'} {\n";
|
|
}
|
|
|
|
foreach my $c (keys %{$t->{'chains'}}) {
|
|
my $chain = $t->{'chains'}->{$c};
|
|
$rv .= "\tchain $c {\n";
|
|
if ($chain->{'type'}) {
|
|
$rv .= "\t\ttype $chain->{'type'} hook $chain->{'hook'} priority $chain->{'priority'}; policy $chain->{'policy'};\n";
|
|
}
|
|
|
|
# Add rules for this chain
|
|
my @rules = sort { $a->{'index'} <=> $b->{'index'} }
|
|
grep { ref($_) eq 'HASH' && $_->{'chain'} eq $c } @{$t->{'rules'}};
|
|
foreach my $r (@rules) {
|
|
$rv .= "\t\t$r->{'text'}\n";
|
|
}
|
|
$rv .= "\t}\n";
|
|
}
|
|
$rv .= "}\n";
|
|
}
|
|
return $rv;
|
|
}
|
|
|
|
# save_table(&table)
|
|
# Saves a single table to the save file or applies it
|
|
sub save_table
|
|
{
|
|
my ($table) = @_;
|
|
# Re-read all tables to ensure we have the full picture if we are overwriting the file
|
|
# But here we probably just want to update the specific table in the list of tables we have.
|
|
# Since we usually operate on a list of tables, we might need to pass the full list or
|
|
# re-read the state.
|
|
# For simplicity, we usually load all, modify one, and save all.
|
|
}
|
|
|
|
# save_configuration(@tables)
|
|
# Writes the configuration to the save file. If direct mode is on, applies it.
|
|
sub save_configuration
|
|
{
|
|
my (@tables) = @_;
|
|
my $out = &dump_nftables_save(@tables);
|
|
my $file = $config{'save_file'} || "$module_config_directory/nftables.conf";
|
|
|
|
# Write to file
|
|
&open_tempfile(my $fh, ">$file");
|
|
&print_tempfile($fh, $out);
|
|
&close_tempfile($fh);
|
|
|
|
if ($config{'direct'}) {
|
|
return &apply_restore($file);
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
# apply_restore([file])
|
|
# Applies the configuration from the save file
|
|
sub apply_restore
|
|
{
|
|
my ($file) = @_;
|
|
$file ||= $config{'save_file'} || "$module_config_directory/nftables.conf";
|
|
my $cmd = $config{'nft_cmd'} || &has_command("nft");
|
|
my $out = &backquote_logged("$cmd -f $file 2>&1");
|
|
if ($?) {
|
|
return "<pre>$out</pre>";
|
|
}
|
|
return undef;
|
|
}
|
|
|
|
# describe_rule(&rule)
|
|
sub describe_rule
|
|
{
|
|
my ($r) = @_;
|
|
my @conds;
|
|
if ($r->{'iif'}) {
|
|
push(@conds, &text('index_rule_iif', &html_escape($r->{'iif'})));
|
|
}
|
|
if ($r->{'oif'}) {
|
|
push(@conds, &text('index_rule_oif', &html_escape($r->{'oif'})));
|
|
}
|
|
if ($r->{'saddr'}) {
|
|
push(@conds, &text('index_rule_saddr', &html_escape($r->{'saddr'})));
|
|
}
|
|
if ($r->{'daddr'}) {
|
|
push(@conds, &text('index_rule_daddr', &html_escape($r->{'daddr'})));
|
|
}
|
|
if ($r->{'l4proto'} || ($r->{'proto'} && !$r->{'dport'} && !$r->{'sport'})) {
|
|
my $p = $r->{'l4proto'} || $r->{'proto'};
|
|
push(@conds, &text('index_rule_proto', &html_escape($p)));
|
|
}
|
|
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)
|
|
# Returns HTML for an interface chooser menu
|
|
sub interface_choice
|
|
{
|
|
my ($name, $value, $blanktext) = @_;
|
|
if (&foreign_check("net")) {
|
|
&foreign_require("net", "net-lib.pl");
|
|
return &net::interface_choice($name, $value, $blanktext, 0, 1);
|
|
}
|
|
else {
|
|
return &ui_textbox($name, $value, 20);
|
|
}
|
|
}
|
|
|
|
# get_webmin_port()
|
|
# Returns the configured Webmin port, or 10000 if unknown
|
|
sub get_webmin_port
|
|
{
|
|
my %miniserv;
|
|
if (&get_miniserv_config(\%miniserv) && $miniserv{'port'} =~ /^\d+$/) {
|
|
return $miniserv{'port'};
|
|
}
|
|
return 10000;
|
|
}
|
|
|
|
1;
|