Handle hostnames with upper-case letters
[webmin.git] / firewall / firewall-lib.pl
1 # firewall-lib.pl
2 # Functions for parsing iptables-save format files
3 # - help pages
4
5 BEGIN { push(@INC, ".."); };
6 use WebminCore;
7 &init_config();
8 if ($config{'save_file'}) {
9         # Force use of a different save file, and webmin's functions
10         $iptables_save_file = $config{'save_file'};
11         }
12 else {
13         if (-r "$module_root_directory/$gconfig{'os_type'}-lib.pl") {
14                 # Use the operating system's save file and functions
15                 do "$gconfig{'os_type'}-lib.pl";
16                 }
17
18         if (!$iptables_save_file) {
19                 # Use webmin's own save file
20                 $iptables_save_file = "$module_config_directory/iptables.save";
21                 }
22         }
23
24 %access = &get_module_acl();
25
26 @known_tables = ( "filter", "mangle", "nat" );
27 @known_args =   ('-p', '-m', '-s', '-d', '-i', '-o', '-f',
28                  '--dport', '--sport', '--tcp-flags', '--tcp-option',
29                  '--icmp-type', '--mac-source', '--limit', '--limit-burst',
30                  '--ports', '--uid-owner', '--gid-owner',
31                  '--pid-owner', '--sid-owner', '--state', '--tos', '-j',
32                  '--to-ports', '--to-destination', '--to-source',
33                  '--reject-with', '--dports', '--sports',
34                  '--comment',
35                  '--physdev-is-bridged',
36                  '--physdev-is-in',
37                  '--physdev-is-out',
38                  '--physdev-in',
39                  '--physdev-out');
40
41 # get_iptables_save([file])
42 # Parse the iptables save file into a list of tables 
43 # format seems to be:
44 #  *table
45 #  :chain defaultpolicy
46 #  -A chain options
47 #  -N chain
48 #  COMMIT
49 sub get_iptables_save
50 {
51 local (@rv, $table, %got);
52 local $lnum = 0;
53 open(FILE, $_[0] || ($config{'direct'} ? "iptables-save 2>/dev/null |"
54                                        : $iptables_save_file));
55 local $cmt;
56 while(<FILE>) {
57         local $read_comment;
58         s/\r|\n//g;
59         if (s/#\s*(.*)$//) {
60                 $cmt .= " " if ($cmt);
61                 $cmt .= $1;
62                 $read_comment=1;
63                 }
64         if (/^\*(\S+)/) {
65                 # Start of a new table
66                 $got{$1}++;
67                 push(@rv, $table = { 'line' => $lnum,
68                                      'eline' => $lnum,
69                                      'name' => $1,
70                                      'rules' => [ ],
71                                      'defaults' => { } });
72                 }
73         elsif (/^:(\S+)\s+(\S+)/) {
74                 # Default policy definition
75                 $table->{'defaults'}->{$1} = $2;
76                 }
77         elsif (/^(\[[^\]]*\]\s+)?-N\s+(\S+)(.*)/) {
78                 # New chain definition
79                 $table->{'defaults'}->{$2} = '-';
80                 }
81         elsif (/^(\[[^\]]*\]\s+)?-(A|I)\s+(\S+)(.*)/) {
82                 # Rule definition
83                 local $rule = { 'line' => $lnum,
84                                 'eline' => $lnum,
85                                 'index' => scalar(@{$table->{'rules'}}),
86                                 'cmt' => $cmt,
87                                 'chain' => $3,
88                                 'args' => $4 };
89                 if ($2 eq "I") {
90                         unshift(@{$table->{'rules'}}, $rule);
91                         }
92                 else {
93                         push(@{$table->{'rules'}}, $rule);
94                         }
95
96                 # Parse arguments
97                 foreach $a (@known_args) {
98                         local @vl;
99                         while($rule->{'args'} =~
100                                s/\s+(!?)\s*($a)\s+(!?)\s*("[^"]*")(\s+|$)/ / ||
101                               $rule->{'args'} =~
102                                s/\s+(!?)\s*($a)\s+(!?)\s*('[^']*')(\s+|$)/ / ||
103                               $rule->{'args'} =~
104                                s/\s+(!?)\s*($a)\s+(!?)\s*(([^ \-!]\S*(\s+|$))+)/ / ||
105                               $rule->{'args'} =~
106                                s/\s+(!?)\s*($a)()(\s+|$)/ /) {
107                                 push(@vl, [ $1 || $3, &split_quoted_string($4) ]);
108                                 }
109                         local ($aa = $a); $aa =~ s/^-+//;
110                         if ($a eq '-m') {
111                                 $rule->{$aa} = \@vl if (@vl);
112                                 }
113                         else {
114                                 $rule->{$aa} = $vl[0];
115                                 }
116                         }
117                 }
118         elsif (/^COMMIT/) {
119                 # Marks end of a table
120                 $table->{'eline'} = $lnum;
121                 }
122         elsif (/\S/) {
123                 &error(&text('eiptables', "<tt>$_</tt>"));
124                 }
125         $lnum++;
126         if (! defined($read_comment)) { $cmt=undef; }
127         }
128 close(FILE);
129 @rv = sort { $a->{'name'} cmp $b->{'name'} } @rv;
130 local $i;
131 map { $_->{'index'} = $i++ } @rv;
132 return @rv;
133 }
134
135 # save_table(&table)
136 # Updates an existing IPtable in the save file
137 sub save_table
138 {
139 local $lref;
140 if ($config{'direct'}) {
141         # Read in the current iptables-save output
142         $lref = &read_file_lines("iptables-save 2>/dev/null |");
143         }
144 else {
145         # Updating the save file
146         $lref = &read_file_lines($iptables_save_file);
147         }
148 local @lines = ( "*$_[0]->{'name'}" );
149 local ($d, $r);
150 foreach $d (keys %{$_[0]->{'defaults'}}) {
151         push(@lines, ":$d $_[0]->{'defaults'}->{$d} [0:0]");
152         }
153 foreach $r (@{$_[0]->{'rules'}}) {
154         local $line;
155         $line = "# $r->{'cmt'}\n" if ($r->{'cmt'});
156         $line .= "-A $r->{'chain'}";
157         foreach $a (@known_args) {
158                 local ($aa = $a); $aa =~ s/^-+//;
159                 if ($r->{$aa}) {
160                         local @al = ref($r->{$aa}->[0]) ?
161                                         @{$r->{$aa}} : ( $r->{$aa} );
162                         foreach $ag (@al) {
163                                 local $n = shift(@$ag);
164                                 local @w = ( $n ? ( $n ) : (), $a, @$ag );
165                                 @w = map { $_ =~ /'/ ? "\"$_\"" :
166                                            $_ =~ /"/ ? "'".$_."'" :
167                                            $_ =~ /\s/ ? "\"$_\"" : $_ } @w;
168                                 $line .= " ".join(" ", @w);
169                                 }
170                         }
171                 }
172         $line .= " $r->{'args'}" if ($r->{'args'} =~ /\S/);
173         push(@lines, $line);
174         }
175 push(@lines, "COMMIT");
176 if (defined($_[0]->{'line'})) {
177         # Update in file
178         splice(@$lref, $_[0]->{'line'}, $_[0]->{'eline'} - $_[0]->{'line'} + 1,
179                @lines);
180         }
181 else {
182         # Append new table to file
183         push(@$lref, "# Generated by webmin", @lines, "# Completed");
184         }
185 if ($config{'direct'}) {
186         # Pass new lines to iptables-restore
187         open(SAVE, "| iptables-restore");
188         print SAVE map { $_."\n" } @$lref;
189         close(SAVE);
190         }
191 else {
192         # Just save the file
193         &flush_file_lines();
194         }
195 }
196
197 # describe_rule(&rule)
198 # Returns a human-readable description of some rule conditions
199 sub describe_rule
200 {
201 local (@c, $d);
202 foreach $d ('p', 's', 'd', 'i', 'o', 'f', 'dport',
203             'sport', 'tcp-flags', 'tcp-option',
204             'icmp-type', 'mac-source', 'limit', 'limit-burst',
205             'ports', 'uid-owner', 'gid-owner',
206             'pid-owner', 'sid-owner', 'state', 'tos',
207             'dports', 'sports', 'physdev-in', 'physdev-out') {
208         if ($_[0]->{$d}) {
209                 local ($n, @v) = @{$_[0]->{$d}};
210                 @v = map { uc($_) } @v if ($d eq 'p');
211                 local $txt = &text("desc_$d$n", map { "<b>$_</b>" } @v);
212                 push(@c, $txt) if ($txt);
213                 }
214         }
215 local $rv;
216 if (@c) {
217         $rv = &text('desc_conds', join(" $text{'desc_and'} ", @c));
218         }
219 else {
220         $rv = $text{'desc_always'};
221         }
222 return $rv;
223 }
224
225 # create_firewall_init()
226 # Do whatever is needed to have the firewall started at boot time
227 sub create_firewall_init
228 {
229 if (defined(&enable_at_boot)) {
230         # Use distro's function
231         &enable_at_boot();
232         }
233 else {
234         # May need to create init script
235         &create_webmin_init();
236         }
237 }
238
239 # create_webmin_init()
240 # Create (if necessary) the Webmin iptables init script
241 sub create_webmin_init
242 {
243 local $res = &has_command("iptables-restore");
244 local $ipt = &has_command("iptables");
245 local $start = "$res <$iptables_save_file";
246 local $stop = "$ipt -t filter -F\n".
247               "$ipt -t nat -F\n".
248               "$ipt -t mangle -F\n".
249               "$ipt -t filter -P INPUT ACCEPT\n".
250               "$ipt -t filter -P OUTPUT ACCEPT\n".
251               "$ipt -t filter -P FORWARD ACCEPT\n".
252               "$ipt -t nat -P PREROUTING ACCEPT\n".
253               "$ipt -t nat -P POSTROUTING ACCEPT\n".
254               "$ipt -t nat -P OUTPUT ACCEPT\n".
255               "$ipt -t mangle -P PREROUTING ACCEPT\n".
256               "$ipt -t mangle -P OUTPUT ACCEPT";
257 &foreign_require("init", "init-lib.pl");
258 &init::enable_at_boot("webmin-iptables", "Load IPtables save file",
259                       $start, $stop);
260 }
261
262 # interface_choice(name, value)
263 sub interface_choice
264 {
265 local @ifaces;
266 if (&foreign_check("net")) {
267         &foreign_require("net", "net-lib.pl");
268         return &net::interface_choice($_[0], $_[1], undef, 0, 1);
269         }
270 else {
271         return "<input name=$_[0] size=6 value='$_[1]'>";
272         }
273 }
274
275 sub check_previous
276 {
277         my (@p,$max,$n)=@_;
278         for ($i=0;$i<$max;$i++)
279         {
280                 if ($n eq $p[$i]){return 1}
281         }
282         return -1;
283 }
284  
285 sub by_string_for_iptables
286 {
287         my @p=("PREROUTING","INPUT","FORWARD","OUTPUT","POSTROUTING");
288
289         for ($i=0;$i<@p;$i++)
290         {
291                 if ($a eq $p[$i]){
292                         if (&check_previous(@p,$i,$b)){return -1;}
293                         else{ return 1;}}
294                 if ($b eq $p[$i]){
295                         if (&check_previous(@p,$i,$b)){return 1;}
296                         else{ return -1;}}
297         }
298
299         return $a cmp $b;
300 }
301
302 sub missing_firewall_commands
303 {
304 local $c;
305 foreach $c ("iptables", "iptables-restore", "iptables-save") {
306         return $c if (!&has_command($c));
307         }
308 return undef;
309 }
310
311 # iptables_restore()
312 # Activates the current firewall rules, and returns any error
313 sub iptables_restore
314 {
315 local $out = &backquote_logged("cd / ; iptables-restore <$iptables_save_file 2>&1");
316 return $? ? "<pre>$out</pre>" : undef;
317 }
318
319 # iptables_save()
320 # Saves the active firewall rules, and returns any error
321 sub iptables_save
322 {
323 local $out = &backquote_logged("iptables-save >$iptables_save_file 2>&1");
324 return $? ? "<pre>$out</pre>" : undef;
325 }
326
327 # can_edit_table(name)
328 sub can_edit_table
329 {
330 return $access{$_[0]};
331 }
332
333 # can_jump(jump|&rule)
334 sub can_jump
335 {
336 return 1 if (!$access{'jumps'});
337 if (!%can_jumps_cache) {
338         %can_jumps_cache = map { lc($_), 1 } split(/\s+/, $access{'jumps'});
339         }
340 local $j = ref($_[0]) ? $_[0]->{'j'}->[1] : $_[0];
341 return 1 if (!$j);      # always allow 'do nothing'
342 return $can_jumps_cache{lc($j)};
343 }
344
345 # run_before_command()
346 # Runs the before-saving command, if any
347 sub run_before_command
348 {
349 if ($config{'before_cmd'}) {
350         &system_logged("($config{'before_cmd'}) </dev/null >/dev/null 2>&1");
351         }
352 }
353
354 # run_after_command()
355 # Runs the after-saving command, if any
356 sub run_after_command
357 {
358 if ($config{'after_cmd'}) {
359         &system_logged("($config{'after_cmd'}) </dev/null >/dev/null 2>&1");
360         }
361 }
362
363 # run_before_apply_command()
364 # Runs the before-applying command, if any. If it failes, returns the error
365 # message output
366 sub run_before_apply_command
367 {
368 if ($config{'before_apply_cmd'}) {
369         local $out = &backquote_logged("($config{'before_apply_cmd'}) </dev/null 2>&1");
370         return $out if ($?);
371         }
372 return undef;
373 }
374
375 # run_after_apply_command()
376 # Runs the after-applying command, if any
377 sub run_after_apply_command
378 {
379 if ($config{'after_apply_cmd'}) {
380         &system_logged("($config{'after_apply_cmd'}) </dev/null >/dev/null 2>&1");
381         }
382 }
383
384 # apply_configuration()
385 # Calls all the appropriate apply functions and programs, and returns an error
386 # message if anything fails
387 sub apply_configuration
388 {
389 local $err = &run_before_apply_command();
390 return $err if ($err);
391 if (defined(&apply_iptables)) {
392         # Call distro's apply command
393         $err = &apply_iptables();
394         }
395 else {
396         # Manually run iptables-restore
397         $err = &iptables_restore();
398         }
399 return $err if ($err);
400 &run_after_apply_command();
401 return undef;
402 }
403
404 # list_cluster_servers()
405 # Returns a list of servers on which the firewall is managed
406 sub list_cluster_servers
407 {
408 &foreign_require("servers", "servers-lib.pl");
409 local %ids = map { $_, 1 } split(/\s+/, $config{'servers'});
410 return grep { $ids{$_->{'id'}} } &servers::list_servers();
411 }
412
413 # add_cluster_server(&server)
414 sub add_cluster_server
415 {
416 local @sids = split(/\s+/, $config{'servers'});
417 $config{'servers'} = join(" ", @sids, $_[0]->{'id'});
418 &save_module_config();
419 }
420
421 # delete_cluster_server(&server)
422 sub delete_cluster_server
423 {
424 local @sids = split(/\s+/, $config{'servers'});
425 $config{'servers'} = join(" ", grep { $_ != $_[0]->{'id'} } @sids);
426 &save_module_config();
427 }
428
429 # server_name(&server)
430 sub server_name
431 {
432 return $_[0]->{'desc'} ? $_[0]->{'desc'} : $_[0]->{'host'};
433 }
434
435 # copy_to_cluster([force])
436 # Copy all firewall rules from this server to those in the cluster
437 sub copy_to_cluster
438 {
439 return if (!$config{'servers'});                # no servers defined
440 return if (!$_[0] && $config{'cluster_mode'});  # only push out when applying
441 local $s;
442 local $ltemp;
443 if ($config{'direct'}) {
444         # Dump current configuration
445         $ltemp = &transname();
446         system("iptables-save >$ltemp 2>/dev/null");
447         }
448 foreach $s (&list_cluster_servers()) {
449         &remote_foreign_require($s, "firewall", "firewall-lib.pl");
450         if ($config{'direct'}) {
451                 # Directly activate on remote server!
452                 local $rtemp = &remote_write($s, $ltemp);
453                 unlink($ltemp);
454                 local $err = &remote_eval($s, "firewall",
455                   "\$out = `iptables-restore <$rtemp 2>&1`; [ \$out, \$? ]"); 
456                 &remote_eval($s, "firewall", "unlink('$rtemp')");
457                 &error(&text('apply_remote', $s->{'host'}, $err->[0]))
458                         if ($err->[1]);
459                 }
460         else {
461                 # Can just copy across save file
462                 local $rfile = &remote_eval($s, "firewall",
463                                             "\$iptables_save_file");
464                 &remote_write($s, $iptables_save_file, $rfile);
465                 }
466         }
467 }
468
469 # apply_cluster_configuration()
470 # Activate the current configuration on all servers in the cluster
471 sub apply_cluster_configuration
472 {
473 return undef if (!$config{'servers'});
474 if ($config{'cluster_mode'}) {
475         &copy_to_cluster(1);
476         }
477 local $s;
478 foreach $s (&list_cluster_servers()) {
479         &remote_foreign_require($s->{'host'}, "firewall", "firewall-lib.pl");
480         local $err = &remote_foreign_call($s->{'host'}, "firewall", "apply_configuration");
481         if ($err) {
482                 return &text('apply_remote', $s->{'host'}, $err);
483                 }
484         }
485 return undef;
486 }
487
488 1;
489