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