Merge pull request #2371 from webmin/dev/bandwidth-firewalld

Add support for bandwidth monitoring with Firewalld and Journald
This commit is contained in:
Jamie Cameron
2025-01-23 16:03:16 -08:00
committed by GitHub
8 changed files with 250 additions and 57 deletions

View File

@@ -8,7 +8,11 @@
BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();
if (&foreign_installed("syslog-ng", 1) == 2) {
if (&has_command('journalctl')) {
$syslog_module = undef;
$syslog_journald = "journald" ;
}
elsif (&foreign_installed("syslog-ng", 1) == 2) {
&foreign_require("syslog-ng");
$syslog_module = "syslog-ng";
}
@@ -16,18 +20,20 @@ elsif (&foreign_installed("syslog")) {
&foreign_require("syslog");
$syslog_module = "syslog";
}
else {
$syslog_module = undef;
}
&foreign_require("cron", "cron-lib.pl");
&foreign_require("net", "net-lib.pl");
%access = &get_module_acl();
$bandwidth_log = $config{'bandwidth_log'} || "/var/log/bandwidth";
$hours_dir = $config{'bandwidth_dir'} || "$module_config_directory/hours";
$hours_dir = $config{'bandwidth_dir'} ||
(-e "$module_config_directory/hours" ?
"$module_config_directory/hours" :
"$module_var_directory/hours");
$cron_cmd = "$module_config_directory/rotate.pl";
$pid_file = "$module_config_directory/rotate.pid";
$pid_file = -e "$module_config_directory/rotate.pid" ?
"$module_config_directory/rotate.pid" :
"$module_var_directory/rotate.pid";
# list_hours()
# Returns a list of all hours for which traffic is available
@@ -163,7 +169,7 @@ return &ui_textbox("$_[2]_hour", $_[0], 2).":".
sub detect_firewall_system
{
local $m;
foreach $m ("shorewall", "firewall", "ipfw", "ipfilter") {
foreach $m ("shorewall", "firewalld", "firewall", "ipfw", "ipfilter") {
return $m if (&check_firewall_system($m));
}
return undef;
@@ -220,6 +226,144 @@ sub is_server_port
{
}
############### functions for FirewallD #################
# get_firewalld_rule(family, chain, direction, iface)
# Returns the rich rule for logging packets
sub get_firewalld_rule
{
my ($family, $chain, $direction, $iface) = @_;
# Define rule components
my %switch = (
'in' => 'i',
'out' => 'o',
);
# Construct and return the rich rule
my $udirection = uc($direction);
my $rule = {
'family' => $family,
'table' => 'filter',
'chain' => uc($chain),
'priority' => 0,
'rule' => "-$switch{$direction} $iface -j LOG \
--log-prefix BANDWIDTH_$udirection:",
};
return &firewalld::construct_direct_rule($rule);
}
# check_firewalld_rules(iface)
# Returns 1 if the FirewallD rules needed are setup, 0 if not
sub check_firewalld_rules
{
my $iface = shift // $config{'iface'};
&foreign_require("firewalld"); # Load the firewalld module
my %ip_families = &firewalld::check_ip_family();
my @directions = ('in', 'out');
my @chains = ('input', 'output', 'forward');
my $conflicting = sub {
my ($chain, $direction) = @_;
return 1 if ($direction eq 'out' && $chain eq 'input');
return 1 if ($direction eq 'in' && $chain eq 'output');
};
# Check each direction for each available IP family
for my $chain (@chains) {
# If IPv4 is available
if ($ip_families{ipv4}) {
foreach my $direction (@directions) {
next if ($conflicting->($chain, $direction));
return 0 if (!&firewalld::check_direct_rule(
&get_firewalld_rule('ipv4', $chain,
$direction, $iface)));
}
}
# If IPv6 is available
if ($ip_families{ipv6}) {
foreach my $direction (@directions) {
next if ($conflicting->($chain, $direction));
return 0 if (!&firewalld::check_direct_rule(
&get_firewalld_rule('ipv6', $chain,
$direction, $iface)));
}
}
}
return 1;
}
# setup_firewalld_rules()
# If any FirewallD rules are missing, add them
sub firewalld_rules_control
{
my ($action, $iface) = @_;
&foreign_require("firewalld");
my %ip_families = &firewalld::check_ip_family();
my @directions = ('in', 'out');
my @chains = ('input', 'output', 'forward');
my $conflicting = sub {
my ($chain, $direction) = @_;
return 1 if ($direction eq 'out' && $chain eq 'input');
return 1 if ($direction eq 'in' && $chain eq 'output');
};
# Add the rules for each direction and IP family
foreach my $chain (@chains) {
if ($ip_families{ipv4}) {
foreach my $direction (@directions) {
next if ($conflicting->($chain, $direction));
my ($out, $rs) = &firewalld::direct_rule($action, {
'permanent' => 1,
'rule' => &get_firewalld_rule('ipv4', $chain,
$direction, $iface),
});
return $out if ($rs);
}
}
if ($ip_families{ipv6}) {
foreach my $direction (@directions) {
next if ($conflicting->($chain, $direction));
my ($out, $rs) = &firewalld::direct_rule($action, {
'permanent' => 1,
'rule' => &get_firewalld_rule('ipv6', $chain,
$direction, $iface),
});
return $out if ($rs);
}
}
}
return &firewalld::apply_firewalld();
}
# setup_firewalld_rules(iface)
# If any FirewallD rules are missing, add them
sub setup_firewalld_rules
{
my $iface = shift // $config{'iface'};
return &firewalld_rules_control('add', $iface);
}
# delete_firewalld_rules()
# Delete firewall rules for bandwidth logging
sub delete_firewalld_rules
{
my $iface = shift // $config{'iface'};
return &firewalld_rules_control('remove', $iface);
}
# process_firewalld_line(line, &hours, time-now)
# Process an IPtables firewall line, and returns 1 if successful
sub process_firewalld_line
{
return &process_firewall_line(@_);
}
# get_firewalld_loglevel()
sub get_firewalld_loglevel
{
return ( "kern.=debug" );
}
############### functions for IPtables #################
# check_firewall_rules()

View File

@@ -1,3 +1,3 @@
firewall_system=Firewall type,4,firewall-IPtables,ipfw-IPFW,ipfilter-IPFilter,shorewall-Shorewall,-Detect automatically
firewall_system=Firewall type,4,-Detect automatically,firewalld-Firewalld,firewall-IPtables,ipfw-IPFW,ipfilter-IPFilter,shorewall-Shorewall
bandwidth_log=Log file to create for firewall messages,0
bandwidth_dir=Directory for bandwidth data,3,Default (/etc/webmin/bandwidth/hours)

View File

@@ -34,7 +34,7 @@ foreach $m (split(/\s+/, $module_info{'depends'})) {
}
# Make sure one of the syslog modules works
if (!$syslog_module) {
if (!$syslog_module && !$syslog_journald) {
&ui_print_header(undef, $text{'index_title'}, "", "intro",
1, 1);
&ui_print_endpage(&text('index_esyslog'));
@@ -66,16 +66,20 @@ else {
1, 1, 0, undef, undef, undef,
&text('index_firesys',
$text{'system_'.$config{'firewall_system'}},
$text{'syslog_'.$syslog_module}));
$text{'syslog_'.($syslog_module || $syslog_journald)}));
# Make sure the needed firewall rules and syslog entry are in place
$missingrule = !&check_rules();
if ($syslog_module eq "syslog") {
if ($syslog_journald) {
# Systemd journal
$sysconf = 1; # nothing to do
}
elsif ($syslog_module eq "syslog") {
# Normal syslog
$conf = &syslog::get_config();
$sysconf = &find_sysconf($conf);
}
else {
elsif ($syslog_module eq "syslog-ng") {
# Syslog-ng
$conf = &syslog_ng::get_config();
($ngdest, $ngfilter, $nglog) = &find_sysconf_ng($conf);
@@ -374,10 +378,13 @@ if (@hours) {
push(@cols, $k);
}
my $bar = sprintf
"<img src=images/blue.gif width=%d height=10>",
"<span style='display: flex;'>".
"<img src=images/blue.gif width=%d% ".
"height=10>",
$max ? int($width * $icount{$k}/$max)+1 : 1;
$bar .= sprintf
"<img src=images/red.gif width=%d height=10>",
"<img src=images/red.gif width=%d% ".
"height=10></span>",
$max ? int($width * $ocount{$k}/$max)+1 : 1;
push(@cols, $bar);
push(@cols, &nice_size($icount{$k}),

View File

@@ -4,14 +4,14 @@ index_efiresys2=The configured $1 firewall system was not found on your system.
index_elog=The file $1 used for bandwidth logging is actually a directory on your system. Adjust the <a href='$2'>module configuration</a> to use a different path.
index_edir=The directory for storing bandwidth data $1 does not exist, or is not a directory. Adjust the <a href='$2'>module configuration</a> to use a different path.
index_emod=The Webmin module $1 is not installed on this system or is not supported by your OS. The Bandwidth Monitoring module cannot operate without it.
index_esyslog=Neither of the System Logs modules are installed on this system and supported by your OS. The Bandwidth Monitoring module cannot operate without one of them.
index_firesys=Using $1 firewall and $2
index_esyslog=None of the supported system logging systems, such as Journald, Rsyslog, or others, are installed or supported by your OS. This module requires at least one of them to operate correctly.
index_firesys=Using $1 with $2
index_setupcannot=However, you do not have permissions to set it up!
index_setupdesc=Before this module can report network usage, it needs to be set up to monitor traffic on the chosen network interface.
index_setupdesc2=This module will log <em>all</em> network traffic sent or received on the selected interface, which can consume a large amount of disk space and CPU time on a fast network connection.
index_missing3=Several firewall rules must be added, and a syslog configuration entry created.
index_missing2=Several firewall rules must be added.
index_missing1=A syslog configuration entry must be created.
index_missing3=Several firewall rules need to be added, along with a configuration entry for the system logging system.
index_missing2=Several firewall rules need to be added.
index_missing1=A configuration entry for the system logging system must be created.
index_iface=Chosen network interface
index_other=Other..
index_setup=Setup Now
@@ -55,7 +55,7 @@ index_low=Server ports only?
index_resolv=Resolve hostnames?
index_nomatch=No traffic matched the selected criteria.
index_turnoff=Turn Off Monitoring
index_turnoffdesc=Click this button to remove the firewall rules, syslog configuration and Cron job used for bandwidth monitoring. All existing collected data will remain untouched.
index_turnoffdesc=Click this button to remove the firewall rules, related system logging configuration, and Cron job used for bandwidth monitoring. The existing collected data will remain intact.
index_rotate=Update Statistics
index_rotatedesc=Click this button to process all logged network traffic up to the current time, making it immediately available for reporting.
index_eiptables=Warning - Your IPtables configuration has an error : $1. Setting up bandwidth monitoring will clear all firewall rules.
@@ -73,16 +73,19 @@ setup_ecannot=You are not allowed to enable monitoring
setup_eiface=Missing or invalid interface name
setup_ezone=Failed to find Shorewall zone for the selected interface
system_firewalld=Firewalld
system_firewall=IPtables
system_ipfw=IPFW
system_ipfilter=IPFilter
system_shorewall=Shorewall
syslog_journald=Journald
syslog_syslog=Syslog
syslog_syslog-ng=Syslog-NG
rotate_title=Updating Statistics
rotate_doing=Processing logged network traffic ..
rotate_done=.. done.
rotate_done=.. done
rotate_failed=.. failed : $1
__norefs=1

View File

@@ -2,18 +2,11 @@
# Run rotate.pl now
require './bandwidth-lib.pl';
&ui_print_header(undef, $text{'rotate_title'}, "");
print "<b>$text{'rotate_doing'}</b>\n";
print "<pre>";
open(OUT, "$cron_cmd 2>&1 |");
while(<OUT>) {
print &html_escape($_);
}
close(OUT);
print "</pre>\n";
print "<b>$text{'rotate_done'}</b><p>\n";
&ui_print_unbuffered_header(undef, $text{'rotate_title'}, "");
print &ui_text_wrap($text{'rotate_doing'});
my ($out) = &backquote_logged("$cron_cmd 2>&1");
$out = $out ? &text('rotate_failed', &html_strip($out)) : $text{'rotate_done'};
print &ui_text_wrap("<br>$out");
&webmin_log("rotate");
&ui_print_footer("", $text{'index_return'});

View File

@@ -2,45 +2,74 @@
# Parse the firewall log and rotate it
$no_acl_check++;
require './bandwidth-lib.pl';
use Time::Local;
require './bandwidth-lib.pl';
our (%config, $module_config_file, $module_var_directory, $pid_file,
$syslog_module, $syslog_journald, $bandwidth_log);
my ($logfh, $timestamp_file, $lastline);
# Detect firewall system if needed
if (!$config{'firewall_system'}) {
$sys = &detect_firewall_system();
my $sys = &detect_firewall_system();
if ($sys) {
$config{'firewall_system'} = $sys;
&lock_file($module_config_file);
&save_module_config();
&unlock_file($module_config_file);
}
else {
die "Failed to detect firewall system!";
die("Failed to detect firewall system!\n");
}
}
# See if this process is already running
if ($pid = &check_pid_file($pid_file)) {
if (my $pid = &check_pid_file($pid_file)) {
print STDERR "rotate.pl process $pid is already running\n";
exit;
exit(1);
}
open(PID, ">$pid_file");
print PID $$,"\n";
close(PID);
open(my $pid, ">$pid_file");
print $pid $$,"\n";
close($pid);
$time_now = time();
@time_now = localtime($time_now);
@hours = ( );
# Get the current time
my $time_now = time();
my @time_now = localtime($time_now);
my @hours = ( );
# Pre-process command
&pre_process();
# Open the log file or pipe to journalctl
if ($syslog_journald) {
$timestamp_file = "$module_var_directory/last-processed";
my $last_processed = 0;
if (-r $timestamp_file) {
$last_processed = &read_file_contents($timestamp_file);
chomp($last_processed);
$last_processed = int($last_processed) || 0;
}
my $journal_cmd = &has_command("journalctl");
$journal_cmd = "$journal_cmd -k --since=\@$last_processed ".
"--until=\@$time_now --grep=\"BANDWIDTH_(IN|OUT):\"";
open($logfh, '-|', $journal_cmd) ||
die("Cannot open $journal_cmd pipe: $!\n");
}
else {
open($logfh, "<".$bandwidth_log) ||
die("Cannot open $bandwidth_log: $!\n");
}
# Scan the entries in the log file
&pre_process();
open(LOG, "<".$bandwidth_log);
while(<LOG>) {
while(<$logfh>) {
if (&process_line($_, \@hours, $time_now)) {
# Found a valid line
$lastline = $_;
}
elsif (/last\s+message\s+repeated\s+(\d+)/) {
# re-process the last line N-1 times
for($i=0; $i<$1-1; $i++) {
for(my $i=0; $i<$1-1; $i++) {
&process_line($lastline, \@hours, $time_now);
}
}
@@ -48,20 +77,29 @@ while(<LOG>) {
#print "skipping $_";
}
}
close(LOG);
close($logfh);
# Save all hours
foreach $hour (@hours) {
foreach my $hour (@hours) {
&save_hour($hour);
}
# Truncate the file (if it exists) and notify syslog
if (-r $bandwidth_log) {
open(LOG, ">".$bandwidth_log);
close(LOG);
open(my $log, ">".$bandwidth_log);
close($log);
}
&foreign_call($syslog_module, "signal_syslog") if (!$syslog_journald);
# Save last collection time to start from here next time
if ($syslog_journald && @hours) {
&lock_file($timestamp_file);
&write_file_contents($timestamp_file, $time_now);
&unlock_file($timestamp_file);
}
&foreign_call($syslog_module, "signal_syslog");
# Remove PID file
unlink($pid_file);
# Exit with success
exit(0);

View File

@@ -13,7 +13,11 @@ $iface =~ /^\S+$/ || &error($text{'setup_eiface'});
$err = &setup_rules($iface);
&error($err) if ($err);
if ($syslog_module eq "syslog") {
if ($syslog_journald) {
# Systemd journal
# No setup needed
}
elsif ($syslog_module eq "syslog") {
# Add syslog entry
$conf = &syslog::get_config();
$sysconf = &find_sysconf($conf);
@@ -32,7 +36,7 @@ if ($syslog_module eq "syslog") {
&error($err) if ($err);
}
}
else {
elsif ($syslog_module eq "syslog-ng") {
# Add syslog-ng entry
$conf = &syslog_ng::get_config();
($dest, $filter, $log) = &find_sysconf_ng($conf);

View File

@@ -9,7 +9,11 @@ $access{'setup'} || &error($text{'turnoff_ecannot'});
$err = &delete_rules();
&error($err) if ($err);
if ($syslog_module eq "syslog") {
if ($syslog_journald) {
# Systemd journal
# Nothing to do
}
elsif ($syslog_module eq "syslog") {
# Remove syslog entry
$conf = &syslog::get_config();
$sysconf = &find_sysconf($conf);
@@ -21,7 +25,7 @@ if ($syslog_module eq "syslog") {
&error($err) if ($err);
}
}
else {
elsif ($syslog_module eq "syslog-ng") {
# Remove syslog-ng entries
$conf = &syslog_ng::get_config();
($dest, $filter, $log) = &find_sysconf_ng($conf);