File Coverage

lib/Wireguard/WGmeta/Wrapper/ConfigT.pm
Criterion Covered Total %
statement 212 225 94.2
branch 33 42 78.5
condition 4 9 44.4
subroutine 30 31 96.7
pod 10 10 100.0
total 289 317 91.1


line stmt bran cond sub pod time code
1             =pod
2              
3             =head1 NAME
4              
5             WGmeta::Wrapper::ConfigT - Class for interfacing the wireguard configuration supporting concurrent access
6              
7             =head1 DESCRIPTION
8              
9             Specialized child class of L which is capable of handling concurrent access.
10              
11             =head1 SYNOPSIS
12              
13             The interface is almost identical with the exception
14             of L
15              
16             use Wireguard::WGmeta::Wrapper::ConfigT;
17             my $wg_metaT = Wireguard::WGmeta::Wrapper::ConfigT->new('');
18              
19             =head1 CONCURRENCY
20              
21             To ensure that no inconsistent config files are generated, calls to a C may result in a reload from disk - namely
22             when the config file on disk is newer than the current (parsed) one. So keep in mind to C as soon as possible
23             (this is obviously only true for environments where such situations are possible to occur)
24              
25             # thread/process A
26             $wg_metaT->set('wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'alias', 'A');
27              
28             # thread/process B
29             $wg_metaT->set('wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'alias', 'B');
30             $wg_metaT->commit(1);
31              
32             # thread/process A (alias 'A' is overwritten by 'B')
33             wg_metaT->disable_by_alias('wg0', 'A'); # throws exception `invalid alias`!
34              
35             For more details about the reloading behaviour please refer to L.
36              
37             B
38              
39             FUNCTION commit($integrity_hashes)
40             FOR $interface IN $known_interfaces
41             IF has_changed($interface) THEN
42             lock_exclusive($interface)
43             UNLESS my_config_is_latest THEN
44             $on_disk <- read_from_disk($interface)
45             $contents <- create_wg_config($interface, $on_disk,$integrity_hashes)
46             write($contents)
47              
48             FUNCTION create_wg_config($interface, $on_disk, $integrity_hashes);
49             $may_conflicting <- search_for_common_data($interface, $on_disk)
50             FOR $section IN $may_conflicting
51             $sha_internal <- calculate_sha_from_internal()
52             $sha_disk <- calculate_sha_from_disk()
53             IF $sha_internal NE $sha_disk
54             IF $sha_disk EQ $integrity_hashes[$section]
55             $section_data <- take_from_internal()
56             ELSE
57             $section_data <- take_from_disk()
58             ELSE
59             $section_data <- take_from_disk()
60             $config_content .= create_section($section_data)
61             $config_content .= create_non_conflicting()
62             return $config_content
63              
64             =head1 EXAMPLES
65              
66             use Wireguard::WGmeta::Wrapper::ConfigT;
67              
68             # thread A
69             my $wg_metaT = Wireguard::WGmeta::Wrapper::ConfigT->new('');
70             $wg_metaT->set('wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'name', 'set_in_thread_A');
71             # Assumption: Our internal version is equal with the on-disk version at this point
72             my $integrity_hash = $wg_metaT->calculate_sha_from_internal();
73              
74             # thread B
75             my $wg_metaT = Wireguard::WGmeta::Wrapper::ConfigT->new('');
76             $wg_metaT->set('wg0', 'AN_OTHER_PUBLIC_KEY', 'name', 'set_in_thread_B');
77             $wg_metaT->commit(1); # works fine (internal & on_disk have same version)
78              
79             # thread A (non conflicting changes -> same file, different section)
80             $wg_metaT->commit(1); # "Your changes for `WG_0_PEER_A_PUBLIC_KEY` were not applied"
81             $wg_metaT->commit(1, 0, {'WG_0_PEER_A_PUBLIC_KEY' => $integrity_hash}); # works fine -> non conflicting changes
82              
83             # Reload callbacks
84             sub my_reload_callback($interface, $ref_list_args){
85             my @args = @{$ref_list_args};
86             print "$interface, reloaded and $args[0]!";
87             }
88              
89             # register our callback handler
90             $wg_metaT->register_on_reload_listener(\&my_reload_callback, 'handler_id', [ 'hello from listener' ]);
91              
92             # Everytime an interface is reloaded, our handler is called until we uninstall our handler
93             $wg_metaT->remove_on_reload_listener('handler_id');
94              
95             =head1 METHODS
96              
97             =cut
98              
99             package Wireguard::WGmeta::Wrapper::ConfigT;
100 1     1   87208 use strict;
  1         16  
  1         40  
101 1     1   9 use warnings FATAL => 'all';
  1         2  
  1         53  
102 1     1   586 use Digest::SHA qw(sha1_hex);
  1         3636  
  1         124  
103 1     1   12 use Fcntl qw(:flock);
  1         2  
  1         142  
104 1     1   6 use File::Basename;
  1         3  
  1         80  
105 1     1   8 use experimental 'signatures';
  1         3  
  1         8  
106              
107 1     1   828 use Wireguard::WGmeta::Wrapper::Config;
  1         3  
  1         56  
108 1     1   16 use Wireguard::WGmeta::Parser::Middleware;
  1         2  
  1         108  
109 1     1   7 use Wireguard::WGmeta::Parser::Conf qw(INTERNAL_KEY_PREFIX);
  1         3  
  1         45  
110 1     1   7 use Wireguard::WGmeta::ValidAttributes;
  1         2  
  1         68  
111 1     1   6 use Wireguard::WGmeta::Utils;
  1         3  
  1         62  
112              
113 1     1   638 use parent 'Wireguard::WGmeta::Wrapper::Config';
  1         351  
  1         6  
114              
115 1     1   78 use constant FALSE => 0;
  1         3  
  1         157  
116 1     1   9 use constant TRUE => 1;
  1         3  
  1         61  
117 1     1   7 use constant INTEGRITY_HASH_SALT => 'wefnwioefh9032ur3';
  1         2  
  1         2237  
118              
119             our $VERSION = "0.3.2"; # do not change manually, this variable is updated when calling make
120              
121             =head3 is_valid_interface($interface)
122              
123             L
124              
125             =cut
126 25     25 1 79 sub is_valid_interface($self, $interface) {
  25         42  
  25         39  
  25         35  
127 25         81 $self->_sync_interfaces();
128 25         166 return $self->SUPER::is_valid_interface($interface);
129             }
130              
131              
132 1     1 1 3 sub is_valid_alias($self, $interface, $alias) {
  1         3  
  1         3  
  1         4  
  1         11  
133 1         8 $self->may_reload_from_disk($interface);
134 1         17 return $self->SUPER::is_valid_alias($interface, $alias);
135             }
136              
137             =head3 is_valid_identifier($interface, $identifier)
138              
139             L
140              
141             =cut
142 24     24 1 57 sub is_valid_identifier($self, $interface, $identifier) {
  24         41  
  24         48  
  24         36  
  24         40  
143 24         82 $self->may_reload_from_disk($interface);
144 24         93 return $self->SUPER::is_valid_identifier($interface, $identifier);
145             }
146              
147             =head3 try_translate_alias($interface, $may_alias)
148              
149             L
150              
151             =cut
152 21     21 1 33 sub try_translate_alias($self, $interface, $may_alias) {
  21         35  
  21         33  
  21         31  
  21         31  
153 21         71 $self->may_reload_from_disk($interface);
154 21         92 return $self->SUPER::try_translate_alias($interface, $may_alias);
155             }
156              
157             =head3 get_interface_section($interface, $identifier)
158              
159             L
160              
161             =cut
162 2     2 1 21 sub get_interface_section($self, $interface, $identifier) {
  2         4  
  2         6  
  2         4  
  2         6  
163 2         12 $self->may_reload_from_disk($interface);
164 2 50       12 if (exists $self->{parsed_config}{$interface}{$identifier}) {
165 2         5 my %r = %{$self->{parsed_config}{$interface}{$identifier}};
  2         17  
166 2         25 return %r;
167             }
168             else {
169 0         0 return ();
170             }
171             }
172              
173             =head3 get_section_list($interface)
174              
175             L
176              
177             =cut
178 2     2 1 12 sub get_section_list($self, $interface) {
  2         6  
  2         3  
  2         4  
179 2         8 $self->may_reload_from_disk($interface);
180 2         17 return $self->SUPER::get_section_list($interface);
181             }
182              
183             =head3 get_peer_count([$interface])
184              
185             L
186              
187             =cut
188 0     0 1 0 sub get_peer_count($self, $interface = undef) {
  0         0  
  0         0  
  0         0  
189 0         0 $self->may_reload_from_disk($interface);
190 0         0 return $self->SUPER::get_peer_count($interface);
191             }
192              
193 27     27   38 sub _get_all_conf_files($wireguard_home) {
  27         48  
  27         39  
194 27         201 my @config_files = read_dir($wireguard_home, qr/.*\.conf$/);
195 27 50       111 if (@config_files == 0) {
196 0         0 die "No matching interface configuration(s) in " . $wireguard_home;
197             }
198 27         56 my $count = @config_files;
199 27         94 return \@config_files, $count;
200             }
201              
202             =head3 get_interface_list()
203              
204             L
205              
206             =cut
207 2     2 1 7 sub get_interface_list($self) {
  2         4  
  2         4  
208 2         9 $self->_sync_interfaces();
209             # $self->may_reload_from_disk();
210 2         6 return sort keys %{$self->{parsed_config}};
  2         20  
211             }
212              
213             =head3 commit([$is_hot_config = FALSE, $plain = FALSE, $ref_hash_integrity_keys = undef])
214              
215             Writes down the parsed config to the wireguard configuration folder.
216              
217             B
218              
219             =over 1
220              
221             =item
222              
223             C<[$is_hot_config = FALSE])> If set to TRUE, the existing configuration is overwritten. Otherwise,
224             the suffix '_not_applied' is appended to the filename
225              
226             =item
227              
228             C<[$plain = FALSE])> If set to TRUE, no header is generated
229              
230             =item
231              
232             C<[$ref_hash_integrity_keys = undef])> Reference to a hash of integrity keys. Expected structure:
233              
234             {
235             => 'integrity_hash_of_corresponding_section',
236             => 'integrity_hash_of_corresponding_section'
237             }
238              
239             For a more detailed explanation when this information is needed please refer to L.
240              
241             =back
242              
243             B
244              
245             Exception if: Folder or file is not writeable
246              
247             B
248              
249             None
250              
251             =cut
252 11     11 1 31070 sub commit($self, $is_hot_config = FALSE, $plain = FALSE, $ref_hash_integrity_keys = undef) {
  11         37  
  11         22  
  11         27  
  11         22  
  11         25  
253 11         23 for my $interface_name (keys %{$self->{parsed_config}}) {
  11         95  
254 25 100       131 if ($self->_has_changed($interface_name)) {
255 11         25 my $file_name;
256 11 50       44 if ($is_hot_config == TRUE) {
257 11         64 $file_name = $self->{wireguard_home} . $interface_name . '.conf';
258 11         42 $self->{parsed_config}->{$interface_name}{is_hot_config} = 1;
259             }
260             else {
261 0         0 $file_name = $self->{wireguard_home} . $interface_name . $self->{not_applied_suffix};
262 0         0 $self->{parsed_config}->{$interface_name}{is_hot_config} = 0;
263             }
264 11         20 my $on_disk_config = undef;
265 11         20 my $is_new = undef;
266              
267             # --- From here we lock the affected configuration file exclusively ----
268 11         24 my $fh;
269             # check if interface exists - if not, we have a new interface
270 11 100       449 if (-e $file_name) {
271              
272             # in this case open the file for RW
273 10 50       654 open $fh, '+<', $file_name or die "Could not open $file_name: $!";
274 10         136 flock $fh, LOCK_EX;
275 10         78 my $config_contents = read_file($fh, TRUE);
276 10         76 $on_disk_config = parse_wg_config2($config_contents, $interface_name, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix});
277 10         177 seek $fh, 0, 0;
278             }
279             else {
280 1         94 open $fh, '>', $file_name;
281 1         13 flock $fh, LOCK_EX;
282 1         5 $is_new = 1;
283             }
284              
285 11         92 $self->_sync_changes(
286             $interface_name,
287             $on_disk_config,
288             $ref_hash_integrity_keys
289             );
290             # write down to file
291 10         763 truncate $fh, 0;
292 10         87 print $fh create_wg_config2($self->{parsed_config}{$interface_name});
293 10         45 $self->{parsed_config}{$interface_name}{mtime} = get_mtime($file_name);
294 10 100       36 $self->{n_conf_files}++ if (defined $is_new);
295 10         83 $self->_reset_changed($interface_name);
296             # Notify listeners about a file change
297 10         47 $self->_call_reload_listeners($interface_name);
298 10         1393 close $fh;
299             }
300             }
301             }
302              
303 11     11   20 sub _sync_changes($self, $interface, $ref_on_disk_config = undef, $ref_hash_integrity_keys = undef) {
  11         23  
  11         29  
  11         18  
  11         35  
  11         20  
304              
305             # first, we look for sections which are common (disk and internal), then we search for exclusive ones
306 11         44 my @may_conflict;
307             my @exclusive_disk;
308 11         0 my @exclusive_internal;
309 11 100       39 if (defined $ref_on_disk_config) {
310 10         16 for my $identifier_internal (@{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}) {
  10         50  
311 33 100       79 if (exists $ref_on_disk_config->{$identifier_internal}) {
312 29         69 push @may_conflict, $identifier_internal;
313             }
314             else {
315 4         11 push @exclusive_internal, $identifier_internal;
316             }
317             }
318 10         18 for my $identifier_ondisk (@{$ref_on_disk_config->{INTERNAL_KEY_PREFIX . 'section_order'}}) {
  10         30  
319 31 100       80 unless (exists $self->{parsed_config}{$interface}{$identifier_ondisk}) {
320             # if we have the latest data, we can safely assume the peer has been deleted
321 2 100       11 if (!$self->_is_latest_data($interface)) {
322 1         5 push @exclusive_disk, $identifier_ondisk;
323             }
324             }
325             }
326             }
327             else {
328             # if no on-disk reference is provided all sections are considered as exclusive internal
329 1         3 @exclusive_internal = @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}};
  1         5  
330             }
331              
332 11         32 for my $identifier (@may_conflict) {
333             # if the shas differ, the configuration on disk had been changed in the mean time
334 27         82 my $on_disk_sha = _calculate_sha1_from_section($ref_on_disk_config->{$identifier});
335 27         76 my $internal_sha = _calculate_sha1_from_section($self->{parsed_config}{$interface}{$identifier});
336              
337             # if the shas differ, it means that the we either have not the most recent data or the on-disk version has been changed in the meantime.
338 27 100       103 if ($on_disk_sha ne $internal_sha) {
339              
340             # we may have a integrity hash from this section which allows us to modify
341 6 100 100     38 if (defined $ref_hash_integrity_keys && exists $ref_hash_integrity_keys->{$identifier}) {
342              
343             # if the on-disk sha differs from our integrity hash, this section has been changed by an other process or user.
344 4 100       18 if ($on_disk_sha ne $ref_hash_integrity_keys->{$identifier}) {
345 1         78 die "your changes for `$identifier` were not applied";
346             }
347             }
348             else {
349             # take from disk (we have no integrity key for this section)
350 2         11 $self->{parsed_config}{$interface}{$identifier} = $ref_on_disk_config->{$identifier};
351             }
352             }
353             else {
354             # take from disk
355             #$self->{parsed_config}{$identifier} = $ref_on_disk_config->{$identifier};
356             }
357              
358             }
359             # exclusive mode
360 10         29 for my $key (@exclusive_disk) {
361 1         3 $self->{parsed_config}{$interface}{$key} = $ref_on_disk_config->{$key};
362 1         2 push @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}, $key;
  1         7  
363             }
364             }
365              
366              
367             =head3 may_reload_from_disk([$interface = undef])
368              
369             This method is called before any data is returned from one of the C methods. It behaves as follows:
370              
371             =over 1
372              
373             =item *
374              
375             If the interface is not defined, it loops through the known interfaces and reloads them individually (if needed).
376              
377             =item *
378              
379             If the interface is defined (and known), the modify timestamps are compared an if the on-disk version is newer, a reload is triggered.
380              
381             =item *
382              
383             If the interface is defined (but not known -> this could be the case if a new interface has been added), first we check if there is
384             actually a matching config file on disk and if yes, its loaded and parsed from disk.
385              
386             =back
387              
388             Remark: This method is not meant for public access, there is just this extensive documentation block since its behaviour
389             is crucial to the function of this class.
390              
391             B
392              
393             =over 1
394              
395             =item
396              
397             C<$interface> A (possibly) invalid (or new) interface name
398              
399             =back
400              
401             B
402              
403             None
404              
405             =cut
406             # sub may_reload_from_disk($self, $interface = undef) {
407             # unless (defined $interface) {
408             # for my $known_interface (keys %{$self->{parsed_config}}) {
409             # # my $s = $self->_get_my_mtime($known_interface);
410             # # my $t = get_mtime($self->{parsed_config}{$known_interface}{config_path});
411             # if ($self->_get_my_mtime($known_interface) < get_mtime($self->{parsed_config}{$known_interface}{config_path})) {
412             # $self->may_reload_from_disk($known_interface);
413             # }
414             # }
415             # }
416             # elsif (exists $self->{parsed_config}{$interface}) {
417             # # my $s = $self->_get_my_mtime($interface);
418             # # my $t = get_mtime($self->{parsed_config}{$interface}{config_path});
419             # if ($self->_get_my_mtime($interface) < get_mtime($self->{parsed_config}{$interface}{config_path})) {
420             # $self->may_reload_from_disk($interface);
421             # }
422             # }
423             # else {
424             # # we may have a new interface added in the meantime so we probe if there is actually a config file first
425             # if (-e $self->{wireguard_home} . $interface . '.conf') {
426             # $self->may_reload_from_disk($interface, TRUE);
427             # }
428             # }
429             #
430             # }
431              
432 2     2   4 sub _get_my_mtime($self, $interface) {
  2         4  
  2         3  
  2         5  
433 2 50       8 if (exists $self->{parsed_config}{$interface}) {
434 2         15 return $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX. 'mtime'};
435             }
436             else {
437 0         0 return 0;
438             }
439             }
440              
441 2     2   5 sub _is_latest_data($self, $interface) {
  2         4  
  2         4  
  2         3  
442 2         7 my $hot_path = $self->{wireguard_home} . $interface . ".conf";
443 2         8 my $safe_path = $self->{wireguard_home} . $interface . $self->{not_applied_suffix};
444 2 50       35 if (-e $safe_path) {
445 0   0     0 return $self->_get_my_mtime($interface) ge get_mtime($hot_path) || $self->_get_my_mtime($interface) ge get_mtime($safe_path);
446             }
447             # my $t = $self->_get_my_mtime($interface);
448             # my $s = get_mtime($conf_path);
449 2         12 return $self->_get_my_mtime($interface) ge get_mtime($hot_path);
450             }
451              
452 27     27   42 sub _sync_interfaces($self) {
  27         36  
  27         38  
453             # check if there's maybe a new interface by comparing the file counts
454 27         96 my ($conf_files, $count) = _get_all_conf_files($self->{wireguard_home});
455 27 50       92 if ($self->{n_conf_files} != $count) {
456 27         45 for my $conf_path (@{$conf_files}) {
  27         75  
457             # read interface name
458 117         3900 my $i_name = basename($conf_path);
459 117         504 $i_name =~ s/\.conf$//;
460 117 100       393 unless (exists $self->{parsed_config}{$i_name}) {
461 55         197 $self->may_reload_from_disk($i_name, TRUE);
462             }
463             }
464             }
465             # scan for deleted interfaces
466 27         46 for my $internal_interface (keys %{$self->{parsed_config}}) {
  27         100  
467 64 100       1077 if (not -e $self->{parsed_config}{$internal_interface}{INTERNAL_KEY_PREFIX. 'config_path'}) {
468 1 50       7 warn "Interface `$internal_interface` has been deleted in the meantime" if $self->_has_changed($internal_interface);
469 1         9 delete $self->{parsed_config}{$internal_interface};
470             }
471             }
472             }
473 58     58   87 sub _calculate_sha1_from_section($ref_to_hash) {
  58         88  
  58         88  
474 58         85 my %h = %{$ref_to_hash};
  58         331  
475 58         119 return sha1_hex INTEGRITY_HASH_SALT . join '', map {$h{$_}} @{$ref_to_hash->{INTERNAL_KEY_PREFIX . 'order'}};
  251         925  
  58         116  
476             }
477              
478             =head3 calculate_sha_from_internal($interface, $identifier)
479              
480             Calculates the sha1 from a section (already parsed).
481              
482             B
483              
484             It is possible that this method does not return the most recent, on-disk version of this section! It returns your current
485             parsed state! This method does NOT trigger a C!
486              
487             B
488              
489             =over 1
490              
491             =item
492              
493             C<$interface> A valid interface name
494              
495             =item
496              
497             C<$identifier> A valid identifier for this interface
498              
499             =back
500              
501             B
502              
503             The sha1 (in HEX) the requested section
504              
505             =cut
506 4     4 1 946 sub calculate_sha_from_internal($self, $interface, $identifier) {
  4         8  
  4         11  
  4         8  
  4         7  
507 4 50 33     32 if (exists $self->{parsed_config}{$interface} && exists $self->{parsed_config}{$interface}{$identifier}) {
508 4         16 return _calculate_sha1_from_section($self->{parsed_config}{$interface}{$identifier});
509             }
510             else {
511 0           die "Invalid interface `$interface` or section `$identifier`";
512             }
513              
514             }
515              
516             1;