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