#!/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 Switch;

# only enable the following for debugging purposes
#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";

my $colour_multicast = "#A0A0A0";

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

&Header::showhttpheaders();

# Collect all known networks
my %networks = (
	# Localhost
	"127.0.0.0/8" => ${Header::colourfw},

	# Multicast
	"224.0.0.0/3" => $colour_multicast,

	# GREEN
	"$settings{'GREEN_ADDRESS'}/32" => ${Header::colourfw},
	"$settings{'GREEN_NETADDRESS'}/$settings{'GREEN_NETMASK'}" => ${Header::colourgreen},

	# BLUE
	"$settings{'BLUE_ADDRESS'}/32" => ${Header::colourfw},
	"$settings{'BLUE_NETADDRESS'}/$settings{'BLUE_NETMASK'}" => ${Header::colourblue},

	# ORANGE
	"$settings{'ORANGE_ADDRESS'}/32" => ${Header::colourfw},
	"$settings{'ORANGE_NETADDRESS'}/$settings{'ORANGE_NETMASK'}" => ${Header::colourorange},
);

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

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

my %interfaces = (
	$settings{'GREEN_DEV'}  => ${Header::colourgreen},
	$settings{'BLUE_DEV'}   => ${Header::colourblue},
	$settings{'ORANGE_DEV'} => ${Header::colourorange},

	# IPsec
	"gre[0-9]+"             => ${Header::colourvpn},
	"vti[0-9]+"             => ${Header::colourvpn},

	# OpenVPN
	"tun[0-9]+"             => ${Header::colourovpn},
);

my @routes = &General::system_output("ip", "route", "show");

# Find all routes
foreach my $intf (keys %interfaces) {
	# Skip empty interfaces
	next if ($intf eq "");

	foreach my $route (grep(/dev ${intf}/, @routes)) {
		if ($route =~ m/^(\d+\.\d+\.\d+\.\d+\/\d+)/) {
			$networks{$1} = $interfaces{$intf};
		}
	}
}

# Load the WireGuard client pool
if (-e "/var/ipfire/wireguard/settings") {
	my %wgsettings = ();

	&General::readhash("/var/ipfire/wireguard/settings", \%wgsettings);

	$networks{$wgsettings{'CLIENT_POOL'}} = ${Header::colourwg};
}

# Load routed WireGuard networks
if (-e "/var/ipfire/wireguard/peers") {
	my %wgpeers = ();

	# Load all peers
	&General::readhasharray("/var/ipfire/wireguard/peers", \%wgpeers);

	foreach my $key (keys %wgpeers) {
		my $networks = $wgpeers{$key}[8];

		# Split the string
		my @networks = split(/\|/, $networks);

		foreach my $network (@networks) {
			$networks{$network} = ${Header::colourwg};
		}
	}
}

# Add OpenVPN net and RED/BLUE/ORANGE entry (when appropriate)
if (-e "${General::swroot}/ovpn/settings") {
	my %ovpnsettings = ();
	&General::readhash("${General::swroot}/ovpn/settings", \%ovpnsettings);

	$networks{$ovpnsettings{'DOVPN_SUBNET'}} = ${Header::colourovpn};
}

# Add OpenVPN net for custom OVPNs
if (-e "${General::swroot}/ovpn/ccd.conf") {
	open(OVPNSUB, "${General::swroot}/ovpn/ccd.conf");
	foreach my $line (<OVPNSUB>) {
		my @ovpn = split(',', $line);

		$networks{$ovpn[3]} = ${Header::colourovpn};
	}
	close(OVPNSUB);
}

open(IPSEC, "${General::swroot}/vpn/config");
my @ipsec = <IPSEC>;
close(IPSEC);

foreach my $line (@ipsec) {
	my @vpn = split(',', $line);

	my @subnets = split(/\|/, $vpn[12]);
	for my $subnet (@subnets) {
		$networks{$subnet} = ${Header::colourvpn};
	}
}

if (-e "${General::swroot}/ovpn/n2nconf") {
	open(OVPNN2N, "${General::swroot}/ovpn/ovpnconfig");
	foreach my $line (<OVPNN2N>) {
		my @ovpn = split(',', $line);
		next if ($ovpn[4] ne 'net');

		$networks{$ovpn[12]} = ${Header::colourovpn};
	}
	close(OVPNN2N);
}

# Show the page.
&Header::openpage($Lang::tr{'connections'}, 1, '');
&Header::openbigbox('100%', 'left');
&Header::opensection();

# Print legend.
print <<END;
	<table style='width:100%'>
		<tr>
			<td style='text-align:center;'>
				<b>$Lang::tr{'legend'} :</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:${Header::colourgreen}; font-weight:bold;'>
				<b>$Lang::tr{'lan'}</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:${Header::colourred};'>
				<b>$Lang::tr{'internet'}</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:${Header::colourorange};'>
				<b>$Lang::tr{'dmz'}</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:${Header::colourblue};'>
				<b>$Lang::tr{'wireless'}</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:${Header::colourfw};'>
				<b>IPFire</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:${Header::colourvpn};'>
				<b>$Lang::tr{'vpn'}</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:${Header::colourwg};'>
				<b>$Lang::tr{'wireguard'}</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:${Header::colourovpn};'>
				<b>$Lang::tr{'OpenVPN'}</b>
			</td>
			<td style='text-align:center; color:#FFFFFF; background-color:$colour_multicast;'>
				<b>Multicast</b>
			</td>
		</tr>
	</table>
	<br>
END

# Print table header
print <<END;
	<table class="tbl">
		<tr>
			<th>
				$Lang::tr{'protocol'}
			</th>
			<th colspan='2'>
				$Lang::tr{'source ip and port'}
			</th>
			<th></th>
			<th colspan='2'>
				$Lang::tr{'dest ip and port'}
			</th>
			<th></th>
			<th colspan='2'>
				$Lang::tr{'data transfer'}
			</th>
			<th>
				$Lang::tr{'connection'}<br>$Lang::tr{'status'}
			</th>
			<th>
				$Lang::tr{'expires'}<br>($Lang::tr{'hours:minutes:seconds'})
			</th>
		</tr>
END

# Read and sort the connection tracking table
open(CONNTRACK, "/usr/local/bin/getconntracktable | sort -k 5,5 --numeric-sort --reverse |")
	or die "Unable to read conntrack table";

foreach my $line (<CONNTRACK>) {
	my @conn = split(' ', $line);

	# The first bit is the l3 protocol.
	my $l3proto = $conn[0];

	# Skip everything that is not IPv4.
	if ($l3proto ne 'ipv4') {
		next;
	}

	# L4 protocol (tcp, udp, ...).
	my $l4proto = $conn[2];

	# Translate unknown protocols.
	if ($l4proto eq 'unknown') {
		my $l4protonum = $conn[3];
		if ($l4protonum eq '2') {
			$l4proto = 'IGMP';
		} elsif ($l4protonum eq '4') {
			$l4proto = 'IPv4 Encap';
		} elsif ($l4protonum eq '33') {
			$l4proto = 'DCCP';
		} elsif ($l4protonum eq '41') {
			$l4proto = 'IPv6 Encap';
		} elsif ($l4protonum eq '50') {
			$l4proto = 'ESP';
		} elsif ($l4protonum eq '51') {
			$l4proto = 'AH';
		} elsif ($l4protonum eq '132') {
			$l4proto = 'SCTP';
		} else {
			$l4proto = $l4protonum;
		}
	} else {
		$l4proto = uc($l4proto);
	}

	# Source and destination.
	my $sip;
	my $sip_ret;
	my $dip;
	my $dip_ret;
	my $sport;
	my $sport_ret;
	my $dport;
	my $dport_ret;
	my @packets;
	my @bytes;

	my $ttl = $conn[4];
	my $state;
	if ($l4proto eq 'TCP') {
		$state = $conn[5];
	}

	# Kick out everything that is not IPv4.
	foreach my $item (@conn) {
		my ($key, $val) = split('=', $item);

		switch ($key) {
			case "src" {
				if ($sip == "") {
					$sip = $val;
				} else {
					$dip_ret = $val;
				}
			}
			case "dst" {
				if ($dip == "") {
					$dip = $val;
				} else {
					$sip_ret = $val;
				}
			}
			case "sport" {
				if ($sport == "") {
					$sport = $val;
				} else {
					$dport_ret = $val;
				}
			}
			case "dport" {
				if ($dport == "") {
					$dport = $val;
				} else {
					$sport_ret = $val;
				}
			}
			case "packets" {
				push(@packets, $val);
			}
			case "bytes" {
				push(@bytes, $val);
			}
		}
	}

	my $sip_colour = ipcolour($sip);
	# use colour of destination network for DNAT
	my $dip_colour = $dip ne $dip_ret ? ipcolour($dip_ret) : ipcolour($dip);

	my $sserv = '';
	if ($sport < 1024) {
		$sserv = uc(getservbyport($sport, lc($l4proto)));
	}

	my $dserv = '';
	if ($dport < 1024) {
		$dserv = uc(getservbyport($dport, lc($l4proto)));
	}

	# Format bytes
	my $bytes_in  = &General::formatBytes($bytes[0]);
	my $bytes_out = &General::formatBytes($bytes[1]);

	# enumerate location information
	my $srcccode = &Location::Functions::lookup_country_code($sip_ret);
	my $src_flag_icon = &Location::Functions::get_flag_icon($srcccode);
	my $dstccode = &Location::Functions::lookup_country_code($dip_ret);
	my $dst_flag_icon = &Location::Functions::get_flag_icon($dstccode);

	# Format TTL
	$ttl = &General::format_time($ttl);

	my $sip_extra;
	if ($sip_ret && $sip ne $sip_ret) {
		$sip_extra = "<span style='color:#FFFFFF;'>&gt;</span> ";
		$sip_extra .= "<a href='/cgi-bin/ipinfo.cgi?ip=$sip_ret'>";
		$sip_extra .= "	<span style='color:#FFFFFF;'>$sip_ret</span>";
		$sip_extra .= "</a>";
	}

	my $dip_extra;
	if ($dip_ret && $dip ne $dip_ret) {
		$dip_extra = "<span style='color:#FFFFFF;'>&gt;</span> ";
		$dip_extra .= "<a href='/cgi-bin/ipinfo.cgi?ip=$dip_ret'>";
		$dip_extra .= " <span style='color:#FFFFFF;'>$dip_ret</span>";
		$dip_extra .= "</a>";
	}

	my $sport_extra;
	if ($sport ne $sport_ret) {
		my $sserv_ret = '';
		if ($sport_ret < 1024) {
			$sserv_ret = uc(getservbyport($sport_ret, lc($l4proto)));
		}

		$sport_extra = "<span style='color:#FFFFFF;'>&gt;</span> ";
		$sport_extra .= "<a href='https://isc.sans.edu/port.html?port=$sport_ret' target='top' title='$sserv_ret'>";
		$sport_extra .= " <span style='color:#FFFFFF;'>$sport_ret</span>";
		$sport_extra .= "</a>";
	}

	my $dport_extra;
	if ($dport ne $dport_ret) {
		my $dserv_ret = '';
		if ($dport_ret < 1024) {
			$dserv_ret = uc(getservbyport($dport_ret, lc($l4proto)));
		}

		$dport_extra = "<span style='color:#FFFFFF;'>&gt;</span> ";
		$dport_extra .= "<a href='https://isc.sans.edu/port.html?port=$dport_ret' target='top' title='$dserv_ret'>";
		$dport_extra .= " <span style='color:#FFFFFF;'>$dport_ret</span>";
		$dport_extra .= "</a>";
	}

	print <<END;
	<tr>
		<td style='text-align:center'>$l4proto</td>
		<td style='text-align:center; background-color:$sip_colour;'>
			<a href='/cgi-bin/ipinfo.cgi?ip=$sip'>
				<span style='color:#FFFFFF;'>$sip</span>
			</a>
			$sip_extra
		</td>
		<td style='text-align:center; background-color:$sip_colour;'>
			<a href='https://isc.sans.edu/port.html?port=$sport' target='top' title='$sserv'>
				<span style='color:#FFFFFF;'>$sport</span>
			</a>
			$sport_extra
		</td>
		<td style='text-align:center; background-color:$sip_colour;'>
			<a href='country.cgi#$srcccode'><img src='$src_flag_icon' border='0' align='absmiddle' alt='$srcccode' title='$srcccode' /></a>
		</td>
		<td style='text-align:center; background-color:$dip_colour;'>
			<a href='/cgi-bin/ipinfo.cgi?ip=$dip'>
				<span style='color:#FFFFFF;'>$dip</span>
			</a>
			$dip_extra
		</td>
		<td style='text-align:center; background-color:$dip_colour;'>
			<a href='https://isc.sans.edu/port.html?port=$dport' target='top' title='$dserv'>
				<span style='color:#FFFFFF;'>$dport</span>
			</a>
			$dport_extra
		</td>
		<td style='text-align:center; background-color:$dip_colour;'>
			<a href='country.cgi#$dstccode'><img src='$dst_flag_icon' border='0' align='absmiddle' alt='$dstccode' title='$dstccode' /></a>
		</td>
		<td class="text-right">
			&gt; $bytes_in
		</td>
		<td class="text-right">
			&lt; $bytes_out
		</td>
		<td style='text-align:center'>$state</td>
		<td style='text-align:center'>$ttl</td>
	</tr>
END
}

close(CONNTRACK);

# Close the main table.
print "</table>";

&Header::closesection();
&Header::closebigbox();
&Header::closepage();

sub ipcolour($) {
	my $address = shift;

	# Sort all networks so we find the best match
	my @networks = reverse sort {
		&Network::get_prefix($a) <=> &Network::get_prefix($b)
	} keys %networks;

	foreach my $network (@networks) {
		if (defined $network) {
			if (&Network::check_subnet($network)) {
				if (&Network::ip_address_in_network($address, $network)) {
					return $networks{$network};
				}
			}
		}
	}

	# If we don't know the network, the address must be from the RED network
	return ${Header::colourred};
}

1;
