1# firewall-lib.pl 2# Unified functions for firewall4-lib and firewall6-lib 3# has to be included from every perl and cgi script 4# cgi scripts has also to include firewall4/6-lib based on result of get_ipvx_version() 5 6BEGIN { push(@INC, ".."); }; 7use WebminCore; 8&init_config(); 9 10$config{'perpage'} ||= 50; # a value of 0 can cause problems 11 12# provide default values if only firewall-lib is included, e.g. foreign_require(firewall, firewall-lib.pl) calls 13set_ipvx_version(get_ipvx_version()); 14 15# set_ipvx_version(version) 16# version can be ipv6 or ipv4, 17sub set_ipvx_version 18{ 19$ipvx_save=$iptables_save_file; 20$ipvx_lib='firewall4-lib.pl'; 21$ipv4_link='../firewall/'; 22$ipv6_link='../firewall6/'; 23$ipvx_icmp=""; 24$ipvx_arg="inet4"; 25 26if ($_[0] =~ /6$/i) { 27 $ipvx='6'; 28 $ipvx_save=$ip6tables_save_file; 29 $ipvx_lib='firewall6-lib.pl'; 30 $ipvx_icmp="v6"; 31 $ipvx_arg="inet6"; 32 } 33} 34 35# get_ipvx_version 36# get iptables version used from environment 37# if script runs in firewall6 or version=inet6, 6 is returned, else 4 38sub get_ipvx_version 39{ 40return $in{'version'} =~ /6$/ || $module_name =~ /6$/ ? 6 : 4; 41} 42 43 44# get_iptables_save([file|"direct"]) 45# Parse the iptables save file into a list of tables 46# format seems to be: 47# *table 48# :chain defaultpolicy 49# -A chain options 50# -N chain 51# COMMIT 52sub get_iptables_save 53{ 54local ($file) = @_; 55local (@rv, $table, %got); 56local $lnum = 0; 57 58local $direct = "ip${ipvx}tables-save 2>/dev/null |"; 59if (!$file) { 60 # Use default file 61 $file = $config{"direct${ipvx}"} ? $direct : "<".$ipvx_save; 62 } 63elsif ($file eq "direct") { 64 # Read active rules 65 $file = $direct; 66 } 67open(FILE, $file); 68local $cmt; 69LINE: 70while(<FILE>) { 71 local $read_comment; 72 s/\r|\n//g; 73 # regex to filter out chains not managed by firewall, i.e. fail2ban 74 if ($config{"direct${ipvx}"} && $config{'filter_chain'}) { 75 foreach $filter (split(',', $config{'filter_chain'})) { 76 # NOTE: keep ":chain ..." as reference to avoid error when rebuild active config 77 # -A|-I chain ... -j chain -> skip line if machtes filter_chain 78 if (/^.?-(A|I)\s+(\S+).*\s+-j\s+(.*)/) { 79 next LINE if($2 =~ /^$filter$/); 80 } 81 } 82 } 83 if (s/#\s*(.*)$//) { 84 $cmt .= " " if ($cmt); 85 $cmt .= $1; 86 $read_comment=1; 87 } 88 if (/^\*(\S+)/) { 89 # Start of a new table 90 $got{$1}++; 91 push(@rv, $table = { 'line' => $lnum, 92 'eline' => $lnum, 93 'name' => $1, 94 'rules' => [ ], 95 'defaults' => { } }); 96 } 97 elsif (/^:(\S+)\s+(\S+)/) { 98 # Default policy definition 99 $table->{'defaults'}->{$1} = $2; 100 } 101 elsif (/^(\[[^\]]*\]\s+)?-N\s+(\S+)(.*)/) { 102 # New chain definition 103 $table->{'defaults'}->{$2} = '-'; 104 } 105 elsif (/^(\[[^\]]*\]\s+)?-(A|I)\s+(\S+)(.*)/) { 106 # Rule definition 107 local $rule = { 'line' => $lnum, 108 'eline' => $lnum, 109 'index' => scalar(@{$table->{'rules'}}), 110 'cmt' => $cmt, 111 'chain' => $3, 112 'args' => $4 }; 113 if ($2 eq "I") { 114 unshift(@{$table->{'rules'}}, $rule); 115 } 116 else { 117 push(@{$table->{'rules'}}, $rule); 118 } 119 120 # Parse arguments 121 foreach $a (@known_args) { 122 local @vl; 123 while($rule->{'args'} =~ 124 s/\s+(!?)\s*($a)\s+(!?)\s*("[^"]*")(\s+|$)/ / || 125 $rule->{'args'} =~ 126 s/\s+(!?)\s*($a)\s+(!?)\s*('[^']*')(\s+|$)/ / || 127 $rule->{'args'} =~ 128 s/\s+(!?)\s*($a)\s+(!?)\s*(([^ \-!]\S*(\s+|$))+)/ / || 129 $rule->{'args'} =~ 130 s/\s+(!?)\s*($a)()(\s+|$)/ /) { 131 push(@vl, [ $1 || $3, &split_quoted_string($4) ]); 132 } 133 local ($aa = $a); $aa =~ s/^-+//; 134 if ($a eq '-m') { 135 $rule->{$aa} = \@vl if (@vl); 136 } 137 else { 138 $rule->{$aa} = $vl[0]; 139 } 140 } 141 } 142 elsif (/^COMMIT/) { 143 # Marks end of a table 144 $table->{'eline'} = $lnum; 145 } 146 elsif (/\S/) { 147 &error(&text('eiptables', "<tt>$_</tt>")); 148 } 149 $lnum++; 150 if (! defined($read_comment)) { $cmt=undef; } 151 } 152close(FILE); 153@rv = sort { $a->{'name'} cmp $b->{'name'} } @rv; 154local $i; 155map { $_->{'index'} = $i++ } @rv; 156return @rv; 157} 158 159# save_table(&table) 160# Updates an existing IPtable in the save file 161sub save_table 162{ 163local $lref; 164if ($config{"direct${ipvx}"}) { 165 # Read in the current iptables-save output 166 $lref = &read_file_lines("ip${ipvx}tables-save 2>/dev/null |", 1); 167 } 168else { 169 # Updating the save file 170 $lref = &read_file_lines($ipvx_save); 171 } 172local @lines = ( "*$_[0]->{'name'}" ); 173local ($d, $r); 174foreach $d (keys %{$_[0]->{'defaults'}}) { 175 push(@lines, ":$d $_[0]->{'defaults'}->{$d} [0:0]"); 176 } 177foreach $r (@{$_[0]->{'rules'}}) { 178 local $line; 179 $line = "# $r->{'cmt'}\n" if ($r->{'cmt'}); 180 $line .= "-A $r->{'chain'}"; 181 foreach $a (@known_args) { 182 local ($aa = $a); $aa =~ s/^-+//; 183 if ($r->{$aa}) { 184 local @al = ref($r->{$aa}->[0]) ? 185 @{$r->{$aa}} : ( $r->{$aa} ); 186 foreach $ag (@al) { 187 local $n = shift(@$ag); 188 local @w = ( $n ? ( $n ) : (), $a, @$ag ); 189 @w = map { $_ =~ /'/ ? "\"$_\"" : 190 $_ =~ /"/ ? "'".$_."'" : 191 $_ =~ /\s/ ? "\"$_\"" : $_ } @w; 192 $line .= " ".join(" ", @w); 193 } 194 } 195 } 196 $line .= " $r->{'args'}" if ($r->{'args'} =~ /\S/); 197 push(@lines, $line); 198 } 199push(@lines, "COMMIT"); 200if (defined($_[0]->{'line'})) { 201 # Update in file 202 splice(@$lref, $_[0]->{'line'}, $_[0]->{'eline'} - $_[0]->{'line'} + 1, 203 @lines); 204 } 205else { 206 # Append new table to file 207 push(@$lref, "# Generated by webmin", @lines, "# Completed"); 208 } 209if ($config{"direct${ipvx}"}) { 210 # Pass new lines to iptables-restore 211 open(SAVE, "| ip${ipvx}tables-restore"); 212 print SAVE map { $_."\n" } @$lref; 213 close(SAVE); 214 } 215else { 216 # Just save the file 217 &flush_file_lines(); 218 } 219} 220 221# get_ipsets_active() 222# return a list of active ipsets 223sub get_ipsets_active 224{ 225local (@rv, $name, $set={}); 226open(FILE, "ipset list -t 2>/dev/null |"); 227LINE: 228while(<FILE>) { 229 # remove newlines, get arg and value 230 s/\r|\n//g; 231 local ($n, $v) = split(/: /, $_); 232 ($n) = $n =~ /(\S+)/; 233 # get values from name to number 234 $name=$v if ($n eq "Name"); 235 $set->{$n}=$v; 236 if ($n eq "Number") { 237 push(@rv, $set); 238 $set={}; 239 } 240 } 241return @rv; 242} 243 244 245# describe_rule(&rule) 246# Returns a human-readable description of some rule conditions 247sub describe_rule 248{ 249local (@c, $d); 250foreach $d ('p', 's', 'd', 'i', 'o', 'f', 'dport', 251 'sport', 'tcp-flags', 'tcp-option', 252 'icmp-type', 'icmpv6-type', 'mac-source', 'limit', 'limit-burst', 253 'ports', 'uid-owner', 'gid-owner', 254 'pid-owner', 'sid-owner', 'ctstate', 'state', 'tos', 255 'dports', 'sports', 'physdev-in', 'physdev-out', 'args') { 256 if ($_[0]->{$d}) { 257 # get name and values 258 local ($n, @v) = @{$_[0]->{$d}}; 259 # with additional args 260 if ($d eq 'args') { 261 # get args 262 @v = grep {/\S/} split(/ / , $_[0]->{$d}); 263 # first arg is name, next are values 264 $n=shift(@v); 265 # translate src and dest parameter for ipset 266 push(@v, &text("desc_". pop(@v))) if ($n eq "--match-set"); 267 } 268 # uppercase for p 269 @v = map { uc($_) } @v if ($d eq 'p'); 270 # merge all in one for s and d 271 @v = map { join(", ", split(/,/, $_)) } @v if ($d eq 's' || $d eq 'd' ); 272 # compose desc_$n$d to get localized message, provide values as $1, ..., $n 273 local $txt = &text("desc_$d$n", map { "<strong>$_</strong>" } @v); 274 push(@c, $txt) if ($txt); 275 } 276 } 277local $rv; 278if (@c) { 279 $rv = &text('desc_conds', join(" $text{'desc_and'} ", @c)); 280 } 281else { 282 $rv = $text{'desc_always'}; 283 } 284return $rv; 285} 286 287# create_firewall_init() 288# Do whatever is needed to have the firewall started at boot time 289sub create_firewall_init 290{ 291if (defined(&enable_at_boot)) { 292 # Use distro's function 293 &enable_at_boot(); 294 } 295else { 296 # May need to create init script 297 &create_webmin_init(); 298 } 299} 300 301# create_webmin_init() 302# Create (if necessary) the Webmin iptables init script 303sub create_webmin_init 304{ 305local $res = &has_command("ip${ipvx}tables-restore"); 306local $ipt = &has_command("ip${ipvx}tables"); 307local $out = &backquote_command("$res -h 2>&1 </dev/null"); 308if ($out =~ /\s+-w\s+/) { 309 # Supports the wait flag, in case two instances are run at once 310 $res .= " -w"; 311 $ipt .= " -w"; 312 } 313local $start = "$res <$ipvx_save"; 314local $stop = "$ipt -t filter -F\n". 315 "$ipt -t nat -F\n". 316 "$ipt -t mangle -F\n". 317 "$ipt -t filter -P INPUT ACCEPT\n". 318 "$ipt -t filter -P OUTPUT ACCEPT\n". 319 "$ipt -t filter -P FORWARD ACCEPT\n". 320 "$ipt -t nat -P PREROUTING ACCEPT\n". 321 "$ipt -t nat -P POSTROUTING ACCEPT\n". 322 "$ipt -t nat -P OUTPUT ACCEPT\n". 323 "$ipt -t mangle -P PREROUTING ACCEPT\n". 324 "$ipt -t mangle -P OUTPUT ACCEPT"; 325&foreign_require("init", "init-lib.pl"); 326&init::enable_at_boot("webmin-ip${ipvx}tables", 327 "Load ip${ipvx}tables save file", 328 $start, $stop, undef, { 'exit' => 1 }); 329} 330 331# interface_choice(name, value) 332sub interface_choice 333{ 334local ($name, $value) = @_; 335local @ifaces; 336if (&foreign_check("net")) { 337 &foreign_require("net", "net-lib.pl"); 338 return &net::interface_choice($name, $value, undef, 0, 1); 339 } 340else { 341 return &ui_textbox($name, $value, 6); 342 } 343} 344 345sub check_previous 346{ 347 my (@p,$max,$n)=@_; 348 for ($i=0;$i<$max;$i++) 349 { 350 if ($n eq $p[$i]){return 1} 351 } 352 return -1; 353} 354 355sub by_string_for_iptables 356{ 357 my @p=("PREROUTING","INPUT","FORWARD","OUTPUT","POSTROUTING"); 358 359 for ($i=0;$i<@p;$i++) 360 { 361 if ($a eq $p[$i]){ 362 if (&check_previous(@p,$i,$b)){return -1;} 363 else{ return 1;}} 364 if ($b eq $p[$i]){ 365 if (&check_previous(@p,$i,$b)){return 1;} 366 else{ return -1;}} 367 } 368 369 return $a cmp $b; 370} 371 372sub missing_firewall_commands 373{ 374local $c; 375foreach $c ("ip${ipvx}tables", "ip${ipvx}tables-restore", "ip${ipvx}tables-save") { 376 return $c if (!&has_command($c)); 377 } 378return undef; 379} 380 381# iptables_restore() 382# Activates the current firewall rules, and returns any error 383sub iptables_restore 384{ 385local $rcmd = &has_command("ip${ipvx}tables-legacy-restore") || 386 "ip${ipvx}tables-restore"; 387local $out = &backquote_logged("cd / && $rcmd <$ipvx_save 2>&1"); 388return $? ? "<pre>$out</pre>" : undef; 389} 390 391# iptables_save() 392# Saves the active firewall rules, and returns any error 393sub iptables_save 394{ 395local $scmd = &has_command("ip${ipvx}tables-legacy-save") || 396 "ip${ipvx}tables-save"; 397local $out = &backquote_logged("$scmd >$ipvx_save 2>&1"); 398return $? ? "<pre>$out</pre>" : undef; 399} 400 401# can_edit_table(name) 402sub can_edit_table 403{ 404return $access{$_[0]}; 405} 406 407# can_jump(jump|&rule) 408sub can_jump 409{ 410return 1 if (!$access{'jumps'}); 411if (!%can_jumps_cache) { 412 %can_jumps_cache = map { lc($_), 1 } split(/\s+/, $access{'jumps'}); 413 } 414local $j = ref($_[0]) ? $_[0]->{'j'}->[1] : $_[0]; 415return 1 if (!$j); # always allow 'do nothing' 416return $can_jumps_cache{lc($j)}; 417} 418 419# run_before_command() 420# Runs the before-saving command, if any 421sub run_before_command 422{ 423if ($config{'before_cmd'}) { 424 &system_logged("($config{'before_cmd'}) </dev/null >/dev/null 2>&1"); 425 } 426} 427 428# run_after_command() 429# Runs the after-saving command, if any 430sub run_after_command 431{ 432if ($config{'after_cmd'}) { 433 &system_logged("($config{'after_cmd'}) </dev/null >/dev/null 2>&1"); 434 } 435} 436 437# run_before_apply_command() 438# Runs the before-applying command, if any. If it failes, returns the error 439# message output 440sub run_before_apply_command 441{ 442if ($config{'before_apply_cmd'}) { 443 local $out = &backquote_logged("($config{'before_apply_cmd'}) </dev/null 2>&1"); 444 return $out if ($?); 445 } 446return undef; 447} 448 449# run_after_apply_command() 450# Runs the after-applying command, if any 451sub run_after_apply_command 452{ 453if ($config{'after_apply_cmd'}) { 454 &system_logged("($config{'after_apply_cmd'}) </dev/null >/dev/null 2>&1"); 455 } 456} 457 458# apply_configuration() 459# Calls all the appropriate apply functions and programs, and returns an error 460# message if anything fails 461sub apply_configuration 462{ 463local $err = &run_before_apply_command(); 464return $err if ($err); 465local @oldlive = &get_iptables_save("direct"); 466if (defined(&apply_iptables)) { 467 # Call distro's apply command 468 $err = &apply_iptables(); 469 } 470else { 471 # Manually run iptables-restore 472 $err = &iptables_restore(); 473 } 474return $err if ($err); 475if (!$config{"direct${ipvx}"}) { 476 # Put back fail2ban rules 477 local @newlive = &get_iptables_save("direct"); 478 &merge_fail2ban_rules(\@oldlive, \@newlive); 479 } 480&run_after_apply_command(); 481return undef; 482} 483 484# merge_fail2ban_rules(&old-live, &new-live) 485# If there were fail2ban rules before applying but not after, re-create them 486sub merge_fail2ban_rules 487{ 488local ($oldlive, $newlive) = @_; 489local ($oldchain) = grep { $_->{'name'} eq 'f2b-default' } @$oldlive; 490local ($newchain) = grep { $_->{'name'} eq 'f2b-default' } @$newlive; 491return if (!$oldchain); # fail2ban was never used 492local ($oldinput) = grep { $_->{'name'} eq 'INPUT' } @$oldlive; 493return if (!$oldinput); 494local $oldrule; 495# XXX not complete yet 496} 497 498# list_cluster_servers() 499# Returns a list of servers on which the firewall is managed 500sub list_cluster_servers 501{ 502&foreign_require("servers", "servers-lib.pl"); 503local %ids = map { $_, 1 } split(/\s+/, $config{'servers'}); 504return grep { $ids{$_->{'id'}} } &servers::list_servers(); 505} 506 507# add_cluster_server(&server) 508sub add_cluster_server 509{ 510local @sids = split(/\s+/, $config{'servers'}); 511$config{'servers'} = join(" ", @sids, $_[0]->{'id'}); 512&save_module_config(); 513} 514 515# delete_cluster_server(&server) 516sub delete_cluster_server 517{ 518local @sids = split(/\s+/, $config{'servers'}); 519$config{'servers'} = join(" ", grep { $_ != $_[0]->{'id'} } @sids); 520&save_module_config(); 521} 522 523# server_name(&server) 524sub server_name 525{ 526return $_[0]->{'desc'} ? $_[0]->{'desc'} : $_[0]->{'host'}; 527} 528 529# copy_to_cluster([force]) 530# Copy all firewall rules from this server to those in the cluster 531sub copy_to_cluster 532{ 533return if (!$config{'servers'}); # no servers defined 534return if (!$_[0] && $config{'cluster_mode'}); # only push out when applying 535local $s; 536local $ltemp; 537if ($config{"direct${ipvx}"}) { 538 # Dump current configuration 539 $ltemp = &transname(); 540 system("ip${ipvx}tables-save >$ltemp 2>/dev/null"); 541 } 542foreach $s (&list_cluster_servers()) { 543 &remote_foreign_require($s, $module_name); 544 if ($config{"direct${ipvx}"}) { 545 # Directly activate on remote server! 546 local $rtemp = &remote_write($s, $ltemp); 547 unlink($ltemp); 548 local $err = &remote_eval($s, $module_name, 549 "\$out = `ip${ipvx}tables-restore <$rtemp 2>&1`; [ \$out, \$? ]"); 550 &remote_foreign_call($s, $module_name, "unlink_file", $rtemp); 551 &error(&text('apply_remote', $s->{'host'}, $err->[0])) 552 if ($err->[1]); 553 } 554 else { 555 # Can just copy across save file 556 local $rfile = &remote_eval($s, $module_name, 557 "\$ip${ipvx}tables_save_file"); 558 &remote_write($s, $ipvx_save, $rfile); 559 } 560 } 561} 562 563# apply_cluster_configuration() 564# Activate the current configuration on all servers in the cluster 565sub apply_cluster_configuration 566{ 567return undef if (!$config{'servers'}); 568if ($config{'cluster_mode'}) { 569 ©_to_cluster(1); 570 } 571local $s; 572foreach $s (&list_cluster_servers()) { 573 &remote_foreign_require($s->{'host'}, $module_name); 574 local $err = &remote_foreign_call( 575 $s->{'host'}, $module_name, "apply_configuration"); 576 if ($err) { 577 return &text('apply_remote', $s->{'host'}, $err); 578 } 579 } 580return undef; 581} 582 583# validate_iptables_config() 584# Tests that the rules file can be parsed 585sub validate_iptables_config 586{ 587my $out = &backquote_command( 588 "ip${ipvx}tables-restore --test <$ipvx_save 2>&1"); 589return undef if (!$?); 590$out =~ s/Try\s.*more\s+information.*//; 591return $out; 592} 593 594sub supports_conntrack 595{ 596if (!defined($supports_conntrack_cache)) { 597 my $out = &backquote_command("uname -r 2>/dev/null"); 598 $supports_conntrack_cache = $out =~ /^[3-9]\./ ? 1 : 0; 599 } 600return $supports_conntrack_cache; 601} 602 6031; 604 605