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	&copy_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