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