Search and filtering in connections.cgi

Hi all,
inspired by Core 188 WUI changes - #15 by ms have made some changes in connections.cgi (under “Status” in WUI) and am searching for some critics, testings, enhancements and of course for constructive feedback.

The following has been enhanced →

The main changes are

  • Added zone filter to select network zones (LAN, INTERNET, DMZ, etc.) via clickable legend.
  • Included NAT IPs in filtering for all zones.
  • Added search function to filter connections by IP, port, or protocol. With jQuery toggle on/off for search section.
  • Display filter status showing active filters and connection count.
    Have tested so far LAN, INTERNET, WIRELESS and IPFire (also in combination) legend tabs which worked so far some tests might be nice for the other parts according to the zones filtering. Have also tested the search filter which worked so far too, IP addresse can also be filtered by one, two, three or specific four octets.

If someone wants to check it, in here git.ipfire.org Git - people/ummeegge/ipfire-2.x.git/commit all can be found.
To integrate it, you can use the blob/raw format and !!!Please do not forget to backup the existing connection.cgi!!!

Feel free to give it a try and give some feedback.

Paralell project:
currently a little hacky!!! It should deliver configurable refresh cycles, configrable via WUI, and as before above cklicable zone filtering, search by IP, port and protocol is possible too.

or for a fast overview Index of /~ummeegge/connections

If someone … :slight_smile: in here you can find all → git.ipfire.org Git - people/ummeegge/ipfire-2.x.git/commit

Best,

Erik

EDIT(s) - Git address wil always be updated in here:

  • 07.05.25 Fixes and enhancements
  • Fix multi-pattern search to use AND logic
  • Corrected search filter to apply AND instead of OR for IP, port, and protocol criteria.
  • Added case-insensitive protocol matching and input sanitization for robustness.
  • Added ipcolour cache and pre-filtered networks.
  • Replaced external sort with Perl sorting for conntrack table.
  • Removed redundant close(CONNTRACK) call.

09-05-2025:

  • Added parallel project for refresh intervals and a new get_table.cgi
    25.05.2025
  • Added source and destination NAT again into table
  • Updated also language files for new Core version 195
6 Likes

Hi @siosios, you are one the right track since both should be (not that far) combined with another (realtime with a new graphical solution) but please use another channel for this since this is here currently OT.

Thanks and best,

Erik

1 Like

@ummeegge
we are very happy with your solution :trophy:
regression countered :bullseye:
:mechanical_arm: :technologist:

no drawbacks found so far :detective:

Thank you for the flowers,
which version do/did you use for the testings ?

Best,

Erik

youre welcome :folded_hands:
the commit:
https://git.ipfire.org/?p=people/ummeegge/ipfire-2.x.git;a=commit;h=29ea599c07065e8a041f12ab9dec01a1bb5e7be0
including the get_table.cgi is in action here.

report:
connections.cgi?search_enabled=on&ip=&port=&protocol=tcp&refresh_interval=2
is working here non-stop for five days now :mechanical_arm:

1 Like

Source and destination NAT is now also integrated in version 2 git.ipfire.org Git - people/ummeegge/ipfire-2.x.git/commit .

Best,

Erik

2 Likes

working good :magnifying_glass_tilted_right: :+1:

tried version 2 and come up with this error on get_tables.cgi in the http log file

AH00574: ap_content_length_filter: apr_bucket_read() failed, referer: https://x.x.x.x

i tried to run get_tables.cgi via console and it just sits there waiting

[root@diamond cgi-bin]# perl get_table.cgi
Content-Type: application/json; charset=ISO-8859-1

went ahead and increased Timeout to 900

still nothing

Hi siosios and thanks for giving it a try. Have had the apr_bucket_read() failed longer time ago but it seemed to be fixed with the newer version.
Does the connections.cgi WUI gives nevertheless results back ? Did you used specific filters/search request ? What where your settings in refresh intervall ?

Can you compare

md5sum /srv/web/ipfire/cgi-bin/{connections.cgi,get_table.cgi}    
3ac4f523e3780f545994cc72978fcac4  /srv/web/ipfire/cgi-bin/connections.cgi
cf5ea918fde9e65c387199d3d7f16cd0  /srv/web/ipfire/cgi-bin/get_table.cgi
QUERY_STRING="zone=LAN" perl /srv/web/ipfire/cgi-bin/get_table.cgi

/usr/local/bin/getconntracktable | wc -l

free -m

the results and or check the commands while testing ?

Some first ideas,

Best,

Erik

[root@diamond cgi-bin]# md5sum /srv/web/ipfire/cgi-bin/{connections.cgi,get_table.cgi}
3ac4f523e3780f545994cc72978fcac4  /srv/web/ipfire/cgi-bin/connections.cgi
cf5ea918fde9e65c387199d3d7f16cd0  /srv/web/ipfire/cgi-bin/get_table.cgi
[root@diamond cgi-bin]# QUERY_STRING="zone=LAN" perl /srv/web/ipfire/cgi-bin/get_table.cgi
Content-Type: application/json; charset=ISO-8859-1

The query when run ends up with a blinking cursor, there are no errors in the http error log and nothing shows up (pics attached). the files were created using nano and chmod 755.

[root@diamond cgi-bin]# /usr/local/bin/getconntracktable | wc -l
697
[root@diamond cgi-bin]# free -m
               total        used        free      shared  buff/cache   available
Mem:           64221        3997       51140          80        9702       60223
Swap:           1023           0        1023




connections3

Running connections.cgi in console does not give any errors just comes back empty

        <br>
        <table class="tbl">
                <thead>
                        <tr>
                                <th>Protocol:</th>
                                <th colspan='2'>Source IP: Port</th>
                                <th></th>
                                <th colspan='2'>Dest. IP: Port</th>
                                <th></th>
                                <th colspan='2'>Data Transfer</th>
                                <th>Connection<br>Status</th>
                                <th>Expires<br>(H:M:S)</th>
                        </tr>
                </thead>
                <tbody>
                        <!-- Filled by JavaScript -->
                </tbody>
        </table>
</section>              </div>
        </div>

        <div id="footer" class='bigbox fixed'>
                <span class="pull-right">
                        <a href="https://www.ipfire.org/" target="_blank"><strong>IPFire.org</strong></a> &bull;
                        <a href="https://www.ipfire.org/donate" target="_blank">Support the IPFire project with your donation</a>
                </span>

                <strong>IPFire 2.29 (x86_64) - Core-Update 195 Development Build: master/4e8f4314
</strong>
        </div>
</body>
</html>

When i hit refresh or the search button i do see an error popup and quickly go away

Error loading data: 0 error

Hi all,
i think the problem is because of the amount of the connections, since the computation is a kind of expensive the problem is probably due to the sheer number of connection on your system.
Great that you tested it since i do not have this environment at home to check it.
Conclusion: I will erase version 2 since the code base is also too comprehensive and not suitable for bigger environments. But it was worth a try to deliver another way to present data on IPFire :slight_smile: .

Thanks for your check on this and GREAT that we did it here before making bigger noises :+1:

Best,

Erik

2 Likes

I wouldn’t get rid of version 2, I’ve fixed my issue and it turned out to be permissions on /usr/local/bin/getconntracktable were wrong. Anyway Ive added sort-able columns to the connections.cgi for those of you that want to do that but you’ll have to load version 2 of @ummeegge changed connections testing pages

#!/usr/bin/perl
###############################################################################
#                                                                             #
# IPFire.org - A linux based firewall                                         #
# Copyright (C) 2007-2025  IPFire Team  <info@ipfire.org>                     #
#                                                                             #
# This program is free software: you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation, either version 3 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License           #
# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
#                                                                             #
###############################################################################

use strict;
use CGI qw(escape);
use HTML::Entities;

# Enable for debugging
#use warnings;
#use CGI::Carp 'fatalsToBrowser';

require '/var/ipfire/general-functions.pl';
require "${General::swroot}/lang.pl";
require "${General::swroot}/header.pl";
require "${General::swroot}/ids-functions.pl";
require "${General::swroot}/location-functions.pl";
require "${General::swroot}/network-functions.pl";

# Color for multicast networks
my $colour_multicast = "#A0A0A0";

# Cache for IP-to-color mappings
my %ipcolour_cache = ();

# Load ethernet settings
my %settings = ();
&General::readhash("/var/ipfire/ethernet/settings", \%settings);

# Initialize known networks with their zone colors
my %networks = (
	"127.0.0.0/8" => ${Header::colourfw},
	"224.0.0.0/3" => $colour_multicast,
);

# Add network settings for each zone
foreach my $zone (qw(GREEN BLUE ORANGE)) {
	if (exists $settings{"${zone}_ADDRESS"} && defined $settings{"${zone}_ADDRESS"} && $settings{"${zone}_ADDRESS"} ne '' && $settings{"${zone}_ADDRESS"} =~ m/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/) {
		$networks{"$settings{\"${zone}_ADDRESS\"}/32"} = ${Header::colourfw};
	}
	next unless exists $settings{"${zone}_NETADDRESS"} && exists $settings{"${zone}_NETMASK"} && defined $settings{"${zone}_NETADDRESS"} && defined $settings{"${zone}_NETMASK"};
	my $netaddress = $settings{"${zone}_NETADDRESS"};
	my $netmask = $settings{"${zone}_NETMASK"};
	if (defined $netaddress && $netaddress ne '' && $netaddress =~ m/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ &&
	    defined $netmask && $netmask ne '' && $netmask =~ m/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/ &&
	    $netaddress ne '0.0.0.0' && $netmask ne '0.0.0.0') {
		$networks{"$netaddress/$netmask"} =
			$zone eq 'GREEN' ? ${Header::colourgreen} :
			$zone eq 'BLUE' ? ${Header::colourblue} :
			${Header::colourorange};
	}
}

# Add RED interface address
my $address = &IDS::get_red_address();
if ($address) {
	$networks{"${address}/32"} = ${Header::colourfw};
}

# Add aliases
my @aliases = &IDS::get_aliases();
for my $alias (@aliases) {
	$networks{"${alias}/32"} = ${Header::colourfw};
}

# Initialize interfaces
my %interfaces = ();
foreach my $zone (qw(GREEN BLUE ORANGE)) {
	if (exists $settings{"${zone}_DEV"} && defined $settings{"${zone}_DEV"} && $settings{"${zone}_DEV"} ne '' && $settings{"${zone}_DEV"} =~ m/^[a-zA-Z0-9_-]+$/) {
		$interfaces{$settings{"${zone}_DEV"}} =
			$zone eq 'GREEN' ? ${Header::colourgreen} :
			$zone eq 'BLUE' ? ${Header::colourblue} :
			${Header::colourorange};
	}
}
$interfaces{"gre[0-9]+"} = ${Header::colourvpn};
$interfaces{"vti[0-9]+"} = ${Header::colourvpn};
$interfaces{"tun[0-9]+"} = ${Header::colourovpn};

# Add routes to networks
my @routes = &General::system_output("ip", "route", "show");
foreach my $intf (keys %interfaces) {
	next if ($intf eq "");
	foreach my $route (grep(/dev ${intf}/, @routes)) {
		if ($route =~ m/^(\d+\.\d+\.\d+\.\d+\/\d+)/) {
			$networks{$1} = $interfaces{$intf};
		}
	}
}

# Add WireGuard settings
if (-e "/var/ipfire/wireguard/settings") {
	my %wgsettings = ();
	&General::readhash("/var/ipfire/wireguard/settings", \%wgsettings);
	if (exists $wgsettings{'CLIENT_POOL'} && defined $wgsettings{'CLIENT_POOL'} && $wgsettings{'CLIENT_POOL'} ne '') {
		$networks{$wgsettings{'CLIENT_POOL'}} = ${Header::colourwg};
	}
}

# Add WireGuard peers
if (-e "/var/ipfire/wireguard/peers") {
	my %wgpeers = ();
	&General::readhasharray("/var/ipfire/wireguard/peers", \%wgpeers);
	foreach my $key (keys %wgpeers) {
		my $networks = $wgpeers{$key}[8];
		my @networks = split(/\|/, $networks);
		foreach my $network (@networks) {
			$networks{$network} = ${Header::colourwg} if $network && &Network::check_subnet($network);
		}
	}
}

# Add OpenVPN settings
if (-e "${General::swroot}/ovpn/settings") {
	my %ovpnsettings = ();
	&General::readhash("${General::swroot}/ovpn/settings", \%ovpnsettings);
	if (exists $ovpnsettings{'DOVPN_SUBNET'} && defined $ovpnsettings{'DOVPN_SUBNET'} && $ovpnsettings{'DOVPN_SUBNET'} ne '') {
		$networks{$ovpnsettings{'DOVPN_SUBNET'}} = ${Header::colourovpn};
	}
}

# Add OpenVPN client subnets
if (-e "${General::swroot}/ovpn/ccd.conf") {
	open(OVPNSUB, "${General::swroot}/ovpn/ccd.conf") or next;
	while (my $line = <OVPNSUB>) {
		chomp $line;
		my @ovpn = split(',', $line);
		if (@ovpn >= 3 && defined $ovpn[2] && $ovpn[2] ne '' && &Network::check_subnet($ovpn[2])) {
			$networks{$ovpn[2]} = ${Header::colourovpn};
		}
	}
	close(OVPNSUB);
}

# Add IPsec subnets
open(IPSEC, "${General::swroot}/vpn/config") or next;
my @ipsec = <IPSEC>;
close(IPSEC);

foreach my $line (@ipsec) {
	chomp $line;
	my @vpn = split(',', $line);
	my @subnets = split(/\|/, $vpn[12]);
	for my $subnet (@subnets) {
		$networks{$subnet} = ${Header::colourvpn} if $subnet && &Network::check_subnet($subnet);
	}
}

# Add OpenVPN net-to-net subnets
if (-e "${General::swroot}/ovpn/n2nconf") {
	open(OVPNN2N, "${General::swroot}/ovpn/ovpnconfig") or next;
	while (my $line = <OVPNN2N>) {
		chomp $line;
		my @ovpn = split(',', $line);
		next if ($ovpn[4] ne 'net' || !defined $ovpn[12] || $ovpn[12] eq '');
		$networks{$ovpn[12]} = ${Header::colourovpn} if &Network::check_subnet($ovpn[12]);
	}
	close(OVPNN2N);
}

# Sort networks by prefix length
my @networks = reverse sort {
	&Network::get_prefix($a) <=> &Network::get_prefix($b)
} grep { defined($_) && &Network::check_subnet($_) } keys %networks;

# Define known zones with their colors
my %zones = (
	'LAN' => ${Header::colourgreen},
	'INTERNET' => ${Header::colourred},
	'DMZ' => ${Header::colourorange},
	'Wireless' => ${Header::colourblue},
	'IPFire' => ${Header::colourfw},
	'VPN' => ${Header::colourvpn},
	'WireGuard' => ${Header::colourwg},
	'OpenVPN' => ${Header::colourovpn},
	'Multicast' => $colour_multicast,
);

# Process CGI parameters
my $cgi = CGI->new;
my @valid_zones = qw(LAN INTERNET DMZ Wireless IPFire VPN WireGuard OpenVPN Multicast);
my @raw_zone_params = $cgi->multi_param('zone');
my @zone_params = grep { defined $_ && $_ ne '' } map { CGI::escapeHTML($_) } @raw_zone_params;
my @selected_zones = grep { my $z = $_; defined $z && $z ne '' && grep { $_ eq $z } @valid_zones } @zone_params;
my %selected_zones_hash = map { $_ => 1 } @selected_zones;
my $search_ip = $cgi->param('ip') || '';
my $search_port = $cgi->param('port') || '';
my $search_protocol = $cgi->param('protocol') || '';
my $search_enabled = $cgi->param('search_enabled') || '';
my $refresh_interval = $cgi->param('refresh_interval') || 0;

# Sanitize search parameters
if ($search_ip) {
	$search_ip =~ s/[^0-9.]//g;
}
if ($search_port) {
	$search_port =~ s/\D//g;
	if ($search_port < 0 || $search_port > 65535) {
		$search_port = '';
	}
}
if ($search_protocol) {
	$search_protocol =~ s/[^a-zA-Z0-9]//g;
}

# Output HTTP headers
&Header::showhttpheaders();

# Render page header and styles
&Header::openpage($Lang::tr{'connections'}, 1, <<'END'
<style>
	.search_fields { display: none; }
	#error_msg { color: red; margin-top: 10px; display: none; }
	th[data-sort] {
		cursor: pointer;
	}
	th.sort-asc::after {
		content: " ▶";
		font-size: smaller;
		color: #aaa;
	}
	th.sort-desc::after {
		content: " ◀";
		font-size: smaller;
		color: #aaa;
	}
</style>
<script src='/include/jquery.js'></script>
<script>
	$(document).ready(function() {
		if ($("#search_toggle").prop("checked")) {
			$(".search_fields").show();
		}
		$("#search_toggle").change(function() {
			$(".search_fields").toggle();
		});

		let refreshInterval = parseInt($("#refresh_interval").val() || 0) * 1000;
		let refreshTimer;

		let connectionsData = [];
		let sortColumn = 'protocol';
		let sortDirection = 'asc';

		function sortData(data, column, direction) {
			return data.sort(function(a, b) {
				let valA = a[column] || '';
				let valB = b[column] || '';

				if (column === 'bytes_in' || column === 'bytes_out') {
					valA = Number(valA);
					valB = Number(valB);
				} else if (column === 'ttl_seconds') {
					// Convert TTL to seconds for sorting
					valA = parseTTL(a.ttl);
					valB = parseTTL(b.ttl);
				} else {
					valA = valA.toString().toLowerCase();
					valB = valB.toString().toLowerCase();
				}

				if (valA < valB) return direction === 'asc' ? -1 : 1;
				if (valA > valB) return direction === 'asc' ? 1 : -1;
				return 0;
			});
		}

		function renderTable(data) {
			const tbody = $(".tbl tbody");
			tbody.empty();
			let connCount = 0;

			if (!data || !Array.isArray(data)) {
				$("#error_msg").text("Error: Invalid data format").show();
				$("#connection_count").text("(Error: Invalid data format)");
				return;
			}

			$("#error_msg").hide();
			$.each(data, function(i, item) {
				// NAT info extras
				const src_extra = (item.src_ret && item.src_ip !== item.src_ret) ?
					'<span style="color:#FFFFFF;"> ></span>  ' +
					'<a href="/cgi-bin/ipinfo.cgi?ip=' + encodeURIComponent(item.src_ret || '') + '">' +
					'<span style="color:#FFFFFF;">' + (item.src_ret || '') + '</span></a>' : '';
				const dst_extra = (item.dst_ret && item.dst_ip !== item.dst_ret) ?
					'<span style="color:#FFFFFF;"> ></span>  ' +
					'<a href="/cgi-bin/ipinfo.cgi?ip=' + encodeURIComponent(item.dst_ret || '') + '">' +
					'<span style="color:#FFFFFF;">' + (item.dst_ret || '') + '</span></a>' : '';
				const sport_extra = (item.src_ret_port && item.src_port !== item.src_ret_port) ?
					'<span style="color:#FFFFFF;"> ></span>  ' +
					'<a href="https://isc.sans.edu/port.html?port=' + encodeURIComponent(item.src_ret_port || '') + '" target="_blank" title="' + (item.src_ret_service || '') + '">' +
					'<span style="color:#FFFFFF;">' + (item.src_ret_port || '') + '</span></a>' : '';
				const dport_extra = (item.dst_ret_port && item.dst_port !== item.dst_ret_port) ?
					'<span style="color:#FFFFFF;">></span>  ' +
					'<a href="https://isc.sans.edu/port.html?port=' + encodeURIComponent(item.dst_ret_port || '') + '" target="_blank" title="' + (item.dst_ret_service || '') + '">' +
					'<span style="color:#FFFFFF;">' + (item.dst_ret_port || '') + '</span></a>' : '';

				const html = [
					'<tr>',
					'<td style="text-align:center">' + (item.protocol || '') + '</td>',
					'<td style="text-align:center; background-color:' + (item.src_colour || '#FFFFFF') + '">',
					'<a href="/cgi-bin/ipinfo.cgi?ip=' + encodeURIComponent(item.src_ip || '') + '"><span style="color:#FFFFFF;">' + (item.src_ip || '') + '</span></a>',
					src_extra,
					'</td>',
					'<td style="text-align:center; background-color:' + (item.src_colour || '#FFFFFF') + '">',
					'<a href="https://isc.sans.edu/port.html?port=' + encodeURIComponent(item.src_port || '') + '" target="_blank" title="' + (item.src_service || '') + '">',
					'<span style="color:#FFFFFF;">' + (item.src_port || '') + '</span>',
					'</a>',
					sport_extra,
					'</td>',
					'<td style="text-align:center; background-color:' + (item.src_colour || '#FFFFFF') + '">',
					'<a href="country.cgi#' + encodeURIComponent(item.src_country || '') + '">',
					'<img src="' + (item.src_flag_icon || '/images/flags/unknown.png') + '" border="0" align="absmiddle" alt="' + (item.src_country || '') + '" title="' + (item.src_country || '') + '" />',
					'</a>',
					'</td>',
					'<td style="text-align:center; background-color:' + (item.dst_colour || '#FFFFFF') + '">',
					'<a href="/cgi-bin/ipinfo.cgi?ip=' + encodeURIComponent(item.dst_ip || '') + '"><span style="color:#FFFFFF;">' + (item.dst_ip || '') + '</span></a>',
					dst_extra,
					'</td>',
					'<td style="text-align:center; background-color:' + (item.dst_colour || '#FFFFFF') + '">',
					'<a href="https://isc.sans.edu/port.html?port=' + encodeURIComponent(item.dst_port || '') + '" target="_blank" title="' + (item.dst_service || '') + '">',
					'<span style="color:#FFFFFF;">' + (item.dst_port || '') + '</span>',
					'</a>',
					dport_extra,
					'</td>',
					'<td style="text-align:center; background-color:' + (item.dst_colour || '#FFFFFF') + '">',
					'<a href="country.cgi#' + encodeURIComponent(item.dst_country || '') + '">',
					'<img src="' + (item.dst_flag_icon || '/images/flags/unknown.png') + '" border="0" align="absmiddle" alt="' + (item.dst_country || '') + '" title="' + (item.dst_country || '') + '" />',
					'</a>',
					'</td>',
					'<td class="text-right">' + (item.bytes_in || '') + '</td>',
					'<td class="text-right">' + (item.bytes_out || '') + '</td>',
					'<td style="text-align:center">' + (item.state || '') + '</td>',
					'<td style="text-align:center">' + (item.ttl || '') + '</td>',
					'</tr>'
				].join('');
				tbody.append(html);
				connCount++;
			});

			$("#connection_count").text('(' + connCount + ' $Lang::tr{\'connections\'} )');
			// Update sort indicators on headers
			$(".tbl thead th").removeClass("sort-asc sort-desc");
			$(".tbl thead th[data-sort='" + sortColumn + "']").addClass(sortDirection === 'asc' ? "sort-asc" : "sort-desc");
		}

		function updateTable() {
			const zones = $("input[name='zone']").map(function() { return $(this).val(); }).get();
			const params = {
				zone: zones,
				ip: $("input[name='ip']").val() || '',
				port: $("input[name='port']").val() || '',
				protocol: $("input[name='protocol']").val() || '',
				search_enabled: $("#search_toggle").is(":checked") ? 1 : 0
			};

			const queryString = $.param(params, true);

			$.ajax({
				url: '/cgi-bin/get_table.cgi?' + queryString,
				dataType: 'json',
				success: function(data) {
					if (!data || !Array.isArray(data)) {
						$("#error_msg").text("Error: Invalid data format").show();
						$("#connection_count").text("(Error: Invalid data format)");
						return;
					}
					$("#error_msg").hide();
					connectionsData = data;
					connectionsData = sortData(connectionsData, sortColumn, sortDirection);
					renderTable(connectionsData);
				},
				error: function(jqXHR, textStatus, errorThrown) {
					$("#error_msg").text("Error loading data: " + jqXHR.status + " " + textStatus).show();
					$("#connection_count").text("(Error loading data: " + jqXHR.status + " " + textStatus + ")");
				}
			});
		}

		// Click handler for sortable headers
		$(".tbl thead th[data-sort]").click(function() {
			const column = $(this).attr("data-sort");
			if (sortColumn === column) {
				sortDirection = (sortDirection === 'asc') ? 'desc' : 'asc';
			} else {
				sortColumn = column;
				sortDirection = 'asc';
			}
			connectionsData = sortData(connectionsData, sortColumn, sortDirection);
			renderTable(connectionsData);
		});

		$("#refresh_interval").change(function() {
			clearInterval(refreshTimer);
			refreshInterval = parseInt($(this).val() || 0) * 1000;
			if (refreshInterval > 0) {
				refreshTimer = setInterval(updateTable, refreshInterval);
			}
		});

		updateTable();
		if (refreshInterval > 0) {
			refreshTimer = setInterval(updateTable, refreshInterval);
		}
	});
</script>
END
);

# Render main page layout
&Header::openbigbox('100%', 'left');
&Header::opensection();

# Render zone legend
print <<END;
	<table style='width:100%'>
		<tr>
			<td style='text-align:center;'>
				<b>$Lang::tr{'legend'} :</b>
			</td>
END

foreach my $zone (@valid_zones) {
	my $style = $selected_zones_hash{$zone} ? "background-color: #e0e0e0;" : "";
	my $label = get_zone_label($zone) || $zone;
	my $href = build_zone_href($zone, \@selected_zones) || '#';
	print <<END;
			<td style='text-align:center; color:#FFFFFF; background-color:$zones{$zone}; font-weight:bold; $style'>
				<a href='$href' style='color:#FFFFFF; text-decoration:none;'>
					<b>$label</b>
				</a>
			</td>
END
}

print <<END;
		</tr>
	</table>
	<br>
	<div id="error_msg"></div>
END

# Generate filter text for active filters
my $filter_text = '';
if (@selected_zones || $search_enabled) {
	my @filter_parts;
	if (@selected_zones) {
		my @zone_labels = grep { defined $_ } map { get_zone_label($_) } @selected_zones;
		push @filter_parts, join(", ", @zone_labels) if @zone_labels;
	}
	if ($search_enabled) {
		push @filter_parts, ($Lang::tr{'ip address'} || 'IP address') . ": " . encode_entities($search_ip) if $search_ip && $search_ip ne '';
		push @filter_parts, "$Lang::tr{'port'}: " . encode_entities($search_port) if $search_port && $search_port ne '';
		push @filter_parts, "$Lang::tr{'protocol'}: " . encode_entities($search_protocol) if $search_protocol && $search_protocol ne '';
	}
	$filter_text = join(", ", @filter_parts) if @filter_parts;
}

# Render filter form
print <<END;
	<form method='get' action='$ENV{'SCRIPT_NAME'}'>
END

# Add hidden inputs for selected zones
foreach my $zone (@selected_zones) {
	print <<END;
		<input type='hidden' name='zone' value='@{[ CGI::escapeHTML($zone) ]}' />
END
}

print <<END;
		<label><input type='checkbox' id='search_toggle' name='search_enabled' @{[ $search_enabled ? 'checked' : '' ]}> $Lang::tr{'search'}</label>
		<div class='search_fields' style='margin-top:10px;'>
			<label>$Lang::tr{'ip address'}: <input type='text' name='ip' value='$search_ip' /></label>
			<label>$Lang::tr{'port'}: <input type='text' name='port' value='$search_port' /></label>
			<label>$Lang::tr{'protocol'} <input type='text' name='protocol' value='$search_protocol' /></label>
			<input type='submit' value='$Lang::tr{'search'}' />
		</div>
		@{[ $filter_text ? "<p><b>$Lang::tr{'connections filtered_by'} " . encode_entities($filter_text) . " <span id='connection_count'></span></b></p>" : '' ]}
		<label style='margin-top:10px; display:block;'>$Lang::tr{'connections refresh interval'}:
			<select id='refresh_interval' name='refresh_interval'>
				<option value='0' @{[ $refresh_interval == 0 ? 'selected' : '' ]}>$Lang::tr{'disabled'}</option>
				<option value='2' @{[ $refresh_interval == 2 ? 'selected' : '' ]}>2</option>
				<option value='5' @{[ $refresh_interval == 5 ? 'selected' : '' ]}>5</option>
				<option value='10' @{[ $refresh_interval == 10 ? 'selected' : '' ]}>10</option>
				<option value='30' @{[ $refresh_interval == 30 ? 'selected' : '' ]}>30</option>
				<option value='60' @{[ $refresh_interval == 60 ? 'selected' : '' ]}>60</option>
			</select>
		</label>
	</form>
	<br>
END

# Render connections table with sorting attributes on headers
print <<END;
	<table class="tbl">
		<thead>
			<tr>
				<th data-sort="protocol">$Lang::tr{'protocol'}</th>
				<th colspan='2' data-sort="src_ip">$Lang::tr{'source ip and port'}</th>
				<th></th>
				<th colspan='2' data-sort="dst_ip">$Lang::tr{'dest ip and port'}</th>
				<th></th>
				<th colspan='2'>$Lang::tr{'data transfer'}</th>
				<th data-sort="state">$Lang::tr{'connection'}<br>$Lang::tr{'status'}</th>
				<th>$Lang::tr{'expires'}<br>($Lang::tr{'hours:minutes:seconds'})</th>
			</tr>
		</thead>
		<tbody>
			<!-- Filled by JavaScript -->
		</tbody>
	</table>
END

# Close page layout
&Header::closesection();
&Header::closebigbox();
&Header::closepage();

# Determines the color for an IP address based on its network zone
sub ipcolour {
	my $address = shift;
	if (exists $ipcolour_cache{$address}) {
		return $ipcolour_cache{$address};
	}
	foreach my $network (@networks) {
		if (&Network::ip_address_in_network($address, $network)) {
			$ipcolour_cache{$address} = $networks{$network};
			return $networks{$network};
		}
	}
	$ipcolour_cache{$address} = ${Header::colourred};
	return ${Header::colourred};
}

# Builds a URL for toggling a zone filter
sub build_zone_href {
	my ($zone, $selected_zones_ref) = @_;
	return '#' unless defined $zone && $zone ne '';
	my @new_zones = @$selected_zones_ref;
	if ($selected_zones_hash{$zone}) {
		@new_zones = grep { $_ ne $zone } @new_zones;
	} else {
		push @new_zones, $zone;
	}
	# Only include zone parameters, reset search parameters
	my $href = "?" . join("&", map { "zone=" . CGI::escape($_) } @new_zones);
	return $href;
}

# Retrieves the display label for a zone
sub get_zone_label {
	my $zone = shift;
	return $zone unless defined $zone && $zone ne '';
	if ($zone eq 'IPFire') {
		return 'IPFire';
	} elsif ($zone eq 'Multicast') {
		return 'Multicast';
	} elsif ($zone eq 'OpenVPN') {
		return $Lang::tr{'OpenVPN'} || 'OpenVPN';
	} else {
		return $Lang::tr{lc($zone)} || $zone;
	}
}

1;

also modify: /var/ipfire/general-functions.pl lines 58 → 77
change:

sub system_output($) {
	my @command = @_;
	my $pid;
	my @output = ();

	unless ($pid = open(OUTPUT, "-|")) {
		open(STDERR, ">&STDOUT");
		exec { ${command[0]} } @command;
		die "Could not execute @command: $!";
	}

	waitpid($pid, 0);

	while (<OUTPUT>) {
		push(@output, $_);
	}
	close(OUTPUT);

	return @output;
}

to

sub system_output {
    my @command = @_;
    die "No command given" unless @command;

    my $pid = open(my $output_fh, '-|');
    defined $pid or die "Cannot fork: $!";

    if ($pid == 0) {
        # Child process
        # Replace STDOUT and STDERR with the output filehandle
        open(STDERR, '>&STDOUT') or die "Can't dup STDERR to STDOUT: $!";
        exec { $command[0] } @command or die "Cannot exec command: $!";
    }

    # Parent process
    my @output = <$output_fh>;  # Read all output from the command
    close($output_fh);

    waitpid($pid, 0);  # Wait for the child process to finish

    return @output;
}

NOTE: This isnt perfect by any means but its a start, also i must add that changing or modifying the code of the original install version of IPFire can possibly open up vulnerabilities in the firewall software …Use at your own risk.

1 Like

Hi @siosios ,
why do you change general-functions.pl ?

Best,

Erik

because of the amount of connections the system had it was getting hung up at that point and produced no output to the table

1 Like

@ummeegge

:prohibited: :raised_hand:
even it has flaws it is way more better as the stock one :chart_decreasing:

i was also granted the honor to experience the
error data: 0 error mentioned by @siosios
and siosios patch fixed it :rocket: :man_shrugging:

so let's follow the traditional linux way :puzzle_piece:

1 Like

Hello @current_user ,
have leave it for the first at his place but am currently thinking/checking about another approach according to the realtime presentation of IPFire data also for other CGIs (which one might be nice ?). But as mentioned above, this will be a comprehensive code base and i think also not interesting for merge into the distro.

But thank you both again for your positiv feedback :slight_smile: .

Best,

Erik

1 Like

@ummeegge
your welcome :wink:
and i am happy you made this so i can use
the search and filter on y remaining ipfire :zany_face:

i dont know if it is funny or just sad that
we are just 3 really using the connections page :performing_arts:

however a realtime presentation [maybe like opnsense]
would be a very helpful feature even if it is only 3 for users :rocket:

2 Likes

Hello, i just tried version 1 and get a search for ip port and protcol, but since version 2 i only get error 500.

Sadly, I’m not as good at troubleshooting as @siaggio, so I’ll just try to see if it works here. For now, I’m using the old connections.cgi, but I’m excited to see what’s next.
Hey, hope you’re doing well!