# 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() { 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() { 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() { 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 "$1"; } elsif ($_[0] =~ /^\!\@(.*)$/) { # Negated host group return "".&text('not', "$1").""; } elsif ($_[0] =~ /^\%(.*)$/) { # Interface return "".&text('iface', "$1").""; } elsif ($_[0] =~ /^\!\%(.*)$/) { # Negated interface return "".&text('iface_not', "$1").""; } elsif ($_[0] eq '*') { # Anywhere return $text{'anywhere'}; } elsif ($_[0] eq '!*') { # Nowhere return $text{'nowhere'}; } elsif ($_[0] =~ /^\!(.*\/.*)$/) { # Negated network address return &text('not', "$1"); } elsif ($_[0] =~ /^\!([0-9\.]+)\-([0-9]+)$/) { # Negated address range return &text('not', "$1-$2"); } elsif ($_[0] =~ /^\!(.*)$/) { # Negated hostname or IP return &text('not', "$1"); } elsif ($_[0] =~ /^(.*\/.*)$/) { # Network address return "$_[0]"; } elsif ($_[0] =~ /^([0-9\.]+)\-([0-9]+)$/) { # Address range return "$1-$2"; } else { # Hostname or IP return "$_[0]"; } } # 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] ? "\n"; if ($_[2]) { $rv .= sprintf "\n", $_[1] ? "" : "selected", $_[2] == 2 ? $text{'other'} : " "; } local $g; local %vals = map { $_, 1 } split(/\s+/, $_[1]); foreach $g (@groups) { $rv .= sprintf "\n", $g->{'name'}, $vals{$g->{'name'}} ? "selected" : "", $g->{'name'}; } $rv .= "\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] ? "\n"; if ($_[2]) { $rv .= sprintf "\n", $_[1] ? "" : "selected", $_[2] == 2 ? $text{'other'} : " "; } 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 "\n", $s->{'name'}, $got{$s->{'name'}} ? "selected" : "", $s->{'name'}, $_[4] ? "" : " ($desc)"; } $rv .= "\n"; return $rv; } # action_input(name, value, [select-mode]) sub action_input { local $rv; local $a; if ($_[2]) { $rv .= "\n"; } else { foreach $a (@actions) { $rv .= sprintf "%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() { # s/\r|\n//g; # s/#.*$//; # push(@protos, $1) if (/^(\S+)\s+(\d+)/); # } #close(PROTOS); local $p; local $rv = "\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 = "\n"; $rv .= sprintf "\n", !$found ? $_[1] : ""; } return $rv; } else { return ""; } } # time_input(name, [value]) sub time_input { local @times = &list_times(); return undef if (!@times); local $rv = "\n"; return $rv; } # get_nat([file]) sub get_nat { local ($iface, @nets, @maps); open(NAT, $_[0] || $nat_file) || return ( ); chop($iface = ); while() { 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 = ); while() { 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 = ); while() { 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 = ); chop($spoof = ); chop($fin = ); 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() { 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() { 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 "\n"; } } # protocol_name(proto, port) sub protocol_name { local $pr = uc($_[0]); local $pt = $_[1]; if ($pr eq "TCP") { return "$pr/$pt"; } elsif ($pr eq "UDP") { return "$pr/$pt"; } elsif ($pr eq "ICMP") { return "$pr/$pt"; } 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$sn$editServC"); } elsif ($pr eq "UDP") { push(@rv, "$editServO$sn$editServC"); } elsif ($pr eq "ICMP") { push(@rv, "$editServO$sn$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 "
$out
" 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() { 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;