fix #967: source: dest: limit length
[pve-firewall.git] / src / PVE / API2 / Firewall / IPSet.pm
1 package PVE::API2::Firewall::IPSetBase;
2
3 use strict;
4 use warnings;
5 use PVE::Exception qw(raise raise_param_exc);
6 use PVE::JSONSchema qw(get_standard_option);
7
8 use PVE::Firewall;
9
10 use base qw(PVE::RESTHandler);
11
12 my $api_properties = {
13 cidr => {
14 description => "Network/IP specification in CIDR format.",
15 type => 'string', format => 'IPorCIDRorAlias',
16 },
17 name => get_standard_option('ipset-name'),
18 comment => {
19 type => 'string',
20 optional => 1,
21 },
22 nomatch => {
23 type => 'boolean',
24 optional => 1,
25 },
26 };
27
28 sub lock_config {
29 my ($class, $param, $code) = @_;
30
31 die "implement this in subclass";
32 }
33
34 sub load_config {
35 my ($class, $param) = @_;
36
37 die "implement this in subclass";
38
39 #return ($cluster_conf, $fw_conf, $ipset);
40 }
41
42 sub save_config {
43 my ($class, $param, $fw_conf) = @_;
44
45 die "implement this in subclass";
46 }
47
48 sub rule_env {
49 my ($class, $param) = @_;
50
51 die "implement this in subclass";
52 }
53
54 sub save_ipset {
55 my ($class, $param, $fw_conf, $ipset) = @_;
56
57 if (!defined($ipset)) {
58 delete $fw_conf->{ipset}->{$param->{name}};
59 } else {
60 $fw_conf->{ipset}->{$param->{name}} = $ipset;
61 }
62
63 $class->save_config($param, $fw_conf);
64 }
65
66 my $additional_param_hash = {};
67
68 sub additional_parameters {
69 my ($class, $new_value) = @_;
70
71 if (defined($new_value)) {
72 $additional_param_hash->{$class} = $new_value;
73 }
74
75 # return a copy
76 my $copy = {};
77 my $org = $additional_param_hash->{$class} || {};
78 foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; }
79 return $copy;
80 }
81
82 sub register_get_ipset {
83 my ($class) = @_;
84
85 my $properties = $class->additional_parameters();
86
87 $properties->{name} = $api_properties->{name};
88
89 $class->register_method({
90 name => 'get_ipset',
91 path => '',
92 method => 'GET',
93 description => "List IPSet content",
94 permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
95 parameters => {
96 additionalProperties => 0,
97 properties => $properties,
98 },
99 returns => {
100 type => 'array',
101 items => {
102 type => "object",
103 properties => {
104 cidr => {
105 type => 'string',
106 },
107 comment => {
108 type => 'string',
109 optional => 1,
110 },
111 nomatch => {
112 type => 'boolean',
113 optional => 1,
114 },
115 digest => get_standard_option('pve-config-digest', { optional => 0} ),
116 },
117 },
118 links => [ { rel => 'child', href => "{cidr}" } ],
119 },
120 code => sub {
121 my ($param) = @_;
122
123 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
124
125 return PVE::Firewall::copy_list_with_digest($ipset);
126 }});
127 }
128
129 sub register_delete_ipset {
130 my ($class) = @_;
131
132 my $properties = $class->additional_parameters();
133
134 $properties->{name} = get_standard_option('ipset-name');
135
136 $class->register_method({
137 name => 'delete_ipset',
138 path => '',
139 method => 'DELETE',
140 description => "Delete IPSet",
141 protected => 1,
142 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
143 parameters => {
144 additionalProperties => 0,
145 properties => $properties,
146 },
147 returns => { type => 'null' },
148 code => sub {
149 my ($param) = @_;
150
151 $class->lock_config($param, sub {
152 my ($param) = @_;
153
154 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
155
156 die "IPSet '$param->{name}' is not empty\n"
157 if scalar(@$ipset);
158
159 $class->save_ipset($param, $fw_conf, undef);
160
161 });
162
163 return undef;
164 }});
165 }
166
167 sub register_create_ip {
168 my ($class) = @_;
169
170 my $properties = $class->additional_parameters();
171
172 $properties->{name} = $api_properties->{name};
173 $properties->{cidr} = $api_properties->{cidr};
174 $properties->{nomatch} = $api_properties->{nomatch};
175 $properties->{comment} = $api_properties->{comment};
176
177 $class->register_method({
178 name => 'create_ip',
179 path => '',
180 method => 'POST',
181 description => "Add IP or Network to IPSet.",
182 protected => 1,
183 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
184 parameters => {
185 additionalProperties => 0,
186 properties => $properties,
187 },
188 returns => { type => "null" },
189 code => sub {
190 my ($param) = @_;
191
192 $class->lock_config($param, sub {
193 my ($param) = @_;
194
195 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
196
197 my $cidr = $param->{cidr};
198 if ($cidr =~ m/^${PVE::Firewall::ip_alias_pattern}$/) {
199 # make sure alias exists (if $cidr is an alias)
200 PVE::Firewall::resolve_alias($cluster_conf, $fw_conf, $cidr);
201 } else {
202 # normalize like config parser, otherwise duplicates might slip through
203 $cidr = PVE::Firewall::parse_ip_or_cidr($cidr);
204 }
205
206 foreach my $entry (@$ipset) {
207 raise_param_exc({ cidr => "address '$cidr' already exists" })
208 if $entry->{cidr} eq $cidr;
209 }
210
211 raise_param_exc({ cidr => "a zero prefix is not allowed in ipset entries" })
212 if $cidr =~ m!/0+$!;
213
214
215 my $data = { cidr => $cidr };
216
217 $data->{nomatch} = 1 if $param->{nomatch};
218 $data->{comment} = $param->{comment} if $param->{comment};
219
220 unshift @$ipset, $data;
221
222 $class->save_ipset($param, $fw_conf, $ipset);
223
224 });
225
226 return undef;
227 }});
228 }
229
230 sub register_read_ip {
231 my ($class) = @_;
232
233 my $properties = $class->additional_parameters();
234
235 $properties->{name} = $api_properties->{name};
236 $properties->{cidr} = $api_properties->{cidr};
237
238 $class->register_method({
239 name => 'read_ip',
240 path => '{cidr}',
241 method => 'GET',
242 description => "Read IP or Network settings from IPSet.",
243 permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
244 protected => 1,
245 parameters => {
246 additionalProperties => 0,
247 properties => $properties,
248 },
249 returns => { type => "object" },
250 code => sub {
251 my ($param) = @_;
252
253 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
254
255 my $list = PVE::Firewall::copy_list_with_digest($ipset);
256
257 foreach my $entry (@$list) {
258 if ($entry->{cidr} eq $param->{cidr}) {
259 return $entry;
260 }
261 }
262
263 raise_param_exc({ cidr => "no such IP/Network" });
264 }});
265 }
266
267 sub register_update_ip {
268 my ($class) = @_;
269
270 my $properties = $class->additional_parameters();
271
272 $properties->{name} = $api_properties->{name};
273 $properties->{cidr} = $api_properties->{cidr};
274 $properties->{nomatch} = $api_properties->{nomatch};
275 $properties->{comment} = $api_properties->{comment};
276 $properties->{digest} = get_standard_option('pve-config-digest');
277
278 $class->register_method({
279 name => 'update_ip',
280 path => '{cidr}',
281 method => 'PUT',
282 description => "Update IP or Network settings",
283 protected => 1,
284 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
285 parameters => {
286 additionalProperties => 0,
287 properties => $properties,
288 },
289 returns => { type => "null" },
290 code => sub {
291 my ($param) = @_;
292
293 my $found = $class->lock_config($param, sub {
294 my ($param) = @_;
295
296 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
297
298 my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
299 PVE::Tools::assert_if_modified($digest, $param->{digest});
300
301 foreach my $entry (@$ipset) {
302 if($entry->{cidr} eq $param->{cidr}) {
303 $entry->{nomatch} = $param->{nomatch};
304 $entry->{comment} = $param->{comment};
305 $class->save_ipset($param, $fw_conf, $ipset);
306 return 1;
307 }
308 }
309
310 return 0;
311 });
312
313 return if $found;
314
315 raise_param_exc({ cidr => "no such IP/Network" });
316 }});
317 }
318
319 sub register_delete_ip {
320 my ($class) = @_;
321
322 my $properties = $class->additional_parameters();
323
324 $properties->{name} = $api_properties->{name};
325 $properties->{cidr} = $api_properties->{cidr};
326 $properties->{digest} = get_standard_option('pve-config-digest');
327
328 $class->register_method({
329 name => 'remove_ip',
330 path => '{cidr}',
331 method => 'DELETE',
332 description => "Remove IP or Network from IPSet.",
333 protected => 1,
334 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
335 parameters => {
336 additionalProperties => 0,
337 properties => $properties,
338 },
339 returns => { type => "null" },
340 code => sub {
341 my ($param) = @_;
342
343 $class->lock_config($param, sub {
344 my ($param) = @_;
345
346 my ($cluster_conf, $fw_conf, $ipset) = $class->load_config($param);
347
348 my (undef, $digest) = PVE::Firewall::copy_list_with_digest($ipset);
349 PVE::Tools::assert_if_modified($digest, $param->{digest});
350
351 my $new = [];
352
353 foreach my $entry (@$ipset) {
354 push @$new, $entry if $entry->{cidr} ne $param->{cidr};
355 }
356
357 $class->save_ipset($param, $fw_conf, $new);
358 });
359
360 return undef;
361 }});
362 }
363
364 sub register_handlers {
365 my ($class) = @_;
366
367 $class->register_delete_ipset();
368 $class->register_get_ipset();
369 $class->register_create_ip();
370 $class->register_read_ip();
371 $class->register_update_ip();
372 $class->register_delete_ip();
373 }
374
375 package PVE::API2::Firewall::ClusterIPset;
376
377 use strict;
378 use warnings;
379
380 use base qw(PVE::API2::Firewall::IPSetBase);
381
382 sub rule_env {
383 my ($class, $param) = @_;
384
385 return 'cluster';
386 }
387
388 sub lock_config {
389 my ($class, $param, $code) = @_;
390
391 PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
392 }
393
394 sub load_config {
395 my ($class, $param) = @_;
396
397 my $fw_conf = PVE::Firewall::load_clusterfw_conf();
398 my $ipset = $fw_conf->{ipset}->{$param->{name}};
399 die "no such IPSet '$param->{name}'\n" if !defined($ipset);
400
401 return (undef, $fw_conf, $ipset);
402 }
403
404 sub save_config {
405 my ($class, $param, $fw_conf) = @_;
406
407 PVE::Firewall::save_clusterfw_conf($fw_conf);
408 }
409
410 __PACKAGE__->register_handlers();
411
412 package PVE::API2::Firewall::VMIPset;
413
414 use strict;
415 use warnings;
416 use PVE::JSONSchema qw(get_standard_option);
417
418 use base qw(PVE::API2::Firewall::IPSetBase);
419
420 sub rule_env {
421 my ($class, $param) = @_;
422
423 return 'vm';
424 }
425
426 __PACKAGE__->additional_parameters({
427 node => get_standard_option('pve-node'),
428 vmid => get_standard_option('pve-vmid'),
429 });
430
431 sub lock_config {
432 my ($class, $param, $code) = @_;
433
434 PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
435 }
436
437 sub load_config {
438 my ($class, $param) = @_;
439
440 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
441 my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid});
442 my $ipset = $fw_conf->{ipset}->{$param->{name}};
443 die "no such IPSet '$param->{name}'\n" if !defined($ipset);
444
445 return ($cluster_conf, $fw_conf, $ipset);
446 }
447
448 sub save_config {
449 my ($class, $param, $fw_conf) = @_;
450
451 PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
452 }
453
454 __PACKAGE__->register_handlers();
455
456 package PVE::API2::Firewall::CTIPset;
457
458 use strict;
459 use warnings;
460 use PVE::JSONSchema qw(get_standard_option);
461
462 use base qw(PVE::API2::Firewall::IPSetBase);
463
464 sub rule_env {
465 my ($class, $param) = @_;
466
467 return 'ct';
468 }
469
470 __PACKAGE__->additional_parameters({
471 node => get_standard_option('pve-node'),
472 vmid => get_standard_option('pve-vmid'),
473 });
474
475 sub lock_config {
476 my ($class, $param, $code) = @_;
477
478 PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
479 }
480
481 sub load_config {
482 my ($class, $param) = @_;
483
484 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
485 my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid});
486 my $ipset = $fw_conf->{ipset}->{$param->{name}};
487 die "no such IPSet '$param->{name}'\n" if !defined($ipset);
488
489 return ($cluster_conf, $fw_conf, $ipset);
490 }
491
492 sub save_config {
493 my ($class, $param, $fw_conf) = @_;
494
495 PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
496 }
497
498 __PACKAGE__->register_handlers();
499
500 package PVE::API2::Firewall::BaseIPSetList;
501
502 use strict;
503 use warnings;
504 use PVE::JSONSchema qw(get_standard_option);
505 use PVE::Exception qw(raise_param_exc);
506 use PVE::Firewall;
507
508 use base qw(PVE::RESTHandler);
509
510 sub lock_config {
511 my ($class, $param, $code) = @_;
512
513 die "implement this in subclass";
514 }
515
516 sub load_config {
517 my ($class, $param) = @_;
518
519 die "implement this in subclass";
520
521 #return ($cluster_conf, $fw_conf);
522 }
523
524 sub save_config {
525 my ($class, $param, $fw_conf) = @_;
526
527 die "implement this in subclass";
528 }
529
530 sub rule_env {
531 my ($class, $param) = @_;
532
533 die "implement this in subclass";
534 }
535
536 my $additional_param_hash_list = {};
537
538 sub additional_parameters {
539 my ($class, $new_value) = @_;
540
541 if (defined($new_value)) {
542 $additional_param_hash_list->{$class} = $new_value;
543 }
544
545 # return a copy
546 my $copy = {};
547 my $org = $additional_param_hash_list->{$class} || {};
548 foreach my $p (keys %$org) { $copy->{$p} = $org->{$p}; }
549 return $copy;
550 }
551
552 my $get_ipset_list = sub {
553 my ($fw_conf) = @_;
554
555 my $res = [];
556 foreach my $name (sort keys %{$fw_conf->{ipset}}) {
557 my $data = {
558 name => $name,
559 };
560 if (my $comment = $fw_conf->{ipset_comments}->{$name}) {
561 $data->{comment} = $comment;
562 }
563 push @$res, $data;
564 }
565
566 my ($list, $digest) = PVE::Firewall::copy_list_with_digest($res);
567
568 return wantarray ? ($list, $digest) : $list;
569 };
570
571 sub register_index {
572 my ($class) = @_;
573
574 my $properties = $class->additional_parameters();
575
576 $class->register_method({
577 name => 'ipset_index',
578 path => '',
579 method => 'GET',
580 description => "List IPSets",
581 permissions => PVE::Firewall::rules_audit_permissions($class->rule_env()),
582 parameters => {
583 additionalProperties => 0,
584 properties => $properties,
585 },
586 returns => {
587 type => 'array',
588 items => {
589 type => "object",
590 properties => {
591 name => get_standard_option('ipset-name'),
592 digest => get_standard_option('pve-config-digest', { optional => 0} ),
593 comment => {
594 type => 'string',
595 optional => 1,
596 }
597 },
598 },
599 links => [ { rel => 'child', href => "{name}" } ],
600 },
601 code => sub {
602 my ($param) = @_;
603
604 my ($cluster_conf, $fw_conf) = $class->load_config($param);
605
606 return &$get_ipset_list($fw_conf);
607 }});
608 }
609
610 sub register_create {
611 my ($class) = @_;
612
613 my $properties = $class->additional_parameters();
614
615 $properties->{name} = get_standard_option('ipset-name');
616
617 $properties->{comment} = { type => 'string', optional => 1 };
618
619 $properties->{digest} = get_standard_option('pve-config-digest');
620
621 $properties->{rename} = get_standard_option('ipset-name', {
622 description => "Rename an existing IPSet. You can set 'rename' to the same value as 'name' to update the 'comment' of an existing IPSet.",
623 optional => 1 });
624
625 $class->register_method({
626 name => 'create_ipset',
627 path => '',
628 method => 'POST',
629 description => "Create new IPSet",
630 protected => 1,
631 permissions => PVE::Firewall::rules_modify_permissions($class->rule_env()),
632 parameters => {
633 additionalProperties => 0,
634 properties => $properties,
635 },
636 returns => { type => 'null' },
637 code => sub {
638 my ($param) = @_;
639
640 $class->lock_config($param, sub {
641 my ($param) = @_;
642
643 my ($cluster_conf, $fw_conf) = $class->load_config($param);
644
645 if ($param->{rename}) {
646 my (undef, $digest) = &$get_ipset_list($fw_conf);
647 PVE::Tools::assert_if_modified($digest, $param->{digest});
648
649 raise_param_exc({ name => "IPSet '$param->{rename}' does not exist" })
650 if !$fw_conf->{ipset}->{$param->{rename}};
651
652 # prevent overwriting existing ipset
653 raise_param_exc({ name => "IPSet '$param->{name}' does already exist"})
654 if $fw_conf->{ipset}->{$param->{name}} &&
655 $param->{name} ne $param->{rename};
656
657 my $data = delete $fw_conf->{ipset}->{$param->{rename}};
658 $fw_conf->{ipset}->{$param->{name}} = $data;
659 if (my $comment = delete $fw_conf->{ipset_comments}->{$param->{rename}}) {
660 $fw_conf->{ipset_comments}->{$param->{name}} = $comment;
661 }
662 $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
663 } else {
664 foreach my $name (keys %{$fw_conf->{ipset}}) {
665 raise_param_exc({ name => "IPSet '$name' already exists" })
666 if $name eq $param->{name};
667 }
668
669 $fw_conf->{ipset}->{$param->{name}} = [];
670 $fw_conf->{ipset_comments}->{$param->{name}} = $param->{comment} if defined($param->{comment});
671 }
672
673 $class->save_config($param, $fw_conf);
674 });
675
676 return undef;
677 }});
678 }
679
680 sub register_handlers {
681 my ($class) = @_;
682
683 $class->register_index();
684 $class->register_create();
685 }
686
687 package PVE::API2::Firewall::ClusterIPSetList;
688
689 use strict;
690 use warnings;
691 use PVE::Firewall;
692
693 use base qw(PVE::API2::Firewall::BaseIPSetList);
694
695 sub rule_env {
696 my ($class, $param) = @_;
697
698 return 'cluster';
699 }
700
701 sub lock_config {
702 my ($class, $param, $code) = @_;
703
704 PVE::Firewall::lock_clusterfw_conf(10, $code, $param);
705 }
706
707 sub load_config {
708 my ($class, $param) = @_;
709
710 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
711 return (undef, $cluster_conf);
712 }
713
714 sub save_config {
715 my ($class, $param, $fw_conf) = @_;
716
717 PVE::Firewall::save_clusterfw_conf($fw_conf);
718 }
719
720 __PACKAGE__->register_handlers();
721
722 __PACKAGE__->register_method ({
723 subclass => "PVE::API2::Firewall::ClusterIPset",
724 path => '{name}',
725 # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
726 fragmentDelimiter => '',
727 });
728
729 package PVE::API2::Firewall::VMIPSetList;
730
731 use strict;
732 use warnings;
733 use PVE::JSONSchema qw(get_standard_option);
734 use PVE::Firewall;
735
736 use base qw(PVE::API2::Firewall::BaseIPSetList);
737
738 __PACKAGE__->additional_parameters({
739 node => get_standard_option('pve-node'),
740 vmid => get_standard_option('pve-vmid'),
741 });
742
743 sub rule_env {
744 my ($class, $param) = @_;
745
746 return 'vm';
747 }
748
749 sub lock_config {
750 my ($class, $param, $code) = @_;
751
752 PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
753 }
754
755 sub load_config {
756 my ($class, $param) = @_;
757
758 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
759 my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'vm', $param->{vmid});
760 return ($cluster_conf, $fw_conf);
761 }
762
763 sub save_config {
764 my ($class, $param, $fw_conf) = @_;
765
766 PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
767 }
768
769 __PACKAGE__->register_handlers();
770
771 __PACKAGE__->register_method ({
772 subclass => "PVE::API2::Firewall::VMIPset",
773 path => '{name}',
774 # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
775 fragmentDelimiter => '',
776 });
777
778 package PVE::API2::Firewall::CTIPSetList;
779
780 use strict;
781 use warnings;
782 use PVE::JSONSchema qw(get_standard_option);
783 use PVE::Firewall;
784
785 use base qw(PVE::API2::Firewall::BaseIPSetList);
786
787 __PACKAGE__->additional_parameters({
788 node => get_standard_option('pve-node'),
789 vmid => get_standard_option('pve-vmid'),
790 });
791
792 sub rule_env {
793 my ($class, $param) = @_;
794
795 return 'ct';
796 }
797
798 sub lock_config {
799 my ($class, $param, $code) = @_;
800
801 PVE::Firewall::lock_vmfw_conf($param->{vmid}, 10, $code, $param);
802 }
803
804 sub load_config {
805 my ($class, $param) = @_;
806
807 my $cluster_conf = PVE::Firewall::load_clusterfw_conf();
808 my $fw_conf = PVE::Firewall::load_vmfw_conf($cluster_conf, 'ct', $param->{vmid});
809 return ($cluster_conf, $fw_conf);
810 }
811
812 sub save_config {
813 my ($class, $param, $fw_conf) = @_;
814
815 PVE::Firewall::save_vmfw_conf($param->{vmid}, $fw_conf);
816 }
817
818 __PACKAGE__->register_handlers();
819
820 __PACKAGE__->register_method ({
821 subclass => "PVE::API2::Firewall::CTIPset",
822 path => '{name}',
823 # set fragment delimiter (no subdirs) - we need that, because CIDR address contain a slash '/'
824 fragmentDelimiter => '',
825 });
826
827 1;