]> git.proxmox.com Git - pve-firewall.git/blob - src/PVE/FirewallSimulator.pm
fwtester.pl: add warnings to trace
[pve-firewall.git] / src / PVE / FirewallSimulator.pm
1 package PVE::FirewallSimulator;
2
3 use strict;
4 use warnings;
5 use Data::Dumper;
6 use PVE::Firewall;
7 use File::Basename;
8 use Net::IP;
9
10 my $mark;
11 my $trace;
12 my $debug = 0;
13
14 sub debug {
15 my $new_value = shift;
16
17 $debug = $new_value if defined($new_value);
18
19 return $debug;
20 }
21
22 sub reset_trace {
23 $trace = '';
24 }
25
26 sub get_trace {
27 return $trace;
28 }
29
30 sub add_trace {
31 my ($text) = @_;
32
33 if ($debug) {
34 print $text;
35 } else {
36 $trace .= $text;
37 }
38 }
39
40 $SIG{'__WARN__'} = sub {
41 my $err = $@;
42 my $t = $_[0];
43 chomp $t;
44 add_trace("$t\n");
45 $@ = $err;
46 };
47
48 sub nf_dev_match {
49 my ($devre, $dev) = @_;
50
51 $devre =~ s/\+$/\.\*/;
52 return ($dev =~ m/^${devre}$/) ? 1 : 0;
53 }
54
55 sub ipset_match {
56 my ($ipsetname, $ipset, $ipaddr) = @_;
57
58 my $ip = Net::IP->new($ipaddr);
59
60 foreach my $entry (@$ipset) {
61 next if $entry =~ m/^create/; # simply ignore
62 if ($entry =~ m/add \S+ (\S+)$/) {
63 my $test = Net::IP->new($1);
64 if ($test->overlaps($ip)) {
65 add_trace("IPSET $ipsetname match $ipaddr\n");
66 return 1;
67 }
68 } else {
69 die "implement me";
70 }
71 }
72
73 return 0;
74 }
75
76 sub rule_match {
77 my ($ipset_ruleset, $chain, $rule, $pkg) = @_;
78
79 $rule =~ s/^-A $chain // || die "got strange rule: $rule";
80
81 while (length($rule)) {
82
83 if ($rule =~ s/^-m conntrack --ctstate (\S+)\s*//) {
84 my $cstate = $1;
85
86 return undef if $cstate eq 'INVALID'; # no match
87 return undef if $cstate eq 'RELATED,ESTABLISHED'; # no match
88
89 next if $cstate =~ m/NEW/;
90
91 die "cstate test '$cstate' not implemented\n";
92 }
93
94 if ($rule =~ s/^-m addrtype --src-type (\S+)\s*//) {
95 my $atype = $1;
96 die "missing source address type (srctype)\n"
97 if !$pkg->{srctype};
98 return undef if $atype ne $pkg->{srctype};
99 }
100
101 if ($rule =~ s/^-m addrtype --dst-type (\S+)\s*//) {
102 my $atype = $1;
103 die "missing destination address type (dsttype)\n"
104 if !$pkg->{dsttype};
105 return undef if $atype ne $pkg->{dsttype};
106 }
107
108 if ($rule =~ s/^-i (\S+)\s*//) {
109 my $devre = $1;
110 die "missing interface (iface_in)\n" if !$pkg->{iface_in};
111 return undef if !nf_dev_match($devre, $pkg->{iface_in});
112 next;
113 }
114
115 if ($rule =~ s/^-o (\S+)\s*//) {
116 my $devre = $1;
117 die "missing interface (iface_out)\n" if !$pkg->{iface_out};
118 return undef if !nf_dev_match($devre, $pkg->{iface_out});
119 next;
120 }
121
122 if ($rule =~ s/^-p (tcp|udp|igmp|icmp)\s*//) {
123 die "missing proto" if !$pkg->{proto};
124 return undef if $pkg->{proto} ne $1; # no match
125 next;
126 }
127
128 if ($rule =~ s/^--dport (\d+):(\d+)\s*//) {
129 die "missing dport" if !$pkg->{dport};
130 return undef if ($pkg->{dport} < $1) || ($pkg->{dport} > $2); # no match
131 next;
132 }
133
134 if ($rule =~ s/^--dport (\d+)\s*//) {
135 die "missing dport" if !$pkg->{dport};
136 return undef if $pkg->{dport} != $1; # no match
137 next;
138 }
139
140 if ($rule =~ s/^-s (\S+)\s*//) {
141 die "missing source" if !$pkg->{source};
142 my $ip = Net::IP->new($1);
143 return undef if !$ip->overlaps(Net::IP->new($pkg->{source})); # no match
144 next;
145 }
146
147 if ($rule =~ s/^-d (\S+)\s*//) {
148 die "missing destination" if !$pkg->{dest};
149 my $ip = Net::IP->new($1);
150 return undef if !$ip->overlaps(Net::IP->new($pkg->{dest})); # no match
151 next;
152 }
153
154 if ($rule =~ s/^-m set --match-set (\S+) src\s*//) {
155 die "missing source" if !$pkg->{source};
156 my $ipset = $ipset_ruleset->{$1};
157 die "no such ip set '$1'" if !$ipset;
158 return undef if !ipset_match($1, $ipset, $pkg->{source});
159 next;
160 }
161
162 if ($rule =~ s/^-m set --match-set (\S+) dst\s*//) {
163 die "missing destination" if !$pkg->{dest};
164 my $ipset = $ipset_ruleset->{$1};
165 die "no such ip set '$1'" if !$ipset;
166 return undef if !ipset_match($1, $ipset, $pkg->{dest});
167 next;
168 }
169
170 if ($rule =~ s/^-m mac ! --mac-source (\S+)\s*//) {
171 die "missing source mac" if !$pkg->{mac_source};
172 return undef if $pkg->{mac_source} eq $1; # no match
173 next;
174 }
175
176 if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-in (\S+)\s*//) {
177 my $devre = $1;
178 return undef if !$pkg->{physdev_in};
179 return undef if !nf_dev_match($devre, $pkg->{physdev_in});
180 next;
181 }
182
183 if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-out (\S+)\s*//) {
184 my $devre = $1;
185 return undef if !$pkg->{physdev_out};
186 return undef if !nf_dev_match($devre, $pkg->{physdev_out});
187 next;
188 }
189
190 if ($rule =~ s/^-m mark --mark (\d+)\s*//) {
191 return undef if !defined($mark) || $mark != $1;
192 next;
193 }
194
195 # final actions
196
197 if ($rule =~ s/^-j MARK --set-mark (\d+)\s*$//) {
198 $mark = $1;
199 return undef;
200 }
201
202 if ($rule =~ s/^-j (\S+)\s*$//) {
203 return (0, $1);
204 }
205
206 if ($rule =~ s/^-g (\S+)\s*$//) {
207 return (1, $1);
208 }
209
210 if ($rule =~ s/^-j NFLOG --nflog-prefix \"[^\"]+\"$//) {
211 return undef;
212 }
213
214 last;
215 }
216
217 die "unable to parse rule: $rule";
218 }
219
220 sub ruleset_simulate_chain {
221 my ($ruleset, $ipset_ruleset, $chain, $pkg) = @_;
222
223 add_trace("ENTER chain $chain\n");
224
225 my $counter = 0;
226
227 if ($chain eq 'PVEFW-Drop') {
228 add_trace("LEAVE chain $chain\n");
229 return ('DROP', $counter);
230 }
231 if ($chain eq 'PVEFW-reject') {
232 add_trace("LEAVE chain $chain\n");
233 return ('REJECT', $counter);
234 }
235
236 if ($chain eq 'PVEFW-tcpflags') {
237 add_trace("LEAVE chain $chain\n");
238 return (undef, $counter);
239 }
240
241 my $rules = $ruleset->{$chain} ||
242 die "no such chain '$chain'";
243
244 foreach my $rule (@$rules) {
245 $counter++;
246 my ($goto, $action) = rule_match($ipset_ruleset, $chain, $rule, $pkg);
247 if (!defined($action)) {
248 add_trace("SKIP: $rule\n");
249 next;
250 }
251 add_trace("MATCH: $rule\n");
252
253 if ($action eq 'ACCEPT' || $action eq 'DROP' || $action eq 'REJECT') {
254 add_trace("TERMINATE chain $chain: $action\n");
255 return ($action, $counter);
256 } elsif ($action eq 'RETURN') {
257 add_trace("RETURN FROM chain $chain\n");
258 last;
259 } else {
260 if ($goto) {
261 add_trace("LEAVE chain $chain - goto $action\n");
262 return ruleset_simulate_chain($ruleset, $ipset_ruleset, $action, $pkg)
263 #$chain = $action;
264 #$rules = $ruleset->{$chain} || die "no such chain '$chain'";
265 } else {
266 my ($act, $ctr) = ruleset_simulate_chain($ruleset, $ipset_ruleset, $action, $pkg);
267 $counter += $ctr;
268 return ($act, $counter) if $act;
269 add_trace("CONTINUE chain $chain\n");
270 }
271 }
272 }
273
274 add_trace("LEAVE chain $chain\n");
275 if ($chain =~ m/^PVEFW-(INPUT|OUTPUT|FORWARD)$/) {
276 return ('ACCEPT', $counter); # default policy
277 }
278
279 return (undef, $counter);
280 }
281
282 sub copy_packet {
283 my ($pkg) = @_;
284
285 my $res = {};
286
287 while (my ($k,$v) = each %$pkg) {
288 $res->{$k} = $v;
289 }
290
291 return $res;
292 }
293
294 # Try to simulate packet traversal inside kernel. This invokes iptable
295 # checks several times.
296 sub route_packet {
297 my ($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state) = @_;
298
299 my $route_state = $start_state;
300
301 my $physdev_in;
302
303 my $ipt_invocation_counter = 0;
304 my $rule_check_counter = 0;
305
306 while ($route_state ne $target->{iface}) {
307
308 my $chain;
309 my $next_route_state;
310 my $next_physdev_in;
311
312 $pkg->{iface_in} = $pkg->{iface_out} = undef;
313 $pkg->{physdev_in} = $pkg->{physdev_out} = undef;
314
315 if ($route_state eq 'from-bport') {
316 $next_route_state = $from_info->{bridge} || die 'internal error';
317 $next_physdev_in = $from_info->{iface} || die 'internal error';
318 } elsif ($route_state eq 'host') {
319
320 if ($target->{type} eq 'bport') {
321 $pkg->{iface_in} = 'lo';
322 $pkg->{iface_out} = $target->{bridge} || die 'internal error';
323 $chain = 'PVEFW-OUTPUT';
324 $next_route_state = $target->{iface} || die 'internal error';
325 } elsif ($target->{type} eq 'ct') {
326 $pkg->{iface_in} = 'lo';
327 $pkg->{iface_out} = 'venet0';
328 $chain = 'PVEFW-OUTPUT';
329 $next_route_state = 'venet-in';
330 } elsif ($target->{type} eq 'vm') {
331 $pkg->{iface_in} = 'lo';
332 $pkg->{iface_out} = $target->{bridge} || die 'internal error';
333 $chain = 'PVEFW-OUTPUT';
334 $next_route_state = 'fwbr-in';
335 } else {
336 die "implement me";
337 }
338
339 } elsif ($route_state eq 'venet-out') {
340
341 if ($target->{type} eq 'host') {
342
343 $chain = 'PVEFW-INPUT';
344 $pkg->{iface_in} = 'venet0';
345 $pkg->{iface_out} = 'lo';
346 $next_route_state = 'host';
347
348 } elsif ($target->{type} eq 'bport') {
349
350 $chain = 'PVEFW-FORWARD';
351 $pkg->{iface_in} = 'venet0';
352 $pkg->{iface_out} = $target->{bridge} || die 'internal error';
353 $next_route_state = $target->{iface} || die 'internal error';
354
355 } elsif ($target->{type} eq 'vm') {
356
357 $chain = 'PVEFW-FORWARD';
358 $pkg->{iface_in} = 'venet0';
359 $pkg->{iface_out} = $target->{bridge} || die 'internal error';
360 $next_route_state = 'fwbr-in';
361
362 } elsif ($target->{type} eq 'ct') {
363
364 $chain = 'PVEFW-FORWARD';
365 $pkg->{iface_in} = 'venet0';
366 $pkg->{iface_out} = 'venet0';
367 $next_route_state = 'venet-in';
368
369 } else {
370 die "implement me";
371 }
372
373 } elsif ($route_state eq 'fwbr-out') {
374
375 $chain = 'PVEFW-FORWARD';
376 $next_route_state = $from_info->{bridge} || die 'internal error';
377 $next_physdev_in = $from_info->{fwpr} || die 'internal error';
378 $pkg->{iface_in} = $from_info->{fwbr} || die 'internal error';
379 $pkg->{iface_out} = $from_info->{fwbr} || die 'internal error';
380 $pkg->{physdev_in} = $from_info->{tapdev} || die 'internal error';
381 $pkg->{physdev_out} = $from_info->{fwln} || die 'internal error';
382
383 } elsif ($route_state eq 'fwbr-in') {
384
385 $chain = 'PVEFW-FORWARD';
386 $next_route_state = $target->{tapdev};
387 $pkg->{iface_in} = $target->{fwbr} || die 'internal error';
388 $pkg->{iface_out} = $target->{fwbr} || die 'internal error';
389 $pkg->{physdev_in} = $target->{fwln} || die 'internal error';
390 $pkg->{physdev_out} = $target->{tapdev} || die 'internal error';
391
392 } elsif ($route_state =~ m/^vmbr\d+$/) {
393
394 die "missing physdev_in - internal error?" if !$physdev_in;
395 $pkg->{physdev_in} = $physdev_in;
396
397 if ($target->{type} eq 'host') {
398
399 $chain = 'PVEFW-INPUT';
400 $pkg->{iface_in} = $route_state;
401 $pkg->{iface_out} = 'lo';
402 $next_route_state = 'host';
403
404 } elsif ($target->{type} eq 'bport') {
405
406 $chain = 'PVEFW-FORWARD';
407 $pkg->{iface_in} = $route_state;
408 $pkg->{iface_out} = $target->{bridge} || die 'internal error';
409 # conditionally set physdev_out (same behavior as kernel)
410 if ($route_state eq $target->{bridge}) {
411 $pkg->{physdev_out} = $target->{iface} || die 'internal error';
412 }
413 $next_route_state = $target->{iface};
414
415 } elsif ($target->{type} eq 'ct') {
416
417 $chain = 'PVEFW-FORWARD';
418 $pkg->{iface_in} = $route_state;
419 $pkg->{iface_out} = 'venet0';
420 $next_route_state = 'venet-in';
421
422 } elsif ($target->{type} eq 'vm') {
423
424 $chain = 'PVEFW-FORWARD';
425 $pkg->{iface_in} = $route_state;
426 $pkg->{iface_out} = $target->{bridge};
427 # conditionally set physdev_out (same behavior as kernel)
428 if ($route_state eq $target->{bridge}) {
429 $pkg->{physdev_out} = $target->{fwpr} || die 'internal error';
430 }
431 $next_route_state = 'fwbr-in';
432
433 } else {
434 die "implement me";
435 }
436
437 } else {
438 die "implement me $route_state";
439 }
440
441 die "internal error" if !defined($next_route_state);
442
443 if ($chain) {
444 add_trace("IPT check at $route_state (chain $chain)\n");
445 add_trace(Dumper($pkg));
446 $ipt_invocation_counter++;
447 my ($res, $ctr) = ruleset_simulate_chain($ruleset, $ipset_ruleset, $chain, $pkg);
448 $rule_check_counter += $ctr;
449 return ($res, $ipt_invocation_counter, $rule_check_counter) if $res ne 'ACCEPT';
450 }
451
452 $route_state = $next_route_state;
453
454 $physdev_in = $next_physdev_in;
455 }
456
457 return ('ACCEPT', $ipt_invocation_counter, $rule_check_counter);
458 }
459
460 sub extract_ct_info {
461 my ($vmdata, $vmid) = @_;
462
463 my $info = { type => 'ct', vmid => $vmid };
464
465 my $conf = $vmdata->{openvz}->{$vmid} || die "no such CT '$vmid'";
466 if ($conf->{ip_address}) {
467 $info->{ip_address} = $conf->{ip_address}->{value};
468 } else {
469 die "implement me";
470 }
471 return $info;
472 }
473
474 sub extract_vm_info {
475 my ($vmdata, $vmid) = @_;
476
477 my $info = { type => 'vm', vmid => $vmid };
478
479 my $conf = $vmdata->{qemu}->{$vmid} || die "no such VM '$vmid'";
480 my $net = PVE::QemuServer::parse_net($conf->{net0});
481 $info->{macaddr} = $net->{macaddr} || die "unable to get mac address";
482 $info->{bridge} = $net->{bridge} || die "unable to get bridge";
483 $info->{fwbr} = "fwbr${vmid}i0";
484 $info->{tapdev} = "tap${vmid}i0";
485 $info->{fwln} = "fwln${vmid}i0";
486 $info->{fwpr} = "fwpr${vmid}p0";
487
488 return $info;
489 }
490
491 sub simulate_firewall {
492 my ($ruleset, $ipset_ruleset, $host_ip, $vmdata, $test) = @_;
493
494 my $from = $test->{from} || die "missing 'from' field";
495 my $to = $test->{to} || die "missing 'to' field";
496 my $action = $test->{action} || die "missing 'action'";
497
498 my $testid = $test->{id};
499
500 die "from/to needs to be different" if $from eq $to;
501
502 my $pkg = {
503 proto => 'tcp',
504 sport => undef,
505 dport => undef,
506 source => undef,
507 dest => undef,
508 srctype => 'UNICAST',
509 dsttype => 'UNICAST',
510 };
511
512 while (my ($k,$v) = each %$test) {
513 next if $k eq 'from';
514 next if $k eq 'to';
515 next if $k eq 'action';
516 next if $k eq 'id';
517 die "unknown attribute '$k'\n" if !exists($pkg->{$k});
518 $pkg->{$k} = $v;
519 }
520
521 my $from_info = {};
522
523 my $start_state;
524
525 if ($from eq 'host') {
526 $from_info->{type} = 'host';
527 $start_state = 'host';
528 $pkg->{source} = $host_ip if !defined($pkg->{source});
529 } elsif ($from =~ m|^(vmbr\d+)/(\S+)$|) {
530 $from_info->{type} = 'bport';
531 $from_info->{bridge} = $1;
532 $from_info->{iface} = $2;
533 $start_state = 'from-bport';
534 } elsif ($from eq 'outside') {
535 $from_info->{type} = 'bport';
536 $from_info->{bridge} = 'vmbr0';
537 $from_info->{iface} = 'eth0';
538 $start_state = 'from-bport';
539 } elsif ($from eq 'nfvm') {
540 $from_info->{type} = 'bport';
541 $from_info->{bridge} = 'vmbr0';
542 $from_info->{iface} = 'tapXYZ';
543 $start_state = 'from-bport';
544 } elsif ($from =~ m/^ct(\d+)$/) {
545 my $vmid = $1;
546 $from_info = extract_ct_info($vmdata, $vmid);
547 if ($from_info->{ip_address}) {
548 $pkg->{source} = $from_info->{ip_address} if !defined($pkg->{source});
549 $start_state = 'venet-out';
550 } else {
551 die "implement me";
552 }
553 } elsif ($from =~ m/^vm(\d+)$/) {
554 my $vmid = $1;
555 $from_info = extract_vm_info($vmdata, $vmid);
556 $start_state = 'fwbr-out';
557 $pkg->{mac_source} = $from_info->{macaddr};
558 } else {
559 die "unable to parse \"from => '$from'\"\n";
560 }
561
562 my $target;
563
564 if ($to eq 'host') {
565 $target->{type} = 'host';
566 $target->{iface} = 'host';
567 $pkg->{dest} = $host_ip if !defined($pkg->{dest});
568 } elsif ($to =~ m|^(vmbr\d+)/(\S+)$|) {
569 $target->{type} = 'bport';
570 $target->{bridge} = $1;
571 $target->{iface} = $2;
572 } elsif ($to eq 'outside') {
573 $target->{type} = 'bport';
574 $target->{bridge} = 'vmbr0';
575 $target->{iface} = 'eth0';
576 } elsif ($to eq 'nfvm') {
577 $target->{type} = 'bport';
578 $target->{bridge} = 'vmbr0';
579 $target->{iface} = 'tapXYZ';
580 } elsif ($to =~ m/^ct(\d+)$/) {
581 my $vmid = $1;
582 $target = extract_ct_info($vmdata, $vmid);
583 $target->{iface} = 'venet-in';
584
585 if ($target->{ip_address}) {
586 $pkg->{dest} = $target->{ip_address};
587 } else {
588 die "implement me";
589 }
590 } elsif ($to =~ m/^vm(\d+)$/) {
591 my $vmid = $1;
592 $target = extract_vm_info($vmdata, $vmid);
593 $target->{iface} = $target->{tapdev};
594 } else {
595 die "unable to parse \"to => '$to'\"\n";
596 }
597
598 $pkg->{source} = '100.100.1.2' if !defined($pkg->{source});
599 $pkg->{dest} = '100.200.3.4' if !defined($pkg->{dest});
600
601 my ($res, $ic, $rc) = route_packet($ruleset, $ipset_ruleset, $pkg,
602 $from_info, $target, $start_state);
603
604 add_trace("IPT statistics: invocation = $ic, checks = $rc\n");
605
606 return $res if $action eq 'QUERY';
607
608 die "test failed ($res != $action)\n" if $action ne $res;
609
610 return undef;
611 }
612
613 1;
614