Files
webmin/itsecur-firewall/itsecur-lib.pl
2023-09-16 09:29:42 -07:00

1509 lines
34 KiB
Perl

# itsecure-lib.pl
# Version
# ITsecur
# Common functions for all firewall types
# XXX only backup firewall module users?
BEGIN { push(@INC, ".."); };
use WebminCore;
&init_config();
do "$config{'type'}-lib.pl";
@opts = ( 'rules', 'services', 'groups', 'nat','nat2', 'pat', 'spoof', 'syn', 'logs',
'authlogs', 'report',
'users',
&supports_time() ? ('times') : (),
'backup', 'restore',
'remote', 'import' );
# Take out to test
# &supports_bandwidth() ? ('bandwidth') : (),
@backup_opts = grep { $_ ne 'logs' && $_ ne 'backup' && $_ ne 'restore' }
(@opts, 'ipsec', 'searches', 'config');
$groups_file = "$module_config_directory/groups";
$standard_services_file = "$module_root_directory/standard-services";
$services_file = "$module_config_directory/services";
$rules_file = "$module_config_directory/rules";
$nat_file = "$module_config_directory/nat";
$nat2_file = "$module_config_directory/nat2";
$pat_file = "$module_config_directory/pat";
$spoof_file = "$module_config_directory/spoof";
$syn_file = "$module_config_directory/syn";
$times_file = "$module_config_directory/times";
$active_interfaces = "$module_config_directory/active";
$prerules = "$module_config_directory/prerules";
$postrules = "$module_config_directory/postrules";
$prenat = "$module_config_directory/prenat";
$postnat = "$module_config_directory/postnat";
$debug_file = "$module_config_directory/debug";
$searches_dir = "$module_config_directory/searches";
@config_files = ( $groups_file, $services_file,
$rules_file, $nat_file, $nat2_file, $pat_file, $spoof_file,
$syn_file, $times_file );
%access = &get_module_acl();
if (defined($access{'edit'})) {
if ($access{'edit'}) {
@edit_access = @read_access = split(/\s+/, $access{'features'});
}
else {
@read_access = split(/\s+/, $access{'features'});
}
}
else {
@edit_access = split(/\s+/, $access{'features'});
@read_access = split(/\s+/, $access{'rfeatures'});
}
%edit_access = map { $_, 1 } @edit_access;
%read_access = map { $_, 1 } @read_access;
$cron_cmd = "$module_config_directory/backup.pl";
# list_groups([file])
# Returns a list of groups. Each has a name and zero or more member hosts,
# IP addresses, networks or other groups.
sub list_groups
{
local @rv;
open(GROUPS, $_[0] || $groups_file);
while(<GROUPS>) {
s/\r|\n//g;
if (/^(\S+)\t+(.*)$/) {
local $group = { 'name' => $1,
'members' => [ split(/\t+/, $2) ],
'index' => scalar(@rv) };
push(@rv, $group);
}
}
close(GROUPS);
return @rv;
}
# save_groups(group, ...)
# Updates the groups list
sub save_groups
{
local $g;
local @SortGroups=();
foreach $g (@_) {
push(@SortGroups,$g->{'name'}."\t".join("\t", @{$g->{'members'}})."\n");
}
open(GROUPS, ">$groups_file");
print GROUPS sort { lc($a) cmp lc($b) } @SortGroups;
close(GROUPS);
&automatic_backup();
}
# list_services([file])
# Returns a list of services, each of which has a name and multiple
# protocols and port
sub list_services
{
local ($sf, @rv);
#if (!-r $standard_services_file) {
# system("cp $module_root_directory/standard-services $standard_services_file");
# }
foreach $sf ($_[0] || $services_file, $standard_services_file) {
local @frv;
open(SERVS, $sf);
while(<SERVS>) {
s/\r|\n//g;
s/#.*$//;
s/\s+$//;
if (/^(\S+)\t+(.*)$/) {
local $serv = { 'name' => $1,
'standard' =>
($sf eq $standard_services_file),
'index' => scalar(@frv) };
local @pps = split(/\s*\t+\s*/, $2);
local $i;
for($i=0; $i<@pps; $i+=2) {
if ($pps[$i] eq "other") {
push(@{$serv->{'others'}}, $pps[$i+1]);
}
else {
push(@{$serv->{'protos'}}, $pps[$i]);
push(@{$serv->{'ports'}}, $pps[$i+1]);
}
}
push(@frv, $serv);
}
}
close(SERVS);
if ($sf eq $standard_services_file) {
push(@rv, sort { lc($a->{'name'}) cmp lc($b->{'name'}) } @frv);
}
else {
push(@rv, @frv);
}
}
return @rv;
}
# combine_services(comma-list, &services-hash)
# Returns lists of protocols and port numbers taken from a comma-separated list
# of service names
sub combine_services
{
local (@protos, @ports);
foreach $sn (split(/,/, $_[0])) {
local $serv = $_[1]->{$sn};
push(@protos, @{$serv->{'protos'}});
push(@ports, @{$serv->{'ports'}});
local ($cprotos, $cports) = &combine_services(join(",", @{$serv->{'others'}}), $_[1]);
push(@protos, @$cprotos);
push(@ports, @$cports);
}
return (\@protos, \@ports);
}
# save_services(service, ...)
sub save_services
{
#open(SERVS, ">$services_file");
local @SortGroups;
local $data;
foreach $serv (@_) {
next if ($serv->{'standard'});
$data=$serv->{'name'};
local $i;
for($i=0; $i<@{$serv->{'protos'}}; $i++) {
$data = $data . "\t" . $serv->{'protos'}->[$i] . "\t" . $serv->{'ports'}->[$i];
}
for($i=0; $i<@{$serv->{'others'}}; $i++) {
if ( $serv->{'others'}->[$i] ne $serv->{'name'}) {
$data = $data . "\tother\t".$serv->{'others'}->[$i];
}
}
$data=$data . "\n";
push(@SortGroups,$data);
}
open(SERVS, ">$services_file");
print SERVS sort { lc($a) cmp lc($b) } @SortGroups;
close(SERVS);
}
# list_rules([file])
# Returns a list of rules, each of which has a source, destination, service,
# action and log-flag
sub list_rules
{
local @rv;
open(RULES, $_[0] || $rules_file);
local $rn = 1;
while(<RULES>) {
s/\r|\n//g;
if (/^(#*)([^\t]+)\t+([^\t]+)\t+(\S+)\t+(\S+)\t+(\d+)(\t+(\S+))?(\t+(\S+))?$/) {
local $rule = { 'enabled' => !$1,
'source' => $2,
'dest' => $3,
'service' => $4,
'action' => $5,
'log' => $6,
'time' => $8 || "*",
'desc' => &un_urlize($10 || "*"),
'index' => scalar(@rv),
'num' => $rn++ };
push(@rv, $rule);
}
elsif (/^(\S+)$/) {
local $sep = { 'sep' => 1,
'desc' => &un_urlize($1),
'index' => scalar(@rv) };
push(@rv, $sep);
}
}
close(RULES);
return @rv;
}
# save_rules(rule, ...)
sub save_rules
{
open(RULES, ">$rules_file");
local $rule;
foreach $rule (@_) {
if ($rule->{'sep'}) {
print RULES &urlize($rule->{'desc'}),"\n";
}
else {
print RULES ($rule->{'enabled'} ? "" : "#"),
$rule->{'source'},"\t",
$rule->{'dest'},"\t",
$rule->{'service'},"\t",
$rule->{'action'},"\t",
$rule->{'log'},"\t",
$rule->{'time'},"\t",
$rule->{'desc'} eq "*" ? "*"
: &urlize($rule->{'desc'}),"\n";
}
}
close(RULES);
}
# group_name(string, [direction])
# Given a source or destination name that may be a group, makes it nice
sub group_name
{
if ($_[0] =~ /^\@(.*)$/) {
# Host group
return "<font color=#0000ff>$1</font>";
}
elsif ($_[0] =~ /^\!\@(.*)$/) {
# Negated host group
return "<font color=#0000ff>".&text('not', "$1")."</font>";
}
elsif ($_[0] =~ /^\%(.*)$/) {
# Interface
return "<font color=#C0C0C0>".&text('iface', "$1")."</font>";
}
elsif ($_[0] =~ /^\!\%(.*)$/) {
# Negated interface
return "<font color=#C0C0C0>".&text('iface_not', "$1")."</font>";
}
elsif ($_[0] eq '*') {
# Anywhere
return $text{'anywhere'};
}
elsif ($_[0] eq '!*') {
# Nowhere
return $text{'nowhere'};
}
elsif ($_[0] =~ /^\!(.*\/.*)$/) {
# Negated network address
return &text('not', "<tt><font color=#008800>$1</font></tt>");
}
elsif ($_[0] =~ /^\!([0-9\.]+)\-([0-9]+)$/) {
# Negated address range
return &text('not', "<tt><font color=#ffff00>$1-$2</font></tt>");
}
elsif ($_[0] =~ /^\!(.*)$/) {
# Negated hostname or IP
return &text('not', "<tt>$1</tt>");
}
elsif ($_[0] =~ /^(.*\/.*)$/) {
# Network address
return "<tt><font color=#008800>$_[0]</font></tt>";
}
elsif ($_[0] =~ /^([0-9\.]+)\-([0-9]+)$/) {
# Address range
return "<tt><font color=#ffff00>$1-$2</font></tt>";
}
else {
# Hostname or IP
return "<tt>$_[0]</tt>";
}
}
# group_names(string)
sub group_names
{
return join(", ", map { &group_name($_) } split(/\s+/, $_[0]));
}
# group_names_link(dest, [from], [direction])
sub group_names_link
{
local $g;
local @rv;
foreach $g (split(/\s+/, $_[0])) {
if ($g =~ /^\@(.*)$/ || $g =~ /^\!\@(.*)$/) {
push(@rv, &ui_link("edit_group.cgi?name=$1&from=$_[1]",&group_name($g, $_[2])));
}
else {
push(@rv, &group_name($g, $_[2]));
}
}
return join(", ", @rv);
}
# group_input(name, [value], [blankoption], [multiple])
sub group_input
{
local @groups = &list_groups();
return undef if (!@groups);
local $rv = $_[3] ? "<select name=$_[0] size=5 multiple>"
: "<select name=$_[0]>\n";
if ($_[2]) {
$rv .= sprintf "<option value='' %s>%s</option>\n",
$_[1] ? "" : "selected", $_[2] == 2 ? $text{'other'} : "&nbsp;";
}
local $g;
local %vals = map { $_, 1 } split(/\s+/, $_[1]);
foreach $g (@groups) {
$rv .= sprintf "<option value=%s %s>%s</option>\n",
$g->{'name'}, $vals{$g->{'name'}} ? "selected" : "",
$g->{'name'};
}
$rv .= "</select>\n";
return $rv;
}
# service_input(name, value, [blankoption], [multiple], [norange])
sub service_input
{
local @servs = &list_services();
local %got = map { $_, 1 } split(/,/, $_[1]);
local $rv = $_[3] ? "<select name=$_[0] size=5 multiple>"
: "<select name=$_[0]>\n";
if ($_[2]) {
$rv .= sprintf "<option value='' %s>%s</option>\n",
$_[1] ? "" : "selected", $_[2] == 2 ? $text{'other'} : "&nbsp;";
}
local $s;
foreach $s (@servs) {
local $desc;
local @up = &unique(@{$s->{'protos'}});
local $i;
if (@up == 1) {
$desc = uc($up[0])." ".join(", ", @{$s->{'ports'}});
}
else {
for($i=0; $i<@{$s->{'protos'}}; $i++) {
$desc .= ", " if ($desc);
$desc .= uc($s->{'protos'}->[$i])."/".
$s->{'ports'}->[$i];
}
}
for($i=0; $i<@{$s->{'others'}}; $i++) {
$desc .= ", " if ($desc);
$desc .= $s->{'others'}->[$i];
}
$rv .= sprintf "<option value=%s %s>%s%s</option>\n",
$s->{'name'}, $got{$s->{'name'}} ? "selected" : "",
$s->{'name'}, $_[4] ? "" : " ($desc)";
}
$rv .= "</select>\n";
return $rv;
}
# action_input(name, value, [select-mode])
sub action_input
{
local $rv;
local $a;
if ($_[2]) {
$rv .= "<select name=$_[0]>\n";
foreach $a (@actions) {
$rv .= sprintf "<option value=%s %s>%s</option>\n",
$a, $_[1] eq $a ? "selected" : "",
$text{"rule_".$a};
}
$rv .= "</select>\n";
}
else {
foreach $a (@actions) {
$rv .= sprintf "<input type=radio name=%s value=%s %s>%s\n",
$_[0], $a, $_[1] eq $a ? "checked" : "",
$text{"rule_".$a};
}
}
return $rv;
}
# protocol_input(name, value)
sub protocol_input
{
local @protos = ( 'tcp', 'udp', 'icmp', 'ip' );
#open(PROTOS, "/etc/protocols");
#while(<PROTOS>) {
# s/\r|\n//g;
# s/#.*$//;
# push(@protos, $1) if (/^(\S+)\s+(\d+)/);
# }
#close(PROTOS);
local $p;
local $rv = "<select name=$_[0]>\n";
$rv .= sprintf "<option value='' %s>&nbsp;</option>\n",
$_[1] eq '' ? "selected" : "";
foreach $p (&unique(@protos)) {
$rv .= sprintf "<option value='%s' %s>%s</option>\n",
$p, $_[1] eq $p && $p ? "selected" : "",
uc($p) || "-------";
}
$rv .= "</select>\n";
return $rv;
}
sub valid_host
{
if (&check_ipaddress($_[0])) {
return 1;
}
elsif (gethostbyname($_[0])) {
return 2;
}
elsif (&check_netaddress($_[0])) {
#$_[0] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/) {
return 3;
}
elsif ($_[0] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\-(\d+)$/) {
return 4;
}
else {
return 0;
}
}
# iface_input(name, value, [realonly], [nother], [nonemode])
sub iface_input
{
local @ifaces;
if (&foreign_check("net")) {
&foreign_require("net", "net-lib.pl");
local $i;
foreach $i (&net::active_interfaces(), &net::boot_interfaces()) {
push(@ifaces, $i->{'fullname'})
if (!$_[2] || $i->{'virtual'} eq '');
}
@ifaces = &unique(@ifaces);
}
if (@ifaces) {
local $rv = "<select name=$_[0]>\n";
local ($i, $found);
if ($_[4]) {
$rv .= sprintf "<option value='' %s>%s</option>\n",
$_[1] eq "" ? "selected" : "", "&lt;None&gt;";
$found++ if ($_[1] eq "");
}
foreach $i (@ifaces) {
$rv .= sprintf "<option value=%s %s>%s</option>\n",
$i, $_[1] eq $i ? "selected" : "", $i;
$found++ if ($_[1] eq $i);
}
if ($_[3]) {
$rv .= "<option value=$_[1] selected>$_[1]</option>\n" if (!$found && $_[1]);
}
else {
$rv .= sprintf "<option value='' %s>%s</option>\n",
!$found && $_[1] ? "selected" : "", $text{'rule_oifc'};
$rv .= "</select>\n";
$rv .= sprintf "<input name=$_[0]_other size=6 value='%s'>\n",
!$found ? $_[1] : "";
}
return $rv;
}
else {
return "<input name=$_[0] size=6 value='$_[1]'>";
}
}
# time_input(name, [value])
sub time_input
{
local @times = &list_times();
return undef if (!@times);
local $rv = "<select name=$_[0]>\n";
local $t;
foreach $t (@times) {
$rv .= sprintf "<option value=%s %s>%s</option>\n",
$t->{'name'}, $t->{'name'} eq $_[1] ? "selected" : "",
$t->{'name'};
}
$rv .= "</select>\n";
return $rv;
}
# get_nat([file])
sub get_nat
{
local ($iface, @nets, @maps);
open(NAT, $_[0] || $nat_file) || return ( );
chop($iface = <NAT>);
while(<NAT>) {
s/\r|\n//g;
if (/^(\S+)$/) {
push(@nets, $_);
}
elsif (/^(\S+)\t+(\S+)\t+(\S+)$/) {
push(@maps, [ $1, $2, $3 ]);
}
elsif (/^(\S+)\t+(\S+)$/) {
push(@maps, [ $1, $2 ]);
}
}
close(NAT);
return ( $iface, @nets, @maps );
}
# save_nat(iface, net, ..)
sub save_nat
{
open(NAT, ">$nat_file");
print NAT shift(@_),"\n";
local $n;
foreach $n (@_) {
if (ref($n)) {
print NAT join("\t", @$n),"\n";
}
else {
print NAT $n,"\n";
}
}
close(NAT);
}
sub save_nat2
{
open(NAT, ">$nat2_file");
print NAT shift(@_),"\n";
local $n;
foreach $n (@_) {
if (ref($n)) {
print NAT join("\t", @$n),"\n";
}
else {
print NAT $n,"\n";
}
}
close(NAT);
}
# get_pat([file])
sub get_pat
{
local ($defiface, @forwards);
open(PAT, $_[0] || $pat_file) || return ( );
chop($defiface = <PAT>);
while(<PAT>) {
s/\r|\n//g;
if (/^(\S+)\t+(\S+)\t+(\S+)$/) {
push(@forwards, { 'service' => $1,
'host' => $2,
'iface' => $3 });
}
elsif (/^(\S+)\t+(\S+)$/) {
push(@forwards, { 'service' => $1,
'host' => $2,
'iface' => $defiface });
}
}
close(PAT);
return @forwards;
}
# save_pat(forward, ...)
sub save_pat
{
open(PAT, ">$pat_file");
print PAT (@_ ? $_[0]->{'iface'} : ""),"\n";
local $f;
foreach $f (@_) {
if ($f->{'iface'}) {
print PAT "$f->{'service'}\t$f->{'host'}\t$f->{'iface'}\n";
}
else {
print PAT "$f->{'service'}\t$f->{'host'}\n";
}
}
close(PAT);
}
# get_spoof([file])
sub get_spoof
{
local ($iface, @nets);
open(PAT, $_[0] || $spoof_file) || return ( );
chop($iface = <PAT>);
while(<PAT>) {
s/\r|\n//g;
if (/^(\S+)$/) {
push(@nets, $_);
}
}
close(PAT);
return ( $iface, @nets );
}
# save_spoof(iface, net, ...)
sub save_spoof
{
open(PAT, ">$spoof_file");
print PAT shift(@_),"\n";
local $s;
foreach $s (@_) {
print PAT "$s\n";
}
close(PAT);
}
# get_syn([file])
sub get_syn
{
local ($flood, $spoof, $fin);
open(SYN, $_[0] || $syn_file) || return ( );
chop($flood = <SYN>);
chop($spoof = <SYN>);
chop($fin = <SYN>);
close(SYN);
return ($flood, $spoof, $fin);
}
# save_syn(flood, spoof, fin)
sub save_syn
{
open(SYN, ">$syn_file");
print SYN int($_[0]),"\n";
print SYN int($_[1]),"\n";
print SYN int($_[2]),"\n";
close(SYN);
}
# list_times([file])
# Returns a list of all time ranges
sub list_times
{
local @rv;
open(TIMES, $_[0] || $times_file);
while(<TIMES>) {
s/\r|\n//g;
local @t = split(/\t/, $_);
if (@t >= 3) {
local $time = { 'index' => scalar(@rv),
'name' => $t[0],
'hours' => $t[1],
'days' => $t[2] };
push(@rv, $time);
}
}
close(TIMES);
return @rv;
}
# save_times(time, ...)
# Updates the time ranges list
sub save_times
{
open(TIMES, ">$times_file");
local $t;
foreach $t (@_) {
print TIMES $t->{'name'},"\t",
$t->{'hours'},"\t",
$t->{'days'},"\n";
}
close(TIMES);
}
# activate_interface(base, ip)
sub activate_interface
{
&foreign_require("net", "net-lib.pl");
local @active = &net::active_interfaces();
local ($base) = grep { $_->{'fullname'} eq $_[0] } @active;
local ($already) = grep { $_->{'address'} eq $_[1] } @active;
if ($base && !$already) {
# Work out an interface number
local $vmax = 0;
foreach $a (@active) {
$vmax = $a->{'virtual'}
if ($a->{'name'} eq $base->{'name'} &&
$a->{'virtual'} > $vmax);
}
# Activate now
$virt = { 'address' => $_[1],
'netmask' => $base->{'netmask'},
'broadcast' => $base->{'broadcast'},
'name' => $base->{'name'},
'virtual' => $vmax+1,
'up' => 1 };
$virt->{'fullname'} = $virt->{'name'}.":".$virt->{'virtual'};
&net::activate_interface($virt);
# Save for later
open(ACTIVE, ">>$active_interfaces");
print ACTIVE "$virt->{'fullname'}\t$virt->{'address'}\n";
close(ACTIVE);
}
}
# deactivate_all_interfaces()
# Shuts down all interfaces activated by the above function
sub deactivate_all_interfaces
{
&foreign_require("net", "net-lib.pl");
open(ACTIVE, $active_interfaces);
while(<ACTIVE>) {
if (/^(\S+)\s+(\S+)/) {
local $addr = $2;
local @active = &net::active_interfaces();
local ($virt) = grep { $_->{'address'} eq $addr } @active;
if ($virt && $virt->{'virtual'} ne '') {
&net::deactivate_interface($virt);
}
}
}
close(ACTIVE);
unlink($active_interfaces);
}
sub apply_button
{
if (&can_edit("apply")) {
return &ui_link("apply.cgi?return=1",$text{'apply_button'});
}
else {
return undef;
}
}
# expand_hosts(names, &groups)
# Give a list of host or group names, expands them to hosts
sub expand_hosts
{
local ($e, @rv);
local %groups = map { $_->{'name'}, $_ } @{$_[1]};
foreach $e (split(/\s+/, $_[0])) {
if ($e =~ /^\@(.*)$/) {
# Expand to all group members
local $group = $groups{$1};
foreach $m (@{$group->{'members'}}) {
push(@rv, &expand_hosts($m, $_[1]));
}
}
elsif ($e =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\-(\d+)$/) {
# Expand to all IPs in range
push(@rv, map { "$1.$2.$3.$_" } ($4 .. $5) );
}
else {
# Just a single IP, host or network
push(@rv, $e);
}
}
return @rv;
}
# can_use(feature)
sub can_use
{
return 1 if ($read_access{'*'} || $edit_access{'*'});
return $read_access{$_[0]} || $edit_access{$_[0]};
}
# can_edit(feature)
sub can_edit
{
return 0 if (!&can_use($_[0]));
return $edit_access{'*'} || $edit_access{$_[0]};
}
# can_use_error(feature)
sub can_use_error
{
&can_use($_[0]) || &error($text{$_[0]."_ecannot"} ||
&text('ecannot', $text{$_[0]."_title"}));
}
# can_edit_error(feature)
sub can_edit_error
{
&can_edit($_[0]) || &error($text{$_[0]."_ecannot"} ||
&text('ecannot', $text{$_[0]."_title"}));
}
# can_edit_disable(feature)
sub can_edit_disable
{
if (!&can_edit($_[0])) {
print "<script>\n";
print "l = document.forms[0].elements;\n";
print "for(i=0; i<l.length; i++) {\n";
print " if (l[i].name != \"burn\" && l[i].name != \"test\" &&\n";
print " l[i].type != \"hidden\") {\n";
print " l[i].disabled = true;\n";
print " }\n";
print "}\n";
print "</script>\n";
}
}
# protocol_name(proto, port)
sub protocol_name
{
local $pr = uc($_[0]);
local $pt = $_[1];
if ($pr eq "TCP") {
return "<font color=#0000ff>$pr/$pt</font>";
}
elsif ($pr eq "UDP") {
return "<font color=#008800>$pr/$pt</font>";
}
elsif ($pr eq "ICMP") {
return "<font color=#f00000>$pr/$pt</font>";
}
else {
return "$pr/$pt";
}
}
# protocol_names(comma-list, [&services])
sub protocol_names
{
if ($_[0] eq "*") {
return $text{'rule_any'};
}
else {
local %sn = map { $_->{'name'}, $_ }
( $_[1] ? @{$_[1]} : &list_services() );
local $sn;
local @rv;
local ($editServO,$editServC);
foreach $sn (split(/,/, $_[0])) {
local $serv = $sn{$sn};
if (!$serv->{'standard'}){
$editServO = &ui_link("edit_service.cgi?name=".$serv->{'name'}, $editServC);
} else {
$editServO="";
$editServC="";
}
local $pr = @{$serv->{'protos'}} == 1 ? uc($serv->{'protos'}->[0]) : undef;
if ($pr eq "TCP") {
push(@rv, "$editServO<font color=#0000ff>$sn</font>$editServC");
}
elsif ($pr eq "UDP") {
push(@rv, "$editServO<font color=#008800>$sn</font>$editServC");
}
elsif ($pr eq "ICMP") {
push(@rv, "$editServO<font color=#f00000>$sn</font>$editServC");
}
else {
push(@rv, "$editServO $sn $editServC");
}
}
return join(", ", @rv);
}
}
# my_address_in(address/network)
# Returns this system's IP address in some network
sub my_address_in
{
local $net = $_[0];
$net =~ s/^\!\s+//;
if ($net =~ /[a-z]/i) {
$net = &to_ipaddress($net);
}
$net =~ s/^(\d+\.\d+\.\d+).*$/$1/;
&foreign_require("net", "net-lib.pl");
local @ifaces = &net::active_interfaces();
local $i;
local $primary;
foreach $i (@ifaces) {
if ($i->{'up'}) {
if (!$primary && $i->{'fullname'} !~ /^lo/) {
$primary = $i->{'address'};
}
local $addr = $i->{'address'};
$addr =~ s/^(\d+\.\d+\.\d+).*$/$1/;
if ($addr eq $net) {
return $i->{'address'};
}
}
}
return $primary;
}
sub has_ipsec
{
return &foreign_installed("ipsec", 1);
}
# backup_firewall(&what, file, [password])
# Backs up the firewall to some file
sub backup_firewall
{
local ($mode, @dest) = &parse_backup_dest($_[1]);
local $file = $mode == 1 ? $dest[0] : &tempname();
local $ipsec_tmp = "$module_config_directory/ipsec.conf";
local $secrets_tmp = "$module_config_directory/ipsec.secrets";
local $users_tmp = "$module_config_directory/miniserv.users";
local $acl_tmp = "$module_config_directory/webmin.acl";
local $w;
local (@files, @delfiles);
foreach $w (@{$_[0]}) {
if ($w eq "ipsec") {
# Copy the ipsec.conf files
if (&has_ipsec()) {
system("cp $ipsec::config{'file'} $ipsec_tmp");
system("cp $ipsec::config{'secrets'} $secrets_tmp");
push(@delfiles, "ipsec.conf", "ipsec.secrets");
}
}
elsif ($w eq "users") {
# Copy all Webmin users
opendir(DIR, $module_config_directory);
push(@files, grep { /\.acl$/ } readdir(DIR));
closedir(DIR);
system("cp $config_directory/miniserv.users $users_tmp");
system("cp $config_directory/webmin.acl $acl_tmp");
push(@delfiles, "miniserv.users", "webmin.acl");
}
else {
push(@files, $w) if (-r "$module_config_directory/$w");
}
}
push(@files, @delfiles);
local $what = join(" ", @files);
return $text{'backup_ewhat2'} if (!$what);
local $pass = $_[2] ? "-P '$_[2]'" : "";
local $out = &backquote_logged("(cd $module_config_directory ; zip -r $pass '$file' $what) 2>&1");
return "<pre>$out</pre>" if ($?);
unlink(map { "$module_config_directory/$_" } @delfiles);
# Send to destination
if ($mode == 2) {
# FTP somewhere
local $err;
&ftp_upload($dest[2], $dest[3], $file, \$err, undef, $dest[0], $dest[1]);
unlink($file);
return $err if ($err);
}
elsif ($mode == 3) {
# Email somewhere
$data = `cat $file`;
unlink($file);
$host = &get_system_hostname();
$body = "The backup of the firewall configuration on $host is attached to this email.\n";
local $mail = { 'headers' =>
[ [ 'From', $config{'from'} || "webmin\@$host" ],
[ 'To', $dest[0] ],
[ 'Subject', "Firewall backup" ] ],
'attach' =>
[ { 'headers' => [ [ 'Content-type', 'text/plain' ] ],
'data' => $body },
{ 'headers' => [ [ 'Content-type', 'application/zip' ],
[ 'Content-Transfer-Encoding', 'base64' ] ],
'data' => $data } ] };
$main::error_must_die = 1;
if (&foreign_check("mailboxes")) {
&foreign_require("mailboxes", "mailboxes-lib.pl");
eval { &mailboxes::send_mail($mail); };
}
else {
&foreign_require("sendmail", "sendmail-lib.pl");
&foreign_require("sendmail", "boxes-lib.pl");
eval { &sendmail::send_mail($mail); };
}
return $@ if ($@);
}
return undef;
}
sub check_zip
{
&has_command("zip") && &has_command("unzip") ||
&error($text{'backup_ezipcmd'});
}
sub find_backup_job
{
&foreign_require("cron", "cron-lib.pl");
local @jobs = &cron::list_cron_jobs();
local ($job) = grep { $_->{'user'} eq 'root' &&
$_->{'command'} eq $cron_cmd } @jobs;
return $job;
}
sub parse_backup_dest
{
if ($_[0] =~ /^mailto:(.*)/) {
return (3, $1);
}
elsif ($_[0] =~ /^ftp:\/\/([^:]*):([^@]*)@([^\/]+)(\/.*)$/) {
return (2, $1, $2, $3, $4);
}
elsif ($_[0] =~ /^\//) {
return (1, $_[0]);
}
else {
return (0);
}
}
# ftp_upload(host, file, srcfile, [&error], [&callback], [user, pass])
# Download data from a local file to an FTP site
sub ftp_upload
{
local($buf, @n);
local $cbfunc = $_[4];
$download_timed_out = undef;
local $SIG{ALRM} = "download_timeout";
alarm(60);
# connect to host and login
&open_socket($_[0], 21, "SOCK", $_[3]) || return 0;
alarm(0);
if ($download_timed_out) {
if ($_[3]) { ${$_[3]} = $download_timed_out; return 0; }
else { &error($download_timed_out); }
}
&ftp_command("", 2, $_[3]) || return 0;
if ($_[5]) {
# Login as supplied user
local @urv = &ftp_command("USER $_[5]", [ 2, 3 ], $_[3]);
@urv || return 0;
if (int($urv[1]/100) == 3) {
&ftp_command("PASS $_[6]", 2, $_[3]) || return 0;
}
}
else {
# Login as anonymous
local @urv = &ftp_command("USER anonymous", [ 2, 3 ], $_[3]);
@urv || return 0;
if (int($urv[1]/100) == 3) {
&ftp_command("PASS root\@".&get_system_hostname(), 2,
$_[3]) || return 0;
}
}
&$cbfunc(1, 0) if ($cbfunc);
&ftp_command("TYPE I", 2, $_[3]) || return 0;
# get the file size and tell the callback
local @st = stat($_[2]);
if ($cbfunc) {
&$cbfunc(2, $st[7]);
}
# send the file
local $pasv = &ftp_command("PASV", 2, $_[3]);
defined($pasv) || return 0;
$pasv =~ /\(([0-9,]+)\)/;
@n = split(/,/ , $1);
&open_socket("$n[0].$n[1].$n[2].$n[3]", $n[4]*256 + $n[5], "CON", $_[3]) || return 0;
&ftp_command("STOR $_[1]", 1, $_[3]) || return 0;
# transfer data
local $got;
open(PFILE, $_[2]);
while(read(PFILE, $buf, 1024) > 0) {
print CON $buf;
$got += length($buf);
&$cbfunc(3, $got) if ($cbfunc);
}
close(PFILE);
close(CON);
if ($got != $st[7]) {
if ($_[3]) { ${$_[3]} = "Upload incomplete"; return 0; }
else { &error("Upload incomplete"); }
}
&$cbfunc(4) if ($cbfunc);
# finish off..
&ftp_command("", 2, $_[3]) || return 0;
&ftp_command("QUIT", 2, $_[3]) || return 0;
close(SOCK);
return 1;
}
# lock_itsecur_files()
# Lock all firewall config files
sub lock_itsecur_files
{
local $f;
foreach $f (@config_files) {
&lock_file($f);
}
}
# unlock_itsecur_files()
# Unlock all firewall config files
sub unlock_itsecur_files
{
local $f;
foreach $f (@config_files) {
&unlock_file($f);
}
}
sub remote_webmin_log
{
if ($config{'remote_log'} && !fork()) {
# Disconnect from TTY
untie(*STDIN);
untie(*STDOUT);
untie(*STDERR);
close(STDIN);
close(STDOUT);
close(STDERR);
# Send log to remote host
&remote_foreign_require($config{'remote_log'}, $module_name,
"itsecur-lib.pl");
local $d;
foreach $d (@main::locked_file_diff) {
&remote_foreign_call($config{'remote_log'}, $module_name,
"additional_log", $d->{'type'},
$d->{'object'}, $d->{'data'});
}
local $script_name = $0 =~ /([^\/]+)$/ ? $1 : '';
&remote_foreign_call($config{'remote_log'}, $module_name,
"webmin_log", @_[0..3], $module_name,
&get_system_hostname(),
$script_name,
$ENV{'REMOTE_HOST'});
exit(0);
}
&webmin_log(@_);
}
# automatic_backup()
# If a change has been made and an automatic backup directory set, save the
# module's configuration
sub automatic_backup
{
return if (!$config{'auto_dir'} || !-d $config{'auto_dir'});
# Backup to a temp file
local $temp = &tempname();
local $err = &backup_firewall(\@backup_opts, $temp, undef);
if ($err) {
unlink($temp);
return 0;
}
# Make sure this backup is actually different from the last
local $linkfile = "$config{'auto_dir'}/latest.zip";
if (-r $linkfile) {
local $out = `diff '$config{'auto_dir'}/latest.zip' '$temp' 2>&1`;
if ($? == 0) {
# No change!
unlink($temp);
return 0;
}
}
# Copy to directory, and update latest link
use POSIX;
local $newfile = strftime "$config{'auto_dir'}/firewall.%Y-%m-%d-%H:%M:%S.zip",
localtime(time());
system("mv '$temp' '$newfile'");
unlink($linkfile);
symlink($newfile, $linkfile);
return 1;
}
# parse_all_logs([base-only])
# Returns a list of all log structures, newest first
sub parse_all_logs
{
local $baselog = $config{'log'} || &get_log_file();
local @rv;
foreach $log ($config{'all_files'} && !$_[0] ? &all_log_files($baselog)
: ($baselog)) {
if ($log =~ /\.gz$/i) {
open(LOG, "gunzip -c ".quotemeta($log)." |");
}
elsif ($log =~ /\.Z$/i) {
open(LOG, "uncompress -c ".quotemeta($log)." |");
}
else {
open(LOG, $log);
}
while(<LOG>) {
local $info = &parse_log_line($_);
push(@rv, $info) if ($info);
}
close(LOG);
}
return reverse(@rv);
}
# all_log_files(file)
sub all_log_files
{
$_[0] =~ /^(.*)\/([^\/]+)$/;
local $dir = $1;
local $base = $2;
local ($f, @rv);
opendir(DIR, $dir);
foreach $f (readdir(DIR)) {
if ($f =~ /^\Q$base\E/ && -f "$dir/$f") {
push(@rv, "$dir/$f");
}
}
closedir(DIR);
return @rv;
}
@search_fields = ("src", "dst", "dst_iface", "proto", "src_port", "dst_port",
"first", "last", "action", "rule");
# filter_logs(&logs, &in, [&searchvars])
sub filter_logs
{
local @logs = @{$_[0]};
local %in = %{$_[1]};
local $f;
local @servs = &list_services();
local @groups = &list_groups();
local %servs = map { $_->{'name'}, $_ } @servs;
foreach $f (@search_fields) {
if ($in{$f."_mode"}) {
# This search applies .. find all suitable match types
local %matches;
local $tm;
if (($f eq "src_port" || $f eq "dst_port") && $in{$f."_what"}) {
# Lookup all ports and protocols
local ($protos, $ports) =
&combine_services($in{$f."_what"}, \%servs);
local $i;
for($i=0; $i<@$protos; $i++) {
if ($ports->[$i] =~ /^(\d+)\-(\d+)$/) {
local $p;
foreach $p ($1 .. $2) {
$matches{lc($protos->[$i]),$p}++;
}
}
else {
$matches{lc($protos->[$i]),$ports->[$i]}++;
}
}
}
elsif (($f eq "src_port" || $f eq "dst_port") && !$in{$f."_what"}) {
# One specified port number
$matches{$in{$f."_other"}}++;
}
elsif (($f eq "src" || $f eq "dst") && $in{$f."_what"}) {
# Lookup all hosts
local @hosts = &expand_hosts(
'@'.$in{$f."_what"}, \@groups);
local $h;
foreach $h (@hosts) {
local $eh;
foreach $eh (&expand_net($h)) {
$matches{$eh}++;
}
}
}
elsif (($f eq "src" || $f eq "dst") && !$in{$f."_what"}) {
# One other host
local $eh;
foreach $eh (&expand_net($in{$f."_other"})) {
$matches{$eh}++;
}
}
elsif ($f eq "first" || $f eq "last") {
# A time range
eval { $tm = timelocal(
0, $in{$f."_min"}, $in{$f."_hour"},
$in{$f."_day"}, $in{$f."_month"}-1,
$in{$f."_year"}-1900); };
}
else {
$matches{lc($in{$f."_what"})}++;
}
if ($f eq "first" && $tm) {
# Find those after start minute
@logs = grep { $_->{'time'} >= $tm } @logs;
}
elsif ($f eq "last" && $tm) {
# Find those before end minute
@logs = grep { $_->{'time'} < $tm+60 } @logs;
}
elsif ($in{$f."_mode"} == 1) {
# Find matching entries
@logs = grep {
$matches{lc($_->{$f})} ||
$matches{lc($_->{'proto'}),lc($_->{$f})} }
@logs;
}
elsif ($in{$f."_mode"} == 2) {
# Find non-matching entries
@logs = grep {
!($matches{lc($_->{$f})} ||
$matches{lc($_->{'proto'}),lc($_->{$f})}) }
@logs;
}
if ($_[2]) {
local $e;
foreach $e ("mode", "what", "other", "day",
"month", "year") {
if ($in{$f."_".$e} ne "") {
push(@{$_[2]}, $f."_".$e."=".
&urlize($in{$f."_".$e}));
}
}
}
}
}
return @logs;
}
# expand_net(network)
# Given a network address, hostname or IP address, returns a list of all
# IP addresses it contains
sub expand_net
{
if ($_[0] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/) {
local @rv;
local $first = ($1<<24) + ($2<<16) + ($3<<8) + ($4);
local $last = $first + (1<<(32-$5)) - 1;
for($ipnum=$first; $ipnum<=$last; $ipnum++) {
local @ip = ( ($ipnum>>24)&0xff,
($ipnum>>16)&0xff,
($ipnum>>8)&0xff,
($ipnum)&0xff );
push(@rv, join(".", @ip));
}
return @rv;
}
else {
return &to_ipaddress($_[0]);
}
}
# list_searches()
# Returns a list of all saved searches
sub list_searches
{
local @rv;
opendir(DIR, $searches_dir);
local $f;
while($f = readdir(DIR)) {
if ($f ne "." && $f ne "..") {
local $search = &get_search($f);
push(@rv, $search) if ($search);
}
}
closedir(DIR);
return @rv;
}
sub get_search
{
local %search;
if (&read_file("$searches_dir/$_[0]", \%search)) {
return \%search;
}
else {
return undef;
}
}
# save_search(&search)
sub save_search
{
mkdir($searches_dir, 0755);
&write_file("$searches_dir/$_[0]->{'save_name'}", $_[0]);
}
# get_remote()
# Returns the webmin servers object used for remote logging, or undef
sub get_remote
{
return undef if (!$config{'remote_log'});
&foreign_require("servers", "servers-lib.pl");
local @servers = &servers::list_servers();
local ($server) = grep { $_->{'host'} eq $config{'remote_log'} } @servers;
return $server;
}
# save_remote(server, port, username, password, test, save)
sub save_remote
{
local ($host, $port, $user, $pass, $test, $save) = @_;
&foreign_require("servers", "servers-lib.pl");
if ($host) {
# Enabling or updating
local @servers = &servers::list_servers();
local ($newserver) = grep { $_->{'host'} eq $host } @servers;
local $server = &get_remote();
if ($newserver && $server) {
if ($newserver ne $server) {
# Re-name would cause clash, so delete it
&servers::delete_server($newserver->{'id'});
}
}
elsif ($newserver && !$server) {
# Re-naming server
$server = $newserver;
}
elsif (!$newserver && $server) {
# Can just stick to old server
}
else {
# Totally new
$server = { 'id' => time(),
'port' => $port,
'ssl' => 0,
'desc' => 'Firewall logging server',
'type' => 'unknown',
'fast' => 0 };
}
$server->{'host'} = $host;
$server->{'port'} = $port;
$server->{'user'} = $user;
$server->{'pass'} = $pass;
&servers::save_server($server);
$config{'remote_log'} = $server->{'host'};
if ($test) {
# Try a test connection
&remote_error_setup(\&test_error);
eval {
$SIG{'ALRM'} = sub { die "alarm\n" };
alarm(10);
&remote_foreign_require($server->{'host'}, "webmin",
"webmin-lib.pl");
alarm(0);
};
if ($@) {
&error(&text('remote_econnect', $text{'remote_etimeout'}));
}
elsif ($test_error_msg) {
&error(&text('remote_econnect', $test_error_msg));
}
}
}
else {
# Disabling
delete($config{'remote_log'});
}
if ($save) {
&lock_file($module_config_file);
&write_file($module_config_file, \%config);
&unlock_file($module_config_file);
}
}
sub test_error
{
$test_error_msg = join("", @_);
}
sub check_netaddress
{
return $_[0] =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)\/(\d+)$/ &&
$1 >= 0 && $1 <= 255 &&
$2 >= 0 && $2 <= 255 &&
$3 >= 0 && $3 <= 255 &&
$4 >= 0 && $4 <= 255 &&
$5 >= 0 && $5 <= 32;
}
sub is_one_host
{
local @groups = &list_groups();
local @rv=&expand_hosts($_[0], \@groups);
return $#rv;
}
1;