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