]>
Commit | Line | Data |
---|---|---|
f1bafd37 DM |
1 | #!/usr/bin/perl |
2 | ||
3 | use lib '../src'; | |
4 | use strict; | |
5 | use warnings; | |
6 | use Data::Dumper; | |
7 | use PVE::Firewall; | |
8 | ||
9 | my $mark; | |
10 | my $trace; | |
11 | ||
31dc73f1 DM |
12 | my $outside_iface = 'eth0'; |
13 | my $outside_bridge = 'vmbr0'; | |
14 | ||
d1486f38 DM |
15 | my $debug = 0; |
16 | ||
17 | sub add_trace { | |
18 | my ($text) = @_; | |
19 | ||
20 | if ($debug) { | |
21 | print $text; | |
22 | } else { | |
23 | $trace .= $text; | |
24 | } | |
25 | } | |
26 | ||
f1bafd37 DM |
27 | sub rule_match { |
28 | my ($chain, $rule, $pkg) = @_; | |
29 | ||
30 | $rule =~ s/^-A $chain // || die "got strange rule: $rule"; | |
31 | ||
32 | if ($rule =~ s/^-m conntrack\s*//) { | |
33 | return undef; # simply ignore | |
34 | } | |
35 | ||
36 | if ($rule =~ s/^-m addrtype\s*//) { | |
37 | return undef; # simply ignore | |
38 | } | |
39 | ||
40 | if ($rule =~ s/^-i (\S+)\s*//) { | |
41 | die "missing iface_in" if !$pkg->{iface_in}; | |
42 | return undef if $pkg->{iface_in} ne $1; # no match | |
43 | } | |
44 | if ($rule =~ s/^-o (\S+)\s*//) { | |
45 | die "missing iface_out" if !$pkg->{iface_out}; | |
46 | return undef if $pkg->{iface_out} ne $1; # no match | |
47 | } | |
48 | ||
49 | if ($rule =~ s/^-p (tcp|udp)\s*//) { | |
50 | die "missing proto" if !$pkg->{proto}; | |
51 | return undef if $pkg->{proto} ne $1; # no match | |
52 | } | |
53 | ||
54 | if ($rule =~ s/^--dport (\d+):(\d+)\s*//) { | |
55 | die "missing dport" if !$pkg->{dport}; | |
56 | return undef if ($pkg->{dport} < $1) || ($pkg->{dport} > $2); # no match | |
57 | } | |
58 | ||
59 | if ($rule =~ s/^--dport (\d+)\s*//) { | |
60 | die "missing dport" if !$pkg->{dport}; | |
61 | return undef if $pkg->{dport} != $1; # no match | |
62 | } | |
63 | ||
64 | if ($rule =~ s/^-s (\S+)\s*//) { | |
65 | die "missing source" if !$pkg->{source}; | |
66 | return undef if $pkg->{source} ne $1; # no match | |
67 | } | |
68 | ||
69 | if ($rule =~ s/^-d (\S+)\s*//) { | |
70 | die "missing destination" if !$pkg->{dest}; | |
71 | return undef if $pkg->{dest} ne $1; # no match | |
72 | } | |
73 | ||
74 | if ($rule =~ s/^-m mac ! --mac-source (\S+)\s*//) { | |
75 | die "missing source mac" if !$pkg->{mac_source}; | |
76 | return undef if $pkg->{mac_source} eq $1; # no match | |
77 | } | |
78 | ||
79 | if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-in (\S+)\s*//) { | |
80 | my $devre = $1; | |
81 | $devre =~ s/\+/\.\*/; | |
82 | return undef if !$pkg->{physdev_in}; | |
83 | return undef if $pkg->{physdev_in} !~ m/^${devre}$/; | |
84 | } | |
85 | ||
86 | if ($rule =~ s/^-m physdev --physdev-is-bridged --physdev-out (\S+)\s*//) { | |
87 | my $devre = $1; | |
88 | $devre =~ s/\+/\.\*/; | |
89 | return undef if !$pkg->{physdev_out}; | |
90 | return undef if $pkg->{physdev_out} !~ m/^${devre}$/; | |
91 | } | |
92 | ||
93 | if ($rule =~ s/^-j MARK --set-mark (\d+)\s*$//) { | |
94 | $mark = $1; | |
95 | return undef; | |
96 | } | |
97 | ||
98 | if ($rule =~ s/^-j (\S+)\s*$//) { | |
99 | return (0, $1); | |
100 | } | |
101 | ||
102 | if ($rule =~ s/^-g (\S+)\s*$//) { | |
103 | return (1, $1); | |
104 | } | |
105 | ||
106 | die "unable to parse rule: $rule"; | |
107 | } | |
108 | ||
109 | sub ruleset_simulate_chain { | |
110 | my ($ruleset, $chain, $pkg) = @_; | |
111 | ||
d1486f38 | 112 | add_trace("ENTER chain $chain\n"); |
f1bafd37 DM |
113 | |
114 | if ($chain eq 'PVEFW-Drop') { | |
d1486f38 | 115 | add_trace("LEAVE chain $chain\n"); |
f1bafd37 DM |
116 | return 'DROP'; |
117 | } | |
118 | if ($chain eq 'PVEFW-reject') { | |
d1486f38 | 119 | add_trace("LEAVE chain $chain\n"); |
f1bafd37 DM |
120 | return 'REJECT'; |
121 | } | |
122 | ||
123 | if ($chain eq 'PVEFW-tcpflags') { | |
d1486f38 | 124 | add_trace("LEAVE chain $chain\n"); |
f1bafd37 DM |
125 | return undef; |
126 | } | |
127 | ||
128 | my $rules = $ruleset->{$chain} || | |
129 | die "no such chain '$chain'"; | |
130 | ||
131 | foreach my $rule (@$rules) { | |
132 | my ($goto, $action) = rule_match($chain, $rule, $pkg); | |
133 | if (!defined($action)) { | |
d1486f38 | 134 | add_trace("SKIP: $rule\n"); |
f1bafd37 DM |
135 | next; |
136 | } | |
d1486f38 | 137 | add_trace("MATCH: $rule\n"); |
f1bafd37 DM |
138 | |
139 | if ($action eq 'ACCEPT' || $action eq 'DROP' || $action eq 'REJECT') { | |
d1486f38 | 140 | add_trace("TERMINATE chain $chain: $action\n"); |
f1bafd37 DM |
141 | return $action; |
142 | } elsif ($action eq 'RETURN') { | |
d1486f38 | 143 | add_trace("RETURN FROM chain $chain\n"); |
f1bafd37 DM |
144 | last; |
145 | } else { | |
146 | if ($goto) { | |
d1486f38 | 147 | add_trace("LEAVE chain $chain - goto $action\n"); |
f1bafd37 DM |
148 | return ruleset_simulate_chain($ruleset, $action, $pkg) |
149 | #$chain = $action; | |
150 | #$rules = $ruleset->{$chain} || die "no such chain '$chain'"; | |
151 | } else { | |
152 | if ($action = ruleset_simulate_chain($ruleset, $action, $pkg)) { | |
153 | return $action; | |
154 | } | |
d1486f38 | 155 | add_trace("CONTINUE chain $chain\n"); |
f1bafd37 DM |
156 | } |
157 | } | |
158 | } | |
159 | ||
d1486f38 | 160 | add_trace("LEAVE chain $chain\n"); |
f1bafd37 DM |
161 | if ($chain =~ m/^PVEFW-(INPUT|OUTPUT|FORWARD)$/) { |
162 | return 'ACCEPT'; # default policy | |
163 | } | |
164 | ||
165 | return undef; | |
166 | } | |
167 | ||
168 | sub copy_packet { | |
169 | my ($pkg) = @_; | |
170 | ||
171 | my $res = {}; | |
172 | ||
173 | while (my ($k,$v) = each %$pkg) { | |
174 | $res->{$k} = $v; | |
175 | } | |
176 | ||
177 | return $res; | |
178 | } | |
179 | ||
d1486f38 DM |
180 | # Try to simulate packet traversal inside kernel. This invokes iptable |
181 | # checks several times. | |
182 | sub route_packet { | |
183 | my ($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state) = @_; | |
184 | ||
185 | my $route_state = $start_state; | |
186 | ||
187 | my $physdev_in; | |
188 | ||
189 | while ($route_state ne $target->{iface}) { | |
190 | ||
191 | my $chain; | |
192 | my $next_route_state; | |
193 | my $next_physdev_in; | |
194 | ||
195 | $pkg->{iface_in} = $pkg->{iface_out} = undef; | |
196 | $pkg->{physdev_in} = $pkg->{physdev_out} = undef; | |
197 | ||
31dc73f1 DM |
198 | if ($route_state eq 'from-outside') { |
199 | $next_route_state = $outside_bridge || die 'internal error'; | |
200 | $next_physdev_in = $outside_iface || die 'internal error'; | |
201 | } elsif ($route_state eq 'host') { | |
d1486f38 | 202 | |
31dc73f1 DM |
203 | if ($target->{type} eq 'outside') { |
204 | $pkg->{iface_in} = 'lo'; | |
205 | $pkg->{iface_out} = $outside_bridge; | |
206 | $chain = 'PVEFW-OUTPUT'; | |
207 | $next_route_state = $outside_iface | |
208 | } elsif ($target->{type} eq 'ct') { | |
d1486f38 DM |
209 | $pkg->{iface_in} = 'lo'; |
210 | $pkg->{iface_out} = 'venet0'; | |
211 | $chain = 'PVEFW-OUTPUT'; | |
212 | $next_route_state = 'venet-in'; | |
213 | } elsif ($target->{type} eq 'vm') { | |
214 | $pkg->{iface_in} = 'lo'; | |
215 | $pkg->{iface_out} = $target->{bridge} || die 'internal error'; | |
216 | $chain = 'PVEFW-OUTPUT'; | |
217 | $next_route_state = 'fwbr-in'; | |
218 | } else { | |
219 | die "implement me"; | |
220 | } | |
221 | ||
222 | } elsif ($route_state eq 'venet-out') { | |
223 | ||
224 | if ($target->{type} eq 'host') { | |
225 | ||
226 | $chain = 'PVEFW-INPUT'; | |
227 | $pkg->{iface_in} = 'venet0'; | |
228 | $pkg->{iface_out} = 'lo'; | |
229 | $next_route_state = 'host'; | |
230 | ||
31dc73f1 DM |
231 | } elsif ($target->{type} eq 'outside') { |
232 | ||
233 | $chain = 'PVEFW-FORWARD'; | |
234 | $pkg->{iface_in} = 'venet0'; | |
235 | $pkg->{iface_out} = $outside_bridge; | |
236 | $next_route_state = $outside_iface; | |
237 | ||
d1486f38 DM |
238 | } elsif ($target->{type} eq 'vm') { |
239 | ||
240 | $chain = 'PVEFW-FORWARD'; | |
241 | $pkg->{iface_in} = 'venet0'; | |
242 | $pkg->{iface_out} = $target->{bridge} || die 'internal error'; | |
243 | $next_route_state = 'fwbr-in'; | |
244 | ||
245 | } elsif ($target->{type} eq 'ct') { | |
246 | ||
247 | $chain = 'PVEFW-FORWARD'; | |
248 | $pkg->{iface_in} = 'venet0'; | |
249 | $pkg->{iface_out} = 'venet0'; | |
250 | $next_route_state = 'venet-in'; | |
251 | ||
252 | } else { | |
253 | die "implement me"; | |
254 | } | |
255 | ||
256 | } elsif ($route_state eq 'fwbr-out') { | |
257 | ||
258 | $chain = 'PVEFW-FORWARD'; | |
259 | $next_route_state = $from_info->{bridge} || die 'internal error'; | |
260 | $next_physdev_in = $from_info->{fwpr} || die 'internal error'; | |
261 | $pkg->{iface_in} = $from_info->{fwbr} || die 'internal error'; | |
262 | $pkg->{iface_out} = $from_info->{fwbr} || die 'internal error'; | |
263 | $pkg->{physdev_in} = $from_info->{tapdev} || die 'internal error'; | |
264 | $pkg->{physdev_out} = $from_info->{fwln} || die 'internal error'; | |
265 | ||
266 | } elsif ($route_state eq 'fwbr-in') { | |
267 | ||
268 | $chain = 'PVEFW-FORWARD'; | |
269 | $next_route_state = $target->{tapdev}; | |
270 | $pkg->{iface_in} = $target->{fwbr} || die 'internal error'; | |
271 | $pkg->{iface_out} = $target->{fwbr} || die 'internal error'; | |
272 | $pkg->{physdev_in} = $target->{fwln} || die 'internal error'; | |
273 | $pkg->{physdev_out} = $target->{tapdev} || die 'internal error'; | |
274 | ||
275 | } elsif ($route_state =~ m/^vmbr\d+$/) { | |
276 | ||
277 | die "missing physdev_in - internal error?" if !$physdev_in; | |
278 | ||
279 | if ($target->{type} eq 'host') { | |
280 | ||
281 | $chain = 'PVEFW-INPUT'; | |
282 | $pkg->{iface_in} = $route_state; | |
283 | $pkg->{iface_out} = 'lo'; | |
284 | $next_route_state = 'host'; | |
285 | ||
31dc73f1 DM |
286 | if ($route_state eq $outside_bridge) { |
287 | ||
288 | } else { | |
289 | ||
290 | } | |
291 | ||
292 | } elsif ($target->{type} eq 'outside') { | |
293 | ||
294 | $chain = 'PVEFW-FORWARD'; | |
295 | $pkg->{iface_in} = $route_state; | |
296 | $pkg->{iface_out} = $outside_bridge; | |
297 | $pkg->{physdev_in} = $physdev_in; | |
298 | # conditionally set physdev_out (same behavior as kernel) | |
299 | if ($route_state eq $outside_bridge) { | |
300 | $pkg->{physdev_out} = $outside_iface || die 'internal error'; | |
301 | } | |
302 | $next_route_state = $outside_iface; | |
303 | ||
d1486f38 DM |
304 | } elsif ($target->{type} eq 'ct') { |
305 | ||
306 | $chain = 'PVEFW-FORWARD'; | |
307 | $pkg->{iface_in} = $route_state; | |
308 | $pkg->{iface_out} = 'venet0'; | |
309 | $next_route_state = 'venet-in'; | |
310 | ||
311 | } elsif ($target->{type} eq 'vm') { | |
312 | ||
313 | $chain = 'PVEFW-FORWARD'; | |
31dc73f1 DM |
314 | $pkg->{iface_in} = $route_state; |
315 | $pkg->{iface_out} = $target->{bridge}; | |
316 | $pkg->{physdev_in} = $physdev_in; | |
317 | # conditionally set physdev_out (same behavior as kernel) | |
d1486f38 | 318 | if ($route_state eq $target->{bridge}) { |
d1486f38 | 319 | $pkg->{physdev_out} = $target->{fwpr} || die 'internal error'; |
d1486f38 DM |
320 | } |
321 | $next_route_state = 'fwbr-in'; | |
322 | ||
323 | } else { | |
324 | die "implement me"; | |
325 | } | |
326 | ||
327 | } else { | |
328 | die "implement me $route_state"; | |
329 | } | |
330 | ||
331 | die "internal error" if !defined($next_route_state); | |
332 | ||
333 | if ($chain) { | |
334 | add_trace("IPT check at $route_state (chain $chain)\n"); | |
335 | add_trace(Dumper($pkg)); | |
336 | my $res = ruleset_simulate_chain($ruleset, $chain, $pkg); | |
337 | return $res if $res ne 'ACCEPT'; | |
338 | } | |
339 | ||
340 | $route_state = $next_route_state; | |
341 | ||
342 | $physdev_in = $next_physdev_in; | |
343 | } | |
344 | ||
345 | return 'ACCEPT'; | |
346 | } | |
347 | ||
348 | sub extract_ct_info { | |
349 | my ($vmdata, $vmid) = @_; | |
350 | ||
351 | my $info = { type => 'ct', vmid => $vmid }; | |
352 | ||
353 | my $conf = $vmdata->{openvz}->{$vmid} || die "no such CT '$vmid'"; | |
354 | if ($conf->{ip_address}) { | |
355 | $info->{ip_address} = $conf->{ip_address}->{value}; | |
356 | } else { | |
357 | die "implement me"; | |
358 | } | |
359 | return $info; | |
360 | } | |
361 | ||
362 | sub extract_vm_info { | |
363 | my ($vmdata, $vmid) = @_; | |
364 | ||
365 | my $info = { type => 'vm', vmid => $vmid }; | |
366 | ||
367 | my $conf = $vmdata->{qemu}->{$vmid} || die "no such VM '$vmid'"; | |
368 | my $net = PVE::QemuServer::parse_net($conf->{net0}); | |
369 | $info->{macaddr} = $net->{macaddr} || die "unable to get mac address"; | |
370 | $info->{bridge} = $net->{bridge} || die "unable to get bridge"; | |
371 | $info->{fwbr} = "fwbr${vmid}i0"; | |
372 | $info->{tapdev} = "tap${vmid}i0"; | |
373 | $info->{fwln} = "fwln${vmid}i0"; | |
374 | $info->{fwpr} = "fwpr${vmid}p0"; | |
375 | ||
376 | return $info; | |
377 | } | |
f1bafd37 DM |
378 | |
379 | sub simulate_firewall { | |
380 | my ($ruleset, $ipset_ruleset, $vmdata, $test) = @_; | |
381 | ||
382 | my $from = delete $test->{from} || die "missing 'from' field"; | |
383 | my $to = delete $test->{to} || die "missing 'to' field"; | |
384 | my $action = delete $test->{action} || die "missing 'action'"; | |
385 | ||
386 | die "from/to needs to be different" if $from eq $to; | |
387 | ||
388 | my $pkg = { | |
f1bafd37 DM |
389 | proto => 'tcp', |
390 | sport => '1234', | |
391 | dport => '4321', | |
392 | source => '10.11.12.13', | |
393 | dest => '10.11.12.14', | |
394 | }; | |
395 | ||
396 | while (my ($k,$v) = each %$test) { | |
397 | $pkg->{$k} = $v; | |
398 | } | |
399 | ||
d1486f38 DM |
400 | my $from_info = {}; |
401 | ||
402 | my $start_state; | |
f1bafd37 DM |
403 | |
404 | if ($from eq 'host') { | |
d1486f38 DM |
405 | $from_info->{type} = 'host'; |
406 | $start_state = 'host'; | |
31dc73f1 DM |
407 | } elsif ($from eq 'outside') { |
408 | $from_info->{type} = 'outside'; | |
409 | $start_state = 'from-outside'; | |
f1bafd37 DM |
410 | } elsif ($from =~ m/^ct(\d+)$/) { |
411 | my $vmid = $1; | |
d1486f38 DM |
412 | $from_info = extract_ct_info($vmdata, $vmid); |
413 | if ($from_info->{ip_address}) { | |
414 | $pkg->{source} = $from_info->{ip_address}; | |
415 | $start_state = 'venet-out'; | |
f1bafd37 DM |
416 | } else { |
417 | die "implement me"; | |
418 | } | |
f1bafd37 DM |
419 | } elsif ($from =~ m/^vm(\d+)$/) { |
420 | my $vmid = $1; | |
d1486f38 DM |
421 | $from_info = extract_vm_info($vmdata, $vmid); |
422 | $start_state = 'fwbr-out'; | |
423 | $pkg->{mac_source} = $from_info->{macaddr}; | |
f1bafd37 DM |
424 | } else { |
425 | die "implement me"; | |
426 | } | |
427 | ||
d1486f38 DM |
428 | my $target; |
429 | ||
f1bafd37 | 430 | if ($to eq 'host') { |
d1486f38 DM |
431 | $target->{type} = 'host'; |
432 | $target->{iface} = 'host'; | |
31dc73f1 DM |
433 | } elsif ($to eq 'outside') { |
434 | $target->{type} = 'outside'; | |
435 | $target->{iface} = $outside_iface; | |
f1bafd37 DM |
436 | } elsif ($to =~ m/^ct(\d+)$/) { |
437 | my $vmid = $1; | |
d1486f38 DM |
438 | $target = extract_ct_info($vmdata, $vmid); |
439 | $target->{iface} = 'venet-in'; | |
440 | ||
441 | if ($target->{ip_address}) { | |
442 | $pkg->{dest} = $target->{ip_address}; | |
f1bafd37 DM |
443 | } else { |
444 | die "implement me"; | |
445 | } | |
f1bafd37 DM |
446 | } elsif ($to =~ m/^vm(\d+)$/) { |
447 | my $vmid = $1; | |
d1486f38 DM |
448 | $target = extract_vm_info($vmdata, $vmid); |
449 | $target->{iface} = $target->{tapdev}; | |
f1bafd37 DM |
450 | } else { |
451 | die "implement me"; | |
452 | } | |
453 | ||
d1486f38 | 454 | my $res = route_packet($ruleset, $ipset_ruleset, $pkg, $from_info, $target, $start_state); |
f1bafd37 | 455 | |
d1486f38 | 456 | die "test failed ($res != $action)\n" if $action ne $res; |
f1bafd37 | 457 | |
d1486f38 | 458 | return undef; |
f1bafd37 DM |
459 | } |
460 | ||
461 | sub run_tests { | |
462 | my ($vmdata, $testdir) = @_; | |
463 | ||
464 | $vmdata->{testdir} = $testdir; | |
465 | ||
466 | my ($ruleset, $ipset_ruleset) = | |
467 | PVE::Firewall::compile(undef, undef, $vmdata); | |
468 | ||
469 | my $testfile = "$testdir/tests"; | |
470 | my $fh = IO::File->new($testfile) || | |
471 | die "unable to open '$testfile' - $!\n"; | |
472 | ||
473 | while (defined(my $line = <$fh>)) { | |
474 | next if $line =~ m/^\s*$/; | |
475 | next if $line =~ m/^#.*$/; | |
476 | if ($line =~ m/^\{.*\}\s*$/) { | |
477 | my $test = eval $line; | |
478 | die $@ if $@; | |
479 | $trace = ''; | |
d1486f38 | 480 | print Dumper($ruleset) if $debug; |
f1bafd37 DM |
481 | eval { simulate_firewall($ruleset, $ipset_ruleset, $vmdata, $test); }; |
482 | if (my $err = $@) { | |
483 | ||
d1486f38 | 484 | print Dumper($ruleset) if !$debug; |
f1bafd37 | 485 | |
d1486f38 | 486 | print "$trace\n" if !$debug; |
f1bafd37 DM |
487 | |
488 | print "$testfile line $.: $line"; | |
489 | ||
490 | print "test failed: $err\n"; | |
491 | ||
492 | exit(-1); | |
493 | } | |
494 | } else { | |
495 | die "parse error"; | |
496 | } | |
497 | } | |
498 | ||
499 | print "PASS: $testfile\n"; | |
500 | ||
501 | return undef; | |
502 | } | |
503 | ||
504 | my $vmdata = { | |
505 | qemu => { | |
506 | 100 => { | |
d1486f38 DM |
507 | net0 => "e1000=0E:0B:38:B8:B3:21,bridge=vmbr0", |
508 | }, | |
509 | 101 => { | |
510 | net0 => "e1000=0E:0B:38:B8:B3:22,bridge=vmbr0", | |
511 | }, | |
512 | # on bridge vmbr1 | |
513 | 110 => { | |
514 | net0 => "e1000=0E:0B:38:B8:B4:21,bridge=vmbr1", | |
f1bafd37 DM |
515 | }, |
516 | }, | |
517 | openvz => { | |
518 | 200 => { | |
519 | ip_address => { value => '10.0.200.1' }, | |
520 | }, | |
d1486f38 DM |
521 | 201 => { |
522 | ip_address => { value => '10.0.200.2' }, | |
523 | }, | |
f1bafd37 DM |
524 | }, |
525 | }; | |
526 | ||
527 | foreach my $dir (<test-*>) { | |
528 | next if ! -d $dir; | |
529 | run_tests($vmdata, $dir); | |
530 | } | |
531 | ||
532 | print "OK - all tests passed\n"; | |
533 | ||
534 | exit(0); |