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   77468 use strict;
  1         11  
  1         31  
101 1     1   5 use warnings FATAL => 'all';
  1         2  
  1         33  
102 1     1   568 use Digest::SHA qw(sha1_hex);
  1         3191  
  1         85  
103 1     1   7 use Fcntl qw(:flock);
  1         2  
  1         92  
104 1     1   6 use File::Basename;
  1         2  
  1         62  
105 1     1   6 use experimental 'signatures';
  1         2  
  1         6  
106              
107 1     1   674 use Wireguard::WGmeta::Wrapper::Config;
  1         3  
  1         32  
108 1     1   7 use Wireguard::WGmeta::Parser::Middleware;
  1         2  
  1         50  
109 1     1   6 use Wireguard::WGmeta::Parser::Conf qw(INTERNAL_KEY_PREFIX);
  1         2  
  1         34  
110 1     1   6 use Wireguard::WGmeta::ValidAttributes;
  1         2  
  1         51  
111 1     1   5 use Wireguard::WGmeta::Utils;
  1         2  
  1         51  
112              
113 1     1   415 use parent 'Wireguard::WGmeta::Wrapper::Config';
  1         272  
  1         5  
114              
115 1     1   66 use constant FALSE => 0;
  1         2  
  1         49  
116 1     1   5 use constant TRUE => 1;
  1         3  
  1         41  
117 1     1   6 use constant INTEGRITY_HASH_SALT => 'wefnwioefh9032ur3';
  1         2  
  1         2022  
118              
119             our $VERSION = "0.3.4"; # 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 38 sub is_valid_interface($self, $interface) {
  25         36  
  25         38  
  25         29  
127 25         67 $self->_sync_interfaces();
128 25         129 return $self->SUPER::is_valid_interface($interface);
129             }
130              
131              
132 1     1 1 2 sub is_valid_alias($self, $interface, $alias) {
  1         2  
  1         2  
  1         2  
  1         2  
133 1         4 $self->may_reload_from_disk($interface);
134 1         10 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 44 sub is_valid_identifier($self, $interface, $identifier) {
  24         38  
  24         33  
  24         33  
  24         31  
143 24         64 $self->may_reload_from_disk($interface);
144 24         77 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 31 sub try_translate_alias($self, $interface, $may_alias) {
  21         31  
  21         34  
  21         30  
  21         28  
153 21         62 $self->may_reload_from_disk($interface);
154 21         72 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 13 sub get_interface_section($self, $interface, $identifier) {
  2         5  
  2         4  
  2         4  
  2         3  
163 2         8 $self->may_reload_from_disk($interface);
164 2 50       8 if (exists $self->{parsed_config}{$interface}{$identifier}) {
165 2         4 my %r = %{$self->{parsed_config}{$interface}{$identifier}};
  2         15  
166 2         18 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         3  
  2         4  
  2         4  
179 2         8 $self->may_reload_from_disk($interface);
180 2         13 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   72 sub _get_all_conf_files($wireguard_home) {
  27         45  
  27         32  
194 27         154 my @config_files = read_dir($wireguard_home, qr/.*\.conf$/);
195 27 50       94 if (@config_files == 0) {
196 0         0 die "No matching interface configuration(s) in " . $wireguard_home;
197             }
198 27         48 my $count = @config_files;
199 27         87 return \@config_files, $count;
200             }
201              
202             =head3 get_interface_list()
203              
204             L
205              
206             =cut
207 2     2 1 8 sub get_interface_list($self) {
  2         5  
  2         3  
208 2         6 $self->_sync_interfaces();
209             # $self->may_reload_from_disk();
210 2         7 return sort keys %{$self->{parsed_config}};
  2         54  
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 30894 sub commit($self, $is_hot_config = FALSE, $plain = FALSE, $ref_hash_integrity_keys = undef) {
  11         20  
  11         20  
  11         17  
  11         18  
  11         16  
253 11         15 for my $interface_name (keys %{$self->{parsed_config}}) {
  11         54  
254 25 100       79 if ($self->_has_changed($interface_name)) {
255 11         17 my $file_name;
256 11 50       27 if ($is_hot_config == TRUE) {
257 11         32 $file_name = $self->{wireguard_home} . $interface_name . '.conf';
258 11         24 $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         18 my $on_disk_config = undef;
265 11         18 my $is_new = undef;
266              
267             # --- From here we lock the affected configuration file exclusively ----
268 11         16 my $fh;
269             # check if interface exists - if not, we have a new interface
270 11 100       255 if (-e $file_name) {
271              
272             # in this case open the file for RW
273 10 50       442 open $fh, '+<', $file_name or die "Could not open $file_name: $!";
274 10         97 flock $fh, LOCK_EX;
275 10         53 my $config_contents = read_file($fh, TRUE);
276 10         44 $on_disk_config = parse_wg_config2($config_contents, $interface_name, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix});
277 10         113 seek $fh, 0, 0;
278             }
279             else {
280 1         95 open $fh, '>', $file_name;
281 1         37 flock $fh, LOCK_EX;
282 1         5 $is_new = 1;
283             }
284              
285 11         46 $self->_sync_changes(
286             $interface_name,
287             $on_disk_config,
288             $ref_hash_integrity_keys
289             );
290             # write down to file
291 10         449 truncate $fh, 0;
292 10         63 print $fh create_wg_config2($self->{parsed_config}{$interface_name});
293 10         28 $self->{parsed_config}{$interface_name}{mtime} = get_mtime($file_name);
294 10 100       31 $self->{n_conf_files}++ if (defined $is_new);
295 10         47 $self->_reset_changed($interface_name);
296             # Close file handle before calling reload callbacks, otherwise the exclusive lock is kept!
297 10         867 close $fh;
298             # Notify listeners about a file change
299 10         70 $self->_call_reload_listeners($interface_name);
300             }
301             }
302             }
303              
304 11     11   17 sub _sync_changes($self, $interface, $ref_on_disk_config = undef, $ref_hash_integrity_keys = undef) {
  11         18  
  11         19  
  11         16  
  11         15  
  11         16  
305              
306             # first, we look for sections which are common (disk and internal), then we search for exclusive ones
307 11         25 my @may_conflict;
308             my @exclusive_disk;
309 11         0 my @exclusive_internal;
310 11 100       23 if (defined $ref_on_disk_config) {
311 10         16 for my $identifier_internal (@{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}) {
  10         36  
312 33 100       63 if (exists $ref_on_disk_config->{$identifier_internal}) {
313 29         52 push @may_conflict, $identifier_internal;
314             }
315             else {
316 4         10 push @exclusive_internal, $identifier_internal;
317             }
318             }
319 10         19 for my $identifier_ondisk (@{$ref_on_disk_config->{INTERNAL_KEY_PREFIX . 'section_order'}}) {
  10         17  
320 31 100       86 unless (exists $self->{parsed_config}{$interface}{$identifier_ondisk}) {
321             # if we have the latest data, we can safely assume the peer has been deleted
322 2 100       6 if (!$self->_is_latest_data($interface)) {
323 1         5 push @exclusive_disk, $identifier_ondisk;
324             }
325             }
326             }
327             }
328             else {
329             # if no on-disk reference is provided all sections are considered as exclusive internal
330 1         2 @exclusive_internal = @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}};
  1         4  
331             }
332              
333 11         21 for my $identifier (@may_conflict) {
334             # if the shas differ, the configuration on disk had been changed in the mean time
335 27         60 my $on_disk_sha = _calculate_sha1_from_section($ref_on_disk_config->{$identifier});
336 27         73 my $internal_sha = _calculate_sha1_from_section($self->{parsed_config}{$interface}{$identifier});
337              
338             # 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.
339 27 100       85 if ($on_disk_sha ne $internal_sha) {
340              
341             # we may have a integrity hash from this section which allows us to modify
342 6 100 100     26 if (defined $ref_hash_integrity_keys && exists $ref_hash_integrity_keys->{$identifier}) {
343              
344             # if the on-disk sha differs from our integrity hash, this section has been changed by an other process or user.
345 4 100       13 if ($on_disk_sha ne $ref_hash_integrity_keys->{$identifier}) {
346 1         43 die "your changes for `$identifier` were not applied";
347             }
348             }
349             else {
350             # take from disk (we have no integrity key for this section)
351 2         9 $self->{parsed_config}{$interface}{$identifier} = $ref_on_disk_config->{$identifier};
352             }
353             }
354             else {
355             # take from disk
356             #$self->{parsed_config}{$identifier} = $ref_on_disk_config->{$identifier};
357             }
358              
359             }
360             # exclusive mode
361 10         31 for my $key (@exclusive_disk) {
362 1         3 $self->{parsed_config}{$interface}{$key} = $ref_on_disk_config->{$key};
363 1         4 push @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}, $key;
  1         4  
364             }
365             }
366              
367              
368             =head3 may_reload_from_disk([$interface = undef])
369              
370             This method is called before any data is returned from one of the C methods. It behaves as follows:
371              
372             =over 1
373              
374             =item *
375              
376             If the interface is not defined, it loops through the known interfaces and reloads them individually (if needed).
377              
378             =item *
379              
380             If the interface is defined (and known), the modify timestamps are compared an if the on-disk version is newer, a reload is triggered.
381              
382             =item *
383              
384             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
385             actually a matching config file on disk and if yes, its loaded and parsed from disk.
386              
387             =back
388              
389             Remark: This method is not meant for public access, there is just this extensive documentation block since its behaviour
390             is crucial to the function of this class.
391              
392             B
393              
394             =over 1
395              
396             =item
397              
398             C<$interface> A (possibly) invalid (or new) interface name
399              
400             =back
401              
402             B
403              
404             None
405              
406             =cut
407             # sub may_reload_from_disk($self, $interface = undef) {
408             # unless (defined $interface) {
409             # for my $known_interface (keys %{$self->{parsed_config}}) {
410             # # my $s = $self->_get_my_mtime($known_interface);
411             # # my $t = get_mtime($self->{parsed_config}{$known_interface}{config_path});
412             # if ($self->_get_my_mtime($known_interface) < get_mtime($self->{parsed_config}{$known_interface}{config_path})) {
413             # $self->may_reload_from_disk($known_interface);
414             # }
415             # }
416             # }
417             # elsif (exists $self->{parsed_config}{$interface}) {
418             # # my $s = $self->_get_my_mtime($interface);
419             # # my $t = get_mtime($self->{parsed_config}{$interface}{config_path});
420             # if ($self->_get_my_mtime($interface) < get_mtime($self->{parsed_config}{$interface}{config_path})) {
421             # $self->may_reload_from_disk($interface);
422             # }
423             # }
424             # else {
425             # # we may have a new interface added in the meantime so we probe if there is actually a config file first
426             # if (-e $self->{wireguard_home} . $interface . '.conf') {
427             # $self->may_reload_from_disk($interface, TRUE);
428             # }
429             # }
430             #
431             # }
432              
433 2     2   5 sub _get_my_mtime($self, $interface) {
  2         8  
  2         3  
  2         2  
434 2 50       11 if (exists $self->{parsed_config}{$interface}) {
435 2         12 return $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX. 'mtime'};
436             }
437             else {
438 0         0 return 0;
439             }
440             }
441              
442 2     2   4 sub _is_latest_data($self, $interface) {
  2         5  
  2         3  
  2         4  
443 2         9 my $hot_path = $self->{wireguard_home} . $interface . ".conf";
444 2         6 my $safe_path = $self->{wireguard_home} . $interface . $self->{not_applied_suffix};
445 2 50       31 if (-e $safe_path) {
446 0   0     0 return $self->_get_my_mtime($interface) ge get_mtime($hot_path) || $self->_get_my_mtime($interface) ge get_mtime($safe_path);
447             }
448             # my $t = $self->_get_my_mtime($interface);
449             # my $s = get_mtime($conf_path);
450 2         8 return $self->_get_my_mtime($interface) ge get_mtime($hot_path);
451             }
452              
453 27     27   36 sub _sync_interfaces($self) {
  27         36  
  27         43  
454             # check if there's maybe a new interface by comparing the file counts
455 27         63 my ($conf_files, $count) = _get_all_conf_files($self->{wireguard_home});
456 27 50       82 if ($self->{n_conf_files} != $count) {
457 27         40 for my $conf_path (@{$conf_files}) {
  27         58  
458             # read interface name
459 117         3600 my $i_name = basename($conf_path);
460 117         460 $i_name =~ s/\.conf$//;
461 117 100       353 unless (exists $self->{parsed_config}{$i_name}) {
462 55         176 $self->may_reload_from_disk($i_name, TRUE);
463             }
464             }
465             }
466             # scan for deleted interfaces
467 27         48 for my $internal_interface (keys %{$self->{parsed_config}}) {
  27         89  
468 64 100       981 if (not -e $self->{parsed_config}{$internal_interface}{INTERNAL_KEY_PREFIX. 'config_path'}) {
469 1 50       7 warn "Interface `$internal_interface` has been deleted in the meantime" if $self->_has_changed($internal_interface);
470 1         10 delete $self->{parsed_config}{$internal_interface};
471             }
472             }
473             }
474 58     58   114 sub _calculate_sha1_from_section($ref_to_hash) {
  58         80  
  58         82  
475 58         79 my %h = %{$ref_to_hash};
  58         315  
476 58         128 return sha1_hex INTEGRITY_HASH_SALT . join '', map {$h{$_}} @{$ref_to_hash->{INTERNAL_KEY_PREFIX . 'order'}};
  255         747  
  58         105  
477             }
478              
479             =head3 calculate_sha_from_internal($interface, $identifier)
480              
481             Calculates the sha1 from a section (already parsed).
482              
483             B
484              
485             It is possible that this method does not return the most recent, on-disk version of this section! It returns your current
486             parsed state! This method does NOT trigger a C!
487              
488             B
489              
490             =over 1
491              
492             =item
493              
494             C<$interface> A valid interface name
495              
496             =item
497              
498             C<$identifier> A valid identifier for this interface
499              
500             =back
501              
502             B
503              
504             The sha1 (in HEX) the requested section
505              
506             =cut
507 4     4 1 675 sub calculate_sha_from_internal($self, $interface, $identifier) {
  4         7  
  4         7  
  4         7  
  4         7  
508 4 50 33     22 if (exists $self->{parsed_config}{$interface} && exists $self->{parsed_config}{$interface}{$identifier}) {
509 4         11 return _calculate_sha1_from_section($self->{parsed_config}{$interface}{$identifier});
510             }
511             else {
512 0           die "Invalid interface `$interface` or section `$identifier`";
513             }
514              
515             }
516              
517             1;