diff --git a/.debian/DEBIAN/control b/.debian/DEBIAN/control index e7f92c1..38de25f 100644 --- a/.debian/DEBIAN/control +++ b/.debian/DEBIAN/control @@ -1,5 +1,5 @@ Package: wg-meta -Version: 0.0.3 +Version: 0.3.0 Section: base Priority: optional Architecture: all diff --git a/.gitignore b/.gitignore index 597c61d..edb6579 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ Makefile !*/thirdparty/Makefile.am !*/thirdparty/cpanfile*snapshot !*/.debian/Makefile +cover_db diff --git a/CHANGES b/CHANGES index b661666..615ad24 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,12 @@ +0.3.0 2021-07-03 Tobias Bossert (tobib at cpan.org) +- changed: CLI: Seamless support for custom attributes (set and addpeer) +- added: CLI: Ability to remove peers +- added: CLI: Machine readable output (dump) +- changed: Core: removed any predefined wg-meta attributes (except alias and checksum) +- changed: Core: Rewrote parser architecture to be more general and extendable +- changed: Tests: Are now not dependent on threads any more +- changed: Wrapper: API of set() and add_peer() + 0.2.3 2021-04-15 Tobias Bossert (tobib at cpan.org) - changed: Commit behaviour when set to overwrite = False - changed: Adding custom attributes is now possible in code diff --git a/MANIFEST b/MANIFEST index 07e5476..7d66caf 100644 --- a/MANIFEST +++ b/MANIFEST @@ -2,7 +2,6 @@ AUTHORS bin/wg-meta CHANGES COPYRIGHT -dist.sh lib/Wireguard/WGmeta.pm lib/Wireguard/WGmeta/Cli/Commands/Add.pm lib/Wireguard/WGmeta/Cli/Commands/Apply.pm @@ -10,13 +9,15 @@ lib/Wireguard/WGmeta/Cli/Commands/Command.pm lib/Wireguard/WGmeta/Cli/Commands/Disable.pm lib/Wireguard/WGmeta/Cli/Commands/Enable.pm lib/Wireguard/WGmeta/Cli/Commands/Help.pm +lib/Wireguard/WGmeta/Cli/Commands/Remove.pm lib/Wireguard/WGmeta/Cli/Commands/Set.pm lib/Wireguard/WGmeta/Cli/Commands/Show.pm lib/Wireguard/WGmeta/Cli/Human.pm lib/Wireguard/WGmeta/Cli/Router.pm lib/Wireguard/WGmeta/Cli/TerminalHelpers.pm lib/Wireguard/WGmeta/Index.pod -lib/Wireguard/WGmeta/Parser/Config.pm +lib/Wireguard/WGmeta/Parser/Conf.pm +lib/Wireguard/WGmeta/Parser/Middleware.pm lib/Wireguard/WGmeta/Parser/Show.pm lib/Wireguard/WGmeta/Utils.pm lib/Wireguard/WGmeta/Validator.pm @@ -42,7 +43,8 @@ t/test_data/mini_wg0.conf_skip t/test_data/mini_wg1.conf t/test_data/not_a_wg_config.conf t/test_data/wg_show_dump -t/wrapper_test.t t/wrapper_custom_attrs.t +t/wrapper_secure_mode.t +t/wrapper_test.t VERSION -wg-meta_completions.sh \ No newline at end of file +wg-meta_completions.sh diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP index f5e3df6..08f1ee8 100644 --- a/MANIFEST.SKIP +++ b/MANIFEST.SKIP @@ -14,3 +14,5 @@ cpanfile \.perl-version configure\.ac ^_Deparsed_XSubs\.pm +dist.sh +MANIFEST.bak diff --git a/README.md b/README.md index e430313..35997da 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ sudo dpkg -i wg-meta_X.X.X.deb - `Commands::Set|Enable|Disable` omits the header of the generated configuration files. - Line of code is shown for warnings and errors. - `WG_NO_COLOR`: If defined, the show command does not prettify the output with colors. +- `WG_META_NO_WG`: If defined, no wireguard commands are run ## Usage diff --git a/VERSION b/VERSION index 373f8c6..9325c3c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.3 \ No newline at end of file +0.3.0 \ No newline at end of file diff --git a/bin/wg-meta b/bin/wg-meta index d1ddb52..d30e6cb 100755 --- a/bin/wg-meta +++ b/bin/wg-meta @@ -14,33 +14,51 @@ An you don't know the best yet: It's fully compatible with your existing setup ( Intended to use as command wrapper for the C and C commands. Support for C is enabled by default. Please note that B attributes have to be specified in the `wg set` _syntax_, which means _AllowedIPs_ becomes -allowed-ips and so on. +allowed-ips and so on. Also all custom attributes have written in the exact same way as they were introduced! - sudo wg-meta show + sudo wg-meta show wg0 # output - interface: wg0 - State: UP - ListenPort: 51888 - PublicKey: +qz742hzxD3E5z5QF7VOvleVS1onavQpXBK3NdTh40g= + ●interface: wg0 + private-key: WG_0_PEER_B_PRIVATE_KEY + public-key: wg0d845RRItYcmcEW3i+dqatmja18F2P9ujy+lAtsBM= + listen-port: 51888 + fwmark: off + + ●peer: IPv6_only1 + public-key: WG_0_PEER_A_PUBLIC_KEY + preshared-key: PEER_A-PEER_B-PRESHARED_KEY + allowed-ips: fdc9:281f:04d7:9ee9::1/128 + endpoint: 147.86.207.49:10400 + latest-handshake: >month ago + transfer-rx: 0.26 MiB + transfer-tx: 1.36 MiB + persistent-keepalive: off + + + # Access using peer (note the '+' before 'name' -> we add a previously unseen attribute) + sudo wg-meta set wg0 peer WG_0_PEER_A_PUBLIC_KEY +name Fancy_meta_name - +peer: WG_0_PEER_A_PUBLIC_KEY - Name: testero - Alias: Dual_stack_peer1 - AllowedIPs: fdc9:281f:04d7:9ee9::1/128, 10.0.3.43/32 - endpoint: 147.86.207.49:10400 latest-handshake: >month ago transfer-rx: 0.26 MiB transfer-tx: 1.36 MiB + # Access using alias + sudo wg-meta set wg0 IPv6_only1 +description "Some Desc" - # Access using peer - sudo wg-meta set wg0 peer +qz742hzxD3E5z5QF7VOvleVS1onavQpXBK3NdTh40g= name Fancy_meta_name + # Lets check our newly set attributes + sudo wg-meta show wg0 name description - # Access using alias - sudo wg-meta set wg0 some_alias description "Some Desc" + # output + ●interface: wg0 + name: (none) + description: (none) + + ●peer: IPv6_only1 + name: Fancy_meta_name + description: Some Desc # Disable peer - sudo wg-meta disable wg0 some_alias + sudo wg-meta disable wg0 IPv6_only1 # Enable peer - sudo wg-meta enable wg0 +qz742hzxD3E5z5QF7VOvleVS1onavQpXBK3NdTh40g= + sudo wg-meta enable wg0 WG_0_PEER_A_PUBLIC_KEY # Apply config sudo wg-meta apply wg0 @@ -101,7 +119,7 @@ use experimental 'signatures'; use Wireguard::WGmeta::Cli::Router; use Wireguard::WGmeta::Cli::TerminalHelpers; -our $VERSION = "0.0.0"; +our $VERSION = "0.3.0"; local $SIG{__WARN__} = sub($message) { prettify_message($message, 1); diff --git a/lib/Wireguard/WGmeta.pm b/lib/Wireguard/WGmeta.pm index 644896f..9574e4f 100644 --- a/lib/Wireguard/WGmeta.pm +++ b/lib/Wireguard/WGmeta.pm @@ -66,6 +66,6 @@ use strict; use warnings FATAL => 'all'; package Wireguard::WGmeta; -our $VERSION = "0.0.0"; # Do not change manually +our $VERSION = "0.3.0"; # Do not change manually 1; diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Add.pm b/lib/Wireguard/WGmeta/Cli/Commands/Add.pm index c1876d8..218d2da 100644 --- a/lib/Wireguard/WGmeta/Cli/Commands/Add.pm +++ b/lib/Wireguard/WGmeta/Cli/Commands/Add.pm @@ -7,6 +7,8 @@ use experimental 'signatures'; use parent 'Wireguard::WGmeta::Cli::Commands::Command'; use Wireguard::WGmeta::Wrapper::Bridge; +use Wireguard::WGmeta::ValidAttributes; +use Wireguard::WGmeta::Parser::Middleware; sub entry_point($self) { if ($self->_retrieve_or_die($self->{input_args}, 0) eq 'help') { @@ -16,53 +18,76 @@ sub entry_point($self) { # read input parameters my $len = @{$self->{input_args}}; $self->{interface} = $self->_retrieve_or_die($self->{input_args}, 0); - $self->{name} = $self->_retrieve_or_die($self->{input_args}, 1); - $self->{ips} = $self->_retrieve_or_die($self->{input_args}, 2); - if ($len > 3) { - $self->{alias} = $self->_retrieve_or_die($self->{input_args}, 3); + $self->{ips} = $self->_retrieve_or_die($self->{input_args}, 1); + if ($len > 2) { + # We hav additional arguments + my @additional_args = @{$self->{input_args}}[2 .. $len - 1]; + die 'Uneven number of elements (one pair would be without value!)' if scalar @additional_args % 2 != 0; + $self->{additional_args} = \@additional_args; } # generate private/public keypair my ($privkey, $pubkey) = gen_keypair(); $self->{pub_key} = $pubkey; $self->{priv_key} = $privkey; - # would be very nice if we can set a type hint here...possible? - $self->{'wg_meta'} = Wireguard::WGmeta::Wrapper::Config->new($self->{wireguard_home}); $self->_run_command(); } sub _run_command($self) { - my ($iface_privkey, $iface_listen) = $self->{wg_meta}->add_peer( + my ($iface_privkey, $iface_listen) = $self->wg_meta->add_peer( $self->{interface}, - $self->{name}, $self->{ips}, - $self->{pub_key}, - $self->{alias} + $self->{pub_key} ); - # get pubkey of iface priv-key my $iface_pubkey = get_pub_key($iface_privkey); - my $iface_fqdn = $self->{wg_meta}->get_interface_fqdn($self->{interface}); - print "# generated by wg-meta -[Interface] -#+Name = $self->{name} -Address = $self->{ips} -ListenPort = 44544 -PrivateKey = $self->{priv_key} -[Peer] -PublicKey = $iface_pubkey -AllowedIPs = 0.0.0.0/0, ::/0 -Endpoint = $iface_fqdn:$iface_listen -PersistentKeepalive = 25 -"; + # lets create a temporary interface + $self->wg_meta->add_interface('temp', $self->{ips}, 44544, $self->{priv_key}); + $self->wg_meta->add_peer('temp', '0.0.0.0/0, ::/0', $iface_pubkey); + $self->wg_meta->set('temp', $iface_pubkey, 'endpoint', "insert.valid.fqdn.not.valid:$iface_listen"); + $self->wg_meta->set('temp', $iface_pubkey, 'persistent-keepalive', 25); + + my $unknown_handler_temp = sub($attribute, $value) { + my $prefix = substr $attribute, 0, 1; + $prefix eq '+' ? return (substr $attribute, 1), $value : return $attribute, $value; + }; + + if (defined $self->{additional_args}) { + my @additional_args = @{$self->{additional_args}}; + for (my $i = 0; $i < scalar @additional_args; $i += 2) { + my $attribute = $additional_args[$i]; + my $value = $additional_args[$i + 1]; + my $attr_type = get_attr_type($attribute); + if ($attr_type == ATTR_TYPE_IS_WG_META) { + $self->wg_meta->set($self->{interface}, $self->{pub_key}, $attribute, $value); + $self->wg_meta->set('temp', 'temp', $attribute, $value); + } + elsif ($attr_type == ATTR_TYPE_IS_WG_ORIG_INTERFACE) { + $self->wg_meta->set('temp', 'temp', $attribute, $value); + } + else { + $self->wg_meta->set($self->{interface}, $self->{pub_key}, $attribute, $value, \&Wireguard::WGmeta::Cli::Commands::Command::_unknown_attr_handler); + $self->wg_meta->set('temp', 'temp', $attribute, $value, $unknown_handler_temp) if $attr_type != ATTR_TYPE_IS_WG_ORIG_PEER; + } - $self->{wg_meta}->commit(1); + } + } + + print "#Generated by wg-meta\n" . $self->wg_meta->create_config('temp', 0); + # remove our temp interface again + $self->wg_meta->remove_interface('temp'); + $self->wg_meta->commit(1); } sub cmd_help($self) { - print "Usage: wg-meta addpeer [alias]\n"; + print "Usage: wg-meta addpeer [attr1 value1] [attr2 value2] ...\n\n" + . "Notes: \nAttributes meant to reside in the [Interface] section are only applied to the peer's interface\n" + . "wg-meta attributes are applied to the host's peer config and the client interface config\n" + . "and finally, attributes meant to be in the [Peer] section are only applied to the host's peer entry\n\n" + . "Do not forget to reload the configuration afterwards!\n"; + exit(); } diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Apply.pm b/lib/Wireguard/WGmeta/Cli/Commands/Apply.pm index 829ab99..9eefabd 100644 --- a/lib/Wireguard/WGmeta/Cli/Commands/Apply.pm +++ b/lib/Wireguard/WGmeta/Cli/Commands/Apply.pm @@ -17,12 +17,16 @@ sub entry_point($self) { $self->_run_command(); } -sub _run_command($self){ +sub _run_command($self) { my $interface = $self->_retrieve_or_die($self->{input_args}, 0); # please note that there ar potential problems with this commend as mentioned here: https://github.com/WireGuard/wireguard-tools/pull/3 my $cmd_line = "su -c 'wg syncconf $interface <(wg-quick strip $interface)'"; - run_external($cmd_line); + unless (defined $ENV{WGmeta_NO_WG}) { + run_external($cmd_line); + } else { + print "Faked apply \n"; + } } sub cmd_help($self) { diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Command.pm b/lib/Wireguard/WGmeta/Cli/Commands/Command.pm index 310cce9..1c6611f 100644 --- a/lib/Wireguard/WGmeta/Cli/Commands/Command.pm +++ b/lib/Wireguard/WGmeta/Cli/Commands/Command.pm @@ -74,10 +74,20 @@ sub new($class, @input_arguments) { else { $self->{wireguard_home} = WIREGUARD_HOME; } + $self->{wg_meta} = undef; bless $self, $class; return $self; } +#@returns Wireguard::WGmeta::Wrapper::Config +sub wg_meta($self) { + # (sort of) singleton + unless (defined $self->{wg_meta}){ + $self->{wg_meta} = Wireguard::WGmeta::Wrapper::Config->new($self->{wireguard_home}); + } + return $self->{wg_meta}; +} + =head2 entry_point() Method called from L @@ -109,7 +119,7 @@ Exception if the user has insufficient privileges . sub check_privileges($self) { if (not -w $self->{wireguard_home}) { my $username = getpwuid($<); - die "Insufficient privileges - `$username` has rw no permissions to `$self->{wireguard_home}`. You probably forgot `sudo`"; + die "Insufficient privileges - `$username` has no rw permissions to `$self->{wireguard_home}`. You probably forgot `sudo`"; } } @@ -118,4 +128,10 @@ sub _retrieve_or_die($self, $ref_array, $idx) { eval {return $arr[$idx]} or $self->cmd_help(); } +sub _unknown_attr_handler($attribute, $value) { + my $prefix = substr $attribute, 0, 1; + die "`$attribute` is unknown, please add `+` as prefix to add it" unless $prefix eq '+'; + return (substr $attribute, 1), $value; +} + 1; \ No newline at end of file diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Disable.pm b/lib/Wireguard/WGmeta/Cli/Commands/Disable.pm index 021997f..f556ca9 100644 --- a/lib/Wireguard/WGmeta/Cli/Commands/Disable.pm +++ b/lib/Wireguard/WGmeta/Cli/Commands/Disable.pm @@ -12,8 +12,6 @@ sub entry_point($self) { $self->cmd_help(); } $self->check_privileges(); - # would be very nice if we can set a type hint here...possible? - $self->{'wg_meta'} = Wireguard::WGmeta::Wrapper::Config->new($self->{wireguard_home}); $self->_run_command(); } @@ -21,17 +19,15 @@ sub entry_point($self) { sub _run_command($self) { my $interface = $self->_retrieve_or_die($self->{input_args}, 0); my $identifier = $self->_retrieve_or_die($self->{input_args}, 1); - eval { - $identifier = $self->{wg_meta}->translate_alias($interface, $identifier); - }; - $self->{wg_meta}->disable($interface, $identifier); + $identifier = $self->wg_meta->try_translate_alias($interface, $identifier); + $self->wg_meta->disable($interface, $identifier); if (defined $ENV{IS_TESTING}) { # omit header - $self->{wg_meta}->commit(1, 1); + $self->wg_meta->commit(1, 1); } else { - $self->{wg_meta}->commit(1, 0); + $self->wg_meta->commit(1, 0); } } diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Enable.pm b/lib/Wireguard/WGmeta/Cli/Commands/Enable.pm index c80c8cd..97e5b6a 100644 --- a/lib/Wireguard/WGmeta/Cli/Commands/Enable.pm +++ b/lib/Wireguard/WGmeta/Cli/Commands/Enable.pm @@ -14,8 +14,6 @@ sub entry_point($self) { $self->cmd_help(); } $self->check_privileges(); - # would be very nice if we can set a type hint here...possible? - $self->{'wg_meta'} = Wireguard::WGmeta::Wrapper::Config->new($self->{wireguard_home}); $self->_run_command(); } @@ -23,17 +21,15 @@ sub entry_point($self) { sub _run_command($self){ my $interface = $self->_retrieve_or_die($self->{input_args}, 0); my $identifier = $self->_retrieve_or_die($self->{input_args}, 1); - eval { - $identifier = $self->{wg_meta}->translate_alias($interface, $identifier); - }; - $self->{wg_meta}->enable($interface, $identifier); + $identifier = $self->wg_meta->try_translate_alias($interface, $identifier); + $self->wg_meta->enable($interface, $identifier); if (defined $ENV{IS_TESTING}) { # omit header - $self->{wg_meta}->commit(1, 1); + $self->wg_meta->commit(1, 1); } else { - $self->{wg_meta}->commit(1, 0); + $self->wg_meta->commit(1, 0); } } diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Help.pm b/lib/Wireguard/WGmeta/Cli/Commands/Help.pm index d4fa9e4..be9b457 100644 --- a/lib/Wireguard/WGmeta/Cli/Commands/Help.pm +++ b/lib/Wireguard/WGmeta/Cli/Commands/Help.pm @@ -5,6 +5,12 @@ use experimental 'signatures'; use parent 'Wireguard::WGmeta::Cli::Commands::Command'; +sub new($class){ + my $self = {}; + bless $self, $class; + return $self; +} + sub entry_point($self) { $self->cmd_help(); @@ -18,7 +24,8 @@ sub cmd_help($self) { print "\t set: Sets configuration attributes\n"; print "\t enable: Enables a peer\n"; print "\t disable: Disables a peer\n"; - print "\t addpeer: Adds a (basic) peer and prints the client config to std_out\n"; + print "\t addpeer: Adds a peer and prints the client config to std_out\n"; + print "\t removepeer: Removes a peer\n"; print "\t apply: Just a shorthand for `wg syncconf <(wg-quick strip )`\n"; print "You may pass `help` to any of these subcommands to view their usage\n"; exit(); diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Remove.pm b/lib/Wireguard/WGmeta/Cli/Commands/Remove.pm new file mode 100644 index 0000000..d495037 --- /dev/null +++ b/lib/Wireguard/WGmeta/Cli/Commands/Remove.pm @@ -0,0 +1,40 @@ +package Wireguard::WGmeta::Cli::Commands::Remove; +use strict; +use warnings FATAL => 'all'; +use experimental 'signatures'; + +use Wireguard::WGmeta::Wrapper::Config; +use parent 'Wireguard::WGmeta::Cli::Commands::Command'; + + +sub entry_point($self) { + if ($self->_retrieve_or_die($self->{input_args}, 0) eq 'help') { + $self->cmd_help(); + } + $self->check_privileges(); + $self->_run_command(); +} + + +sub _run_command($self) { + my $interface = $self->_retrieve_or_die($self->{input_args}, 0); + my $identifier = $self->_retrieve_or_die($self->{input_args}, 1); + $identifier = $self->wg_meta->try_translate_alias($interface, $identifier); + $self->wg_meta->remove_peer($interface, $identifier); + + if (defined $ENV{IS_TESTING}) { + # omit header + $self->wg_meta->commit(1, 1); + } + else { + $self->wg_meta->commit(1, 0); + } +} + + +sub cmd_help($self) { + print "Usage: wg-meta removepeer {alias | public-key} \n"; + exit; +} + +1; \ No newline at end of file diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Set.pm b/lib/Wireguard/WGmeta/Cli/Commands/Set.pm index 6bca765..34a1d8e 100644 --- a/lib/Wireguard/WGmeta/Cli/Commands/Set.pm +++ b/lib/Wireguard/WGmeta/Cli/Commands/Set.pm @@ -16,7 +16,6 @@ sub entry_point($self) { else { $self->check_privileges(); # would be very nice if we can set a type hint here...possible? - $self->{'wg_meta'} = Wireguard::WGmeta::Wrapper::Config->new($self->{wireguard_home}); $self->_run_command(); } } @@ -40,10 +39,10 @@ sub _run_command($self) { $self->_apply_change_set($interface, @input_args[$cur_start .. $offset]); if (defined $ENV{IS_TESTING}) { # omit header - $self->{wg_meta}->commit(1, 1); + $self->wg_meta->commit(1, 1); } else { - $self->{wg_meta}->commit(1, 0); + $self->wg_meta->commit(1, 0); } } @@ -59,7 +58,7 @@ sub _apply_change_set($self, $interface, @change_set) { $identifier = $self->_retrieve_or_die(\@change_set, 2); # try to resolve alias - $identifier = $self->{wg_meta}->try_translate_alias($interface, $identifier); + $identifier = $self->wg_meta->try_translate_alias($interface, $identifier); $offset += 2; } @@ -88,18 +87,15 @@ sub _apply_change_set($self, $interface, @change_set) { $self->_set_values($interface, $identifier, \%args); } sub cmd_help($self) { - print "Usage: wg-meta set [attr1 value1] [attr2 value2] [peer {alias|public-key}] [attr1 value1] [attr2 value2] ...\n" + print "Usage: wg-meta set [attr1 value1] [attr2 value2] [peer {alias|public-key}] [attr1 value1] [attr2 value2] ...\n\n" + . "Notes:\n The interface aims to follow the official `wg set` specification\n" + . "To introduce new attributes prefix them with `+`"; } sub _set_values($self, $interface, $identifier, $ref_hash_values) { for my $key (keys %{$ref_hash_values}) { - $self->{wg_meta}->set($interface, $identifier, $key, $ref_hash_values->{$key}, TRUE, \&_forward); + $self->wg_meta->set($interface, $identifier, $key, $ref_hash_values->{$key}, \&Wireguard::WGmeta::Cli::Commands::Command::_unknown_attr_handler); } } -sub _forward($interface, $identifier, $attribute, $value) { - # this is just as stub - print("Forwarded to original wg command: `$attribute = $value`"); -} - 1; diff --git a/lib/Wireguard/WGmeta/Cli/Commands/Show.pm b/lib/Wireguard/WGmeta/Cli/Commands/Show.pm index 0e6ffa5..12de6dc 100644 --- a/lib/Wireguard/WGmeta/Cli/Commands/Show.pm +++ b/lib/Wireguard/WGmeta/Cli/Commands/Show.pm @@ -4,6 +4,7 @@ use warnings FATAL => 'all'; use experimental 'signatures'; use parent 'Wireguard::WGmeta::Cli::Commands::Command'; +use FindBin; use Wireguard::WGmeta::Cli::Human; use Wireguard::WGmeta::Cli::TerminalHelpers; @@ -11,25 +12,38 @@ use Wireguard::WGmeta::Wrapper::Config; use Wireguard::WGmeta::Wrapper::Show; use Wireguard::WGmeta::Wrapper::Bridge; use Wireguard::WGmeta::Utils; -use Wireguard::WGmeta::ValidAttributes; - -use constant TRUE => 1; -use constant FALSE => 0; +use Wireguard::WGmeta::Parser::Conf qw(INTERNAL_KEY_PREFIX); +use Wireguard::WGmeta::ValidAttributes qw(KNOWN_ATTRIBUTES); sub new($class, @input_arguments) { my $self = $class->SUPER::new(@input_arguments); - # list of attributes shown in output. Prefix attributes originating from `wg-show` using `#S#`. - my @attr_list = ( - 'name', - 'alias', + my @default_attr_list_peer = ( 'public-key', + 'preshared-key', 'allowed-ips', - '#S#endpoint', - '#S#latest-handshake', - '#S#transfer-rx', - '#S#transfer-tx', - 'disabled' + 'endpoint', + 'latest-handshake', + 'transfer-rx', + 'transfer-tx', + 'persistent-keepalive', + 'disabled', + 'alias' + ); + + my @default_attr_list_interface = ( + 'private-key', + 'public-key', + 'listen-port', + 'fwmark', + 'disabled', + 'alias' + ); + my %wg_show = ( + 'endpoint' => 1, + 'transfer-rx' => 1, + 'transfer-tx' => 1, + 'latest-handshake' => 1, ); # register attribute converters here (no special prefix needed) @@ -39,8 +53,10 @@ sub new($class, @input_arguments) { 'transfer-tx' => \&bits2human ); - $self->{'attr_converters'} = \%attr_converters; - $self->{'attr_list'} = \@attr_list; + $self->{attr_converters} = \%attr_converters; + $self->{wg_show_lookup} = \%wg_show; + $self->{default_attr_list_peer} = \@default_attr_list_peer; + $self->{default_attr_list_interface} = \@default_attr_list_interface; bless $self, $class; return $self; @@ -48,34 +64,30 @@ sub new($class, @input_arguments) { sub entry_point($self) { $self->check_privileges(); - - # set defaults my $len = @{$self->{input_args}}; + my $is_dump = 0; + my $interface = 'all'; if ($len > 0) { - if ($self->_retrieve_or_die($self->{input_args}, 0) eq 'help') { - $self->cmd_help(); - return + my $first_arg = $self->_retrieve_or_die($self->{input_args}, 0); + return $self->cmd_help() if $first_arg eq 'help'; + $is_dump = 1 if $self->_retrieve_or_die($self->{input_args}, -1) eq 'dump'; + $interface = $first_arg; + if ($len > 1) { + my @requested_attributes = @{$self->{input_args}}[1 .. $len - 1]; + return $self->_run_command($interface, $is_dump, \@requested_attributes); } - if ($self->_retrieve_or_die($self->{input_args}, -1) eq 'dump') { - $self->{human_readable} = FALSE; - if ($len == 2) { - $self->{interface} = $self->_retrieve_or_die($self->{input_args}, 0) - } - $self->_run_command(); - } - $self->{interface} = $self->_retrieve_or_die($self->{input_args}, 0) } - $self->_run_command(); + # Default case + $self->_run_command($interface, $is_dump, undef); } -sub _run_command($self) { - my $wg_meta = Wireguard::WGmeta::Wrapper::Config->new($self->{wireguard_home}); - if (exists $self->{interface} && !$wg_meta->is_valid_interface($self->{interface})) { - die "Invalid interface `$self->{interface}`"; +sub _run_command($self, $interface, $is_dump, $ref_attr_list) { + + if (not $interface eq 'all' and not $self->wg_meta->is_valid_interface($interface)) { + die "Invalid interface `$interface`"; } my $out; if (defined $ENV{IS_TESTING}) { - use FindBin; $out = read_file($FindBin::Bin . '/../t/test_data/wg_show_dump'); } else { @@ -86,94 +98,106 @@ sub _run_command($self) { my $output = ''; my @interface_list; - if (defined($self->{interface})) { - @interface_list = ($self->{interface}); + if (not $interface eq 'all') { + @interface_list = ($interface); } else { - @interface_list = $wg_meta->get_interface_list() + @interface_list = $self->wg_meta->get_interface_list() } - for my $iface (sort @interface_list) { - # interface "header" - print BOLD . "interface: " . RESET . $iface . "\n"; - my %interface = $wg_meta->get_interface_section($iface, $iface); - # Print Interface state - print BOLD . " State: " . RESET . (($wg_show->iface_exists($iface)) ? GREEN . "UP" : RED . "DOWN") . RESET . "\n"; - print BOLD . " ListenPort: " . RESET . $interface{'listen-port'} . "\n"; - #print BOLD . " Address: " . RESET . $interface{'address'} . "\n"; - print BOLD . " FQDN: " . RESET . $wg_meta->get_interface_fqdn($iface) . "\n"; - # try to derive iface public key from privatekey - my $iface_pubkey = do { - local $@; - eval { - get_pub_key($interface{'private-key'}) - } or "could_not_derive_publickey_from_privatekey" - }; - print BOLD . " PublicKey: " . RESET . $iface_pubkey . "\n\n"; - - # Attribute values - for my $identifier ($wg_meta->get_section_list($iface)) { - my %interface_section = $wg_meta->get_interface_section($iface, $identifier); - unless (%interface_section) { - die "Interface `$iface` does not exist"; + my $use_default = defined $ref_attr_list ? 0 : 1; + for my $printed_interface (sort @interface_list) { + my $interface_is_active = $wg_show->iface_exists($printed_interface); + my $state = 0; + for my $identifier ($self->wg_meta->get_section_list($printed_interface)) { + my %wg_show_section = ($interface_is_active) ? $wg_show->get_interface_section($printed_interface, $identifier) : (); + my %config_section = $self->wg_meta->get_interface_section($printed_interface, $identifier); + my $type = $config_section{INTERNAL_KEY_PREFIX . 'type'}; + if ($use_default) { + $ref_attr_list = ($type eq 'Interface') ? $self->{default_attr_list_interface} : $self->{default_attr_list_peer} } - - # skip if type interface - if ($interface_section{type} eq 'Peer') { - my %show_section = $wg_show->get_interface_section($iface, $identifier); - $self->_print_section(\%interface_section, \%show_section); - $output .= "\n"; + if ($is_dump) { + $output .= "$printed_interface " + . $self->_get_dump_line(\%config_section, \%wg_show_section, $ref_attr_list) + . "\t" + . $config_section{INTERNAL_KEY_PREFIX . 'type'} + . "\n"; + } + else { + $identifier = $config_section{'alias'} if exists $config_section{'alias'}; + + # we only show a green dot when the peer appears in the show output + $state = ($interface_is_active and keys %wg_show_section > 1) ? 1 : 0; + my $state_marker = ($state == 1) ? BOLD . GREEN . '●' . RESET : BOLD . RED . '●' . RESET; + $output .= $state_marker . BOLD . lc($type) . ": " . RESET . $identifier . "\n"; + $output .= $self->_get_pretty_line(\%config_section, \%wg_show_section, $ref_attr_list) . "\n"; } + } } + print $output; } -sub _print_section($self, $ref_config_section, $ref_show_section) { - #Disabled state - if (exists $ref_config_section->{disabled}) { - if ($ref_config_section->{disabled} == 1) { - print BOLD . RED . '-' . RESET; + +sub _get_pretty_line($self, $ref_config_section, $ref_show_section, $ref_attr_list) { + my @line; + for my $printed_attribute (@{$ref_attr_list}) { + # skip redundant information + next if $printed_attribute eq 'alias' or $printed_attribute eq 'disabled'; + + # first lets check if we have to look in the show output + my $value = '(none)'; + if (exists $self->{wg_show_lookup}{$printed_attribute}) { + $value = $ref_show_section->{$printed_attribute} if exists $ref_show_section->{$printed_attribute}; } + # any other case else { - print BOLD . GREEN . '+' . RESET; + if (exists $ref_config_section->{$printed_attribute}) { + $value = $ref_config_section->{$printed_attribute} + } + else { + # Check if info maybe available in show output + $value = $ref_show_section->{$printed_attribute} if exists $ref_show_section->{$printed_attribute}; + } } + # apply attr converters if any + $value = &{$self->{attr_converters}{$printed_attribute}}($value) if exists $self->{attr_converters}{$printed_attribute}; + push @line, ' ' . $printed_attribute . ': ' . $value; } - else { - print BOLD . GREEN . '+' . RESET; - } - print BOLD . 'peer:' . RESET . " $ref_config_section->{'public-key'}\n"; - for my $attr (@{$self->{attr_list}}) { - my $attr_copy = $attr; - if ($attr_copy !~ s/^\#S\#//g) { - my $attr_type = decide_attr_type($attr_copy); - # exclude PublicKey and Disabled attrs - unless ($attr_copy eq 'public-key' or $attr_copy eq 'disabled') { - if (defined($ref_config_section) && exists $ref_config_section->{$attr_copy}) { - my $cleaned_attr = get_attr_config($attr_type)->{$attr_copy}{in_config_name}; - print " " . BOLD . $cleaned_attr . ": " . RESET . $ref_config_section->{$attr_copy} . "\n"; - } - } + return join("\n", @line); +} + +sub _get_dump_line($self, $ref_config_section, $ref_show_section, $ref_attr_list) { + my @line; + for my $printed_attribute (@{$ref_attr_list}) { + # first lets check if we have to look in the show output + my $value = ('none'); + if (exists $self->{wg_show_lookup}{$printed_attribute}) { + $value = $ref_show_section->{$printed_attribute} if exists $ref_show_section->{$printed_attribute}; + push @line, $value; } + # any other case else { - # wg_show - if (defined($ref_show_section) && exists $ref_show_section->{$attr_copy}) { - my $cleaned_attr = $ref_show_section->{$attr_copy}; - - # check if a converter function is defined - if (exists $self->{attr_converters}{$attr_copy}) { - $cleaned_attr = $self->{attr_converters}{$attr_copy}($cleaned_attr); - } - if ($ref_show_section->{$attr_copy} ne '(none)') { - print " " . BOLD . $attr_copy . ": " . RESET . $cleaned_attr; - } + if (exists $ref_config_section->{$printed_attribute}) { + $value = $ref_config_section->{$printed_attribute} + } + else { + # Check if info maybe available in show output + $value = $ref_show_section->{$printed_attribute} if exists $ref_show_section->{$printed_attribute}; } + push @line, $value; } } - print "\n\n"; + return join("\t", @line); } sub cmd_help($self) { - print "Usage: wg-meta show {interface} \n" + print "Usage: wg-meta show {interface|all} [attribute1, attribute2, ...] [dump] \n" + . "Notes:\n" + . "A green dot indicates an interface/peer's 'real' state which means that its currently possible\n" + . "to connect to this interface/peer.\n" + . "A red dot on the other hand indicates that its not possible to connect. This could mean not applied changes, \n" + . "a disabled peer or the parent interface is down. \n" } 1; diff --git a/lib/Wireguard/WGmeta/Cli/Human.pm b/lib/Wireguard/WGmeta/Cli/Human.pm index ebdb742..d2cb9fc 100644 --- a/lib/Wireguard/WGmeta/Cli/Human.pm +++ b/lib/Wireguard/WGmeta/Cli/Human.pm @@ -11,6 +11,7 @@ package Wireguard::WGmeta::Cli::Human; use strict; use warnings FATAL => 'all'; use experimental 'signatures'; +use Scalar::Util qw(looks_like_number); use base 'Exporter'; our @EXPORT = qw(disabled2human bits2human return_self timestamp2human); @@ -42,8 +43,12 @@ $n_bits * 1_000_000 . "MiB" =cut sub bits2human($n_bits) { - # this calculation is probably not correct, however, I found no reference on what is actually the unit of the wg show dump... - return sprintf("%.2f %s", $n_bits / 1_000_000, "MiB"); + if (looks_like_number($n_bits)){ + # this calculation is probably not correct, however, I found no reference on what is actually the unit of the wg show dump... + return sprintf("%.2f %s", $n_bits / 1_000_000, "MiB"); + } + return "0.0 MiB"; + } =head3 timestamp2human($timestamp) @@ -66,10 +71,10 @@ A string describing how long ago this timestamp was =cut sub timestamp2human($timestamp) { - my $int_timestamp = int($timestamp); - if ($int_timestamp == 0) { + if (not looks_like_number($timestamp) or $timestamp == 0) { return "never" } + my $int_timestamp = int($timestamp); my $delta = time - $int_timestamp; if ($delta > 2592000) { return ">month ago"; diff --git a/lib/Wireguard/WGmeta/Cli/Router.pm b/lib/Wireguard/WGmeta/Cli/Router.pm index 47fcd4a..43bf73e 100644 --- a/lib/Wireguard/WGmeta/Cli/Router.pm +++ b/lib/Wireguard/WGmeta/Cli/Router.pm @@ -22,11 +22,12 @@ use Wireguard::WGmeta::Cli::Commands::Enable; use Wireguard::WGmeta::Cli::Commands::Disable; use Wireguard::WGmeta::Cli::Commands::Apply; use Wireguard::WGmeta::Cli::Commands::Add; +use Wireguard::WGmeta::Cli::Commands::Remove; use base 'Exporter'; our @EXPORT = qw(route_command); -our $VERSION = "0.0.0"; +our $VERSION = "0.3.0"; =head2 route_command($ref_list_input_args) @@ -82,6 +83,10 @@ sub route_command($ref_list_input_args) { Wireguard::WGmeta::Cli::Commands::Add->new(@cmd_args)->entry_point(); last; }; + /^removepeer$/ && do { + Wireguard::WGmeta::Cli::Commands::Remove->new(@cmd_args)->entry_point(); + last; + }; /^apply$/ && do { Wireguard::WGmeta::Cli::Commands::Apply->new(@cmd_args)->entry_point(); last; diff --git a/lib/Wireguard/WGmeta/Cli/TerminalHelpers.pm b/lib/Wireguard/WGmeta/Cli/TerminalHelpers.pm index 89eea35..7547c96 100644 --- a/lib/Wireguard/WGmeta/Cli/TerminalHelpers.pm +++ b/lib/Wireguard/WGmeta/Cli/TerminalHelpers.pm @@ -3,13 +3,15 @@ use strict; use warnings FATAL => 'all'; use experimental 'signatures'; use base 'Exporter'; -our @EXPORT = qw(prettify_message BOLD RESET GREEN RED YELLOW); +our @EXPORT = qw(prettify_message BOLD RESET GREEN RED YELLOW GREEN_BACKGROUND RED_B); use constant BOLD => (!defined($ENV{'WG_NO_COLOR'})) ? "\e[1m" : ""; use constant RESET => (!defined($ENV{'WG_NO_COLOR'})) ? "\e[0m" : ""; use constant GREEN => (!defined($ENV{'WG_NO_COLOR'})) ? "\e[32m" : ""; use constant RED => (!defined($ENV{'WG_NO_COLOR'})) ? "\e[31m" : ""; use constant YELLOW => (!defined($ENV{'WG_NO_COLOR'})) ? "\e[33m" : ""; +use constant GREEN_BACKGROUND => (!defined($ENV{'WG_NO_COLOR'})) ? "\e[42m" : ""; +use constant RED_B => (!defined($ENV{'WG_NO_COLOR'})) ? "\e[41m" : ""; sub prettify_message($error, $is_warning) { $error =~ s/at.*$//g unless (defined $ENV{IS_TESTING}); diff --git a/lib/Wireguard/WGmeta/Index.pod b/lib/Wireguard/WGmeta/Index.pod index 8cefcb2..ccec978 100644 --- a/lib/Wireguard/WGmeta/Index.pod +++ b/lib/Wireguard/WGmeta/Index.pod @@ -16,11 +16,11 @@ Compatible with your existing setup (no configuration changes needed). =item * -A CLI interface with abilities to I, I, I and I your wireguard config(s). +A CLI interface with abilities to I, I, I, I, I and I your wireguard config(s). =item * -A fancy C output which combines the meta-data, running-config and static-configs +A fancy C output which combines the meta-data, running-config and static-configs. Now also available in a machine readable format =item * @@ -46,6 +46,10 @@ B: Directory containing the Wireguard configuration -> Make sure =item * +B: If defined no actual C commands are run + +=item * + B: When defined, it has the following effects: =over 2 @@ -76,7 +80,7 @@ B: If defined, the show command does not prettify the output with c =item * -L - Wireguard configuration parser (bi-directional) +L - Wireguard configuration parser with support of custom attributes =item * diff --git a/lib/Wireguard/WGmeta/Parser/Conf.pm b/lib/Wireguard/WGmeta/Parser/Conf.pm new file mode 100644 index 0000000..a23a88a --- /dev/null +++ b/lib/Wireguard/WGmeta/Parser/Conf.pm @@ -0,0 +1,202 @@ +=pod + +=head1 NAME + +WGmeta::Parser::Conf - Parser for Wireguard configurations + +=head1 SYNOPSIS + + use Wireguard::WGmeta::Parser::Conf; + use Wireguard::WGmeta::Util; + + # Parse a wireguard configuration file + my $config_contents = read_file('/path/to/config.conf', 'interface_name'); + + # Define callbacks + my $on_every_value_callback = sub($attribute, $value, $is_wg_meta){ + # do you magic + return $attribute, $value; + }; + my $on_every_section_callback = sub($identifier, $section_type, $is_disabled){ + # do you magic + return $identifier; + }; + + # And finally parse the configuration + my $parsed_config = parse_raw_wg_config($config_contents, $on_every_value_callback, $on_every_section_callback); + + +=head1 DESCRIPTION + +Parser for Wireguard I<.conf> files with support for custom attributes. A possible implementation is present in L. + +=head1 METHODS + +=cut + +package Wireguard::WGmeta::Parser::Conf; +use strict; +use warnings FATAL => 'all'; +use experimental 'signatures'; + +use constant INTERNAL_KEY_PREFIX => 'int_'; + +use base 'Exporter'; +our @EXPORT = qw(parse_raw_wg_config INTERNAL_KEY_PREFIX); + +our $VERSION = "0.3.0"; + +=head3 parse_raw_wg_config($file_content, $on_every_value, $on_new_section [, $skip, $wg_meta_prefix, $wg_disabled_prefix]) + +Parses a Wireguard configuration + +=over 1 + +=item * + +C<$file_content> Content of Wireguard configuration. Warning, if have to ensure that its a valid file! + +=item * + +C<$on_every_value> A reference to a callback function, fired at every key/value pair. Expected signature: + + my $on_every_value_callback = sub($attribute, $value, $is_wg_meta){ + # do you magic + return $attribute, $value; + }; + +=item * + +C<$on_new_section> Callback for every section. Expected signature: + + my $on_every_section_callback = sub($identifier, $section_type, $is_disabled){ + # do you magic + return $identifier; + }; + +=item * + +C<[$skip = 0]> When you want to skip some lines at the beginning + +=item * + +C<[$wg_meta_prefix = '#+']> wg-meta prefix. Must start with '#' or ';' + +=item * + +C<[$disabled_prefix = '#-']> disabled prefix. Must start with '#' or ';' + +=back + +B + +A reference to a hash similar as described in L. + +=cut +sub parse_raw_wg_config($file_content, $on_every_value, $on_new_section, $skip = 0, $wg_meta_prefix = '#+', $wg_disabled_prefix = '#-') { + my $IDENT_KEY = ''; + my $IS_ACTIVE_COUNTER = 0; + my $IS_ROOT = 1; + my $SECTION_TYPE = 'Root'; + my $IS_WG_META = 0; + + my $parsed_config = {}; + my @peer_order; + my @root_order; + + my $section_data = {}; + my @section_order; + my $generic_autokey = 0; + my $line_count = 0; + + my $section_handler = sub { + if ($IS_ROOT) { + $parsed_config = $section_data; + $section_data = {}; + } + else { + my $is_disabled = $IS_ACTIVE_COUNTER == 1 ? 1 : 0; + my $identifier = &{$on_new_section}($section_data->{$IDENT_KEY}, $SECTION_TYPE, $is_disabled); + die "`$identifier` is already present" if exists($parsed_config->{$identifier}); + $section_data->{INTERNAL_KEY_PREFIX . 'order'} = [ @section_order ]; + $section_data->{'disabled'} = $is_disabled; + $section_data->{INTERNAL_KEY_PREFIX . 'type'} = $SECTION_TYPE; + $parsed_config->{$identifier} = { %$section_data }; + push @peer_order, $identifier; + $section_data = {}; + } + + @section_order = (); + $IDENT_KEY = 'PublicKey'; + $IS_ACTIVE_COUNTER--; + $IS_ROOT = 0; + }; + + for my $line (split "\n", $file_content) { + $line_count++; + next if $line_count <= $skip; + + # Strip-of any leading or trailing whitespace + $line =~ s/^\s+|\s+$//g; + + if ((substr $line, 0, 2) eq $wg_disabled_prefix) { + $line = substr $line, 2; + $IS_ACTIVE_COUNTER = 2 if $IS_ACTIVE_COUNTER != 1; + } + if ((substr $line, 0, 2) eq $wg_meta_prefix) { + # Also slice-off wg-meta prefixes + $line = substr $line, 2; + $IS_WG_META = 1; + } + else { + $IS_WG_META = 0; + } + + # skip empty lines + next unless $line; + + # Simply decide if we are in an interface or peer section + if ((substr $line, 0, 11) eq '[Interface]') { + &$section_handler(); + $SECTION_TYPE = 'Interface'; + $IDENT_KEY = 'PrivateKey'; + next; + } + if ((substr $line, 0, 6) eq '[Peer]') { + &$section_handler(); + $SECTION_TYPE = 'Peer'; + $IDENT_KEY = 'PublicKey'; + next; + } + my ($definitive_key, $definitive_value, $discard); + unless ((substr $line, 0, 1) eq '#') { + my ($raw_key, $raw_value) = _split_and_trim($line, '='); + ($definitive_key, $definitive_value, $discard) = &$on_every_value($raw_key, $raw_value, $IS_WG_META); + next if $discard == 1; + + # Update identity key if changed + $IDENT_KEY = $definitive_key if $raw_key eq $IDENT_KEY; + } + else { + # Handle "normal" comments + $definitive_key = "comment_$generic_autokey"; + $definitive_value = $line; + } + $section_data->{$definitive_key} = $definitive_value; + $IS_ROOT ? push @root_order, $definitive_key : push @section_order, $definitive_key; + $generic_autokey++; + } + # and finalize + &$section_handler(); + $parsed_config->{INTERNAL_KEY_PREFIX . 'section_order'} = \@peer_order; + $parsed_config->{INTERNAL_KEY_PREFIX . 'root_order'} = \@root_order; + + return $parsed_config; +} + +sub _split_and_trim($line, $separator) { + return map {s/^\s+|\s+$//g; + $_} split $separator, $line, 2; +} + +1; \ No newline at end of file diff --git a/lib/Wireguard/WGmeta/Parser/Config.pm b/lib/Wireguard/WGmeta/Parser/Config.pm deleted file mode 100644 index 7c00f7c..0000000 --- a/lib/Wireguard/WGmeta/Parser/Config.pm +++ /dev/null @@ -1,475 +0,0 @@ -=pod - -=head1 NAME - -WGmeta::Parser::Config - Parser for wireguard configuration files. - -=head1 SYNOPSIS - - use Wireguard::WGmeta::Parser::Config; - - my $content = `cat '', '#+', '#-'); - - # and similarly to transform the parsed config into a wireguard compatible format again - my $interface_config = create_wg_config($hash_parsed_configs->{}, '#+', '#-') - -=head1 DESCRIPTION - -Parser for wireguard configuration files supporting optional wg-meta attributes. - -=head1 METHODS - -=cut -use v5.20.0; - -package Wireguard::WGmeta::Parser::Config; -use strict; -use warnings FATAL => 'all'; - -use experimental 'signatures'; - -use Wireguard::WGmeta::ValidAttributes; -use Wireguard::WGmeta::Utils; - -use base 'Exporter'; -our @EXPORT = qw(parse_wg_config create_wg_config); - -our $VERSION = "0.0.0"; # do not change manually, this variable is updated when calling make - -use constant FALSE => 0; -use constant TRUE => 1; - -# constants for states of the config parser -use constant IS_EMPTY => -1; -use constant IS_COMMENT => 0; -use constant IS_SECTION => 2; -use constant IS_NORMAL => 3; - -=head3 parse_wg_config($config_file_content, $interface_name, $wg_meta_prefix, $disabled_prefix [, $use_checksum]) - -Parses the contents of C<$config_file_content> and returns a hash with the following structure: - - { - 'interface_name' => { - 'section_order' => , - 'alias_map' => , - 'checksum' => , - 'n_peers' => , - 'interface_name' => , - 'a_identifier' => { - 'type' => <'Interface' or 'Peer'>, - 'order' => , - 'attr0' => , - 'attrN' => - }, - 'an_other_identifier => { - [...] - } - } - } - -B - -=over 1 - -=item * - -All attributes listed in L are referenced by their key. This means, if you want for -example access I the key would be I. Any attribute not present in L -is stored (and written back) as they appear in Config. - -=item * -This method can be used as stand-alone together with the corresponding L. - -=item * - -If the section is of type 'Peer' the identifier equals to its public-key, otherwise its the interface name again. - -=item * - -wg-meta attributes are always prefixed with C<$wg_meta_prefix>. - -=item * - -If a section is marked as "disabled", this is represented in the attribute I<$wg_meta_prefix. 'Disabled' >. -However, does only exist if this section has been enabled/disabled once. - -=item * - -To check whether a file is actually a Wireguard interface config, the parser first checks the presence of the string -I<[Interface]>. If not present, the file is skipped (without warning!). And in this case the parser returns undefined! - -=back - -B - -=over 1 - -=item * - -C<$config_file_content> String containing the contents of a Wireguard configuration file. - -=item * - -C<$interface_name> Interface name - -=item * - -C<$wg_meta_prefix> wg-meta prefix. Must start with '#' or ';' - -=item * - -C<$disabled_prefix> disabled prefix. Must start with '#' or ';' - -=item * - -C<[$use_checksum = TRUE]> If set to False, checksum is not checked - - -=back - -B - -An exceptions if: - -=over 1 - -=item * - -If the parser ends up in an invalid state (e.g a section without information). - -=back - -A warning: - -=over 1 - -=item * - -On a checksum mismatch - -=back - -B - -A reference to a hash with the structure described above. Or if the configuration file is not a Wireguard configuration: undef. - -=cut -sub parse_wg_config($config_file_content, $interface_name, $wg_meta_prefix, $disabled_prefix, $use_checksum = TRUE) { - my $regex_friendly_meta_prefix = quotemeta $wg_meta_prefix; - - my $parsed_wg_config = {}; - - return undef unless ($config_file_content =~ /\[Interface\]/); - - my %alias_map; - my $current_state = -1; - my $peer_count = 0; - - # state variables - my $STATE_INIT_DONE = FALSE; - my $STATE_READ_SECTION = FALSE; - my $STATE_READ_ID = FALSE; - my $STATE_EMPTY_SECTION = TRUE; - my $STATE_READ_ALIAS = FALSE; - - # data of current section - my $section_type; - my $is_disabled = FALSE; - my $comment_counter = 0; - my $identifier; - my $alias; - my $section_data = {}; - my $checksum = ''; - my @section_data_order; - my @section_order; - - for my $line (split "\n", $config_file_content) { - $current_state = _decide_state($line, $wg_meta_prefix, $disabled_prefix); - - # remove disabled prefix if any - $line =~ s/^$disabled_prefix//g; - - if ($current_state == IS_EMPTY) { - # empty line - } - elsif ($current_state == IS_SECTION) { - # strip-off [] and whitespaces - $line =~ s/^\[|\]\s*$//g; - if (_is_valid_section($line)) { - if ($STATE_EMPTY_SECTION == TRUE && $STATE_INIT_DONE == TRUE) { - die 'Found empty section, aborting'; - } - else { - $STATE_READ_SECTION = TRUE; - - if ($STATE_INIT_DONE == TRUE) { - # we are at the end of a section and therefore we can store the data - - # first check if we read an private or public-key - if ($STATE_READ_ID == FALSE) { - die 'Section without identifying information found (Private -or PublicKey field)' - } - else { - $STATE_READ_ID = FALSE; - $STATE_EMPTY_SECTION = TRUE; - $parsed_wg_config->{$identifier} = $section_data; - $parsed_wg_config->{$identifier}{type} = $section_type; - - # update peer count if we have type [Peer] - $peer_count++ if ($section_type eq 'Peer'); - - # we have to use a copy of the array here - otherwise the reference stays the same in all sections. - $parsed_wg_config->{$identifier}{order} = [ @section_data_order ]; - push @section_order, $identifier; - - # reset vars - $section_data = {}; - $is_disabled = FALSE; - @section_data_order = (); - $section_type = $line; - if ($STATE_READ_ALIAS == TRUE) { - $alias_map{$alias} = $identifier; - $STATE_READ_ALIAS = FALSE; - } - } - } - $section_type = $line; - $STATE_INIT_DONE = TRUE; - } - } - else { - die "Invalid section found: $line"; - } - } - # skip comments before sections -> we replace these with our header anyways... - elsif ($current_state == IS_COMMENT) { - unless ($STATE_INIT_DONE == FALSE) { - my $comment_id = "comment_" . $comment_counter++; - push @section_data_order, $comment_id; - - $line =~ s/^\s+|\s+$//g; - $section_data->{$comment_id} = $line; - } - } - elsif ($current_state == ATTR_TYPE_IS_WG_META) { - # a special wg-meta attribute - if ($STATE_INIT_DONE == FALSE) { - # this is already a wg-meta config and therefore we expect a checksum - (undef, $checksum) = split_and_trim($line, "="); - } - else { - if ($STATE_READ_SECTION == TRUE) { - $STATE_EMPTY_SECTION = FALSE; - my ($attr_name, $attr_value) = split_and_trim($line, "="); - - # remove wg-meta prefix - $attr_name =~ s/^$regex_friendly_meta_prefix//g; - $attr_name = _attr_to_internal_name($attr_name); - if ($attr_name eq 'alias') { - if (exists $alias_map{$attr_value}) { - die "Alias '$attr_value' already exists, aborting"; - } - $STATE_READ_ALIAS = TRUE; - $alias = $attr_value; - } - push @section_data_order, $attr_name; - $section_data->{$attr_name} = $attr_value; - } - else { - die 'Attribute without a section encountered, aborting'; - } - } - } - else { - # normal attribute - if ($STATE_READ_SECTION == TRUE) { - $STATE_EMPTY_SECTION = FALSE; - my ($attr_name, $attr_value) = split_and_trim($line, '='); - $attr_name = _attr_to_internal_name($attr_name); - if (_is_identifying($attr_name)) { - $STATE_READ_ID = TRUE; - if ($section_type eq 'Interface') { - $identifier = $interface_name; - } - else { - $identifier = $attr_value; - } - - } - push @section_data_order, $attr_name; - $section_data->{$attr_name} = $attr_value; - } - else { - die 'Attribute without a section encountered, aborting'; - } - } - } - # store last section - if ($STATE_READ_ID == FALSE) { - die "Section without identifying information found (Private -or PublicKey field) in config file `$interface_name`"; - } - else { - $peer_count++ if ($section_type eq 'Peer'); - $parsed_wg_config->{$identifier} = $section_data; - $parsed_wg_config->{$identifier}{type} = $section_type; - $parsed_wg_config->{checksum} = $checksum; - $parsed_wg_config->{section_order} = \@section_order; - $parsed_wg_config->{alias_map} = \%alias_map; - $parsed_wg_config->{n_peers} = $peer_count; - $parsed_wg_config->{interface_name} = $interface_name; - - $parsed_wg_config->{$identifier}{order} = \@section_data_order; - push @section_order, $identifier; - if ($STATE_READ_ALIAS == TRUE) { - $alias_map{$alias} = $identifier; - } - } - # checksum - unless ($use_checksum == FALSE) { - my $current_hash = compute_md5_checksum(create_wg_config($parsed_wg_config, $wg_meta_prefix, $disabled_prefix, TRUE)); - if ($checksum ne '' && "$current_hash" ne $checksum) { - warn "Config `$interface_name.conf` has been changed by an other program or user. This is just a warning."; - } - } - - return $parsed_wg_config; -} - -# internal method to decide that current state using a line of input -sub _decide_state($line, $comment_prefix, $disabled_prefix) { - #remove leading white space - $line =~ s/^\s+//; - for ($line) { - $_ eq '' && return IS_EMPTY; - (substr $_, 0, 1) eq '[' && return IS_SECTION; - /^\Q${comment_prefix}/ && return ATTR_TYPE_IS_WG_META; - /^\Q${disabled_prefix}/ && do { - $line =~ s/^$disabled_prefix//g; - # lets do a little bit of recursion here ;) - return _decide_state($line, $comment_prefix, $disabled_prefix); - }; - (substr $_, 0, 1) eq '#' && return IS_COMMENT; - return IS_NORMAL; - } -} - -# internal method to whether a section has a valid type -sub _is_valid_section($section) { - return { - Peer => 1, - Interface => 1 - }->{$section}; -} - -# internal method to check if an attribute fulfills identifying properties -sub _is_identifying($attr_name) { - return { - 'private-key' => 1, - 'public-key' => 1 - }->{$attr_name}; -} - -sub _attr_to_internal_name($attr_name) { - if (exists Wireguard::WGmeta::ValidAttributes::NAME_2_KEYS_MAPPING->{$attr_name}) { - return Wireguard::WGmeta::ValidAttributes::NAME_2_KEYS_MAPPING->{$attr_name}; - } - else { - return $attr_name; - } -} - -# internal method to check if a section is disabled -sub _is_disabled($ref_parsed_config_section) { - if (exists $ref_parsed_config_section->{disabled}) { - return $ref_parsed_config_section->{disabled} == TRUE; - } - return FALSE; -} - -=head3 create_wg_config($ref_interface_config, $wg_meta_prefix, $disabled_prefix [, $plain = FALSE]) - -Turns a reference of interface-config hash (just a single interface!) back into a wireguard config. - -B - -=over 1 - -=item * - -C<$ref_interface_config> Reference to hash containing B interface config. - -=item * - -C<$wg_meta_prefix> Has to start with a '#' or ';' character and is ideally the -same as in L - -=item * - -C<$wg_meta_prefix> Same restrictions as parameter C<$wg_meta_prefix> - -=item * - -C<[, $plain = FALSE]> If set to true, no header is added (useful for checksum calculation) - -=back - -B - -A string, ready to be written down as a config file. - -=cut -sub create_wg_config($ref_interface_config, $wg_meta_prefix, $disabled_prefix, $plain = FALSE) { - my $new_config = ""; - - for my $identifier (@{$ref_interface_config->{section_order}}) { - if (_is_disabled($ref_interface_config->{$identifier})) { - $new_config .= $disabled_prefix; - } - # write down [section_type] - $new_config .= "[$ref_interface_config->{$identifier}{type}]\n"; - for my $attr_name (@{$ref_interface_config->{$identifier}{order}}) { - if (_is_disabled($ref_interface_config->{$identifier})) { - $new_config .= $disabled_prefix; - } - if (substr($attr_name, 0, 7) eq 'comment') { - $new_config .= $ref_interface_config->{$identifier}{$attr_name} . "\n"; - } - else { - my $attr_type = decide_attr_type($attr_name, TRUE); - my $meta_prefix = ''; - if ($attr_type == ATTR_TYPE_IS_WG_META_CUSTOM || $attr_type == ATTR_TYPE_IS_WG_META) { - $meta_prefix = $wg_meta_prefix; - } - unless ($attr_type == ATTR_TYPE_IS_UNKNOWN) { - $new_config .= $meta_prefix . get_attr_config($attr_type)->{$attr_name}{in_config_name} - . " = " . $ref_interface_config->{$identifier}{$attr_name} . "\n"; - } - else { - $new_config .= "$attr_name = $ref_interface_config->{$identifier}{$attr_name}\n"; - } - - } - } - $new_config .= "\n"; - } - if ($plain == FALSE) { - my $new_hash = compute_md5_checksum($new_config); - my $config_header = "# This config is generated and maintained by wg-meta.\n" - . "# It is strongly recommended to edit this config only through a supporting wg-meta\n" - . "# implementation (e.g the wg-meta cli interface)\n" - . "#\n" - . "# Changes to this header are always overwritten, you can add normal comments in [Peer] and [Interface] section though.\n" - . "#\n" - . "# Support and issue tracker: https://github.com/sirtoobii/wg-meta\n" - . "#+Checksum = $new_hash\n\n"; - - return $config_header . $new_config; - } - else { - return $new_config; - } -} - -1; \ No newline at end of file diff --git a/lib/Wireguard/WGmeta/Parser/Middleware.pm b/lib/Wireguard/WGmeta/Parser/Middleware.pm new file mode 100644 index 0000000..1650a22 --- /dev/null +++ b/lib/Wireguard/WGmeta/Parser/Middleware.pm @@ -0,0 +1,305 @@ +=pod + +=head1 NAME + +WGmeta::Parser::Middleware - Middleware between the parser and wrapper class(es) + +=head1 SYNOPSIS + + use Wireguard::WGmeta::Parser::Middleware; + use Wireguard::WGmeta::Util; + + # Parse a wireguard configuration file + my $config_contents = read_file('/path/to/config.conf', 'interface_name'); + my $parsed_config = parse_wg_config2($config_contents); + + # And convert it to string representation again + my $new_config_content = create_wg_config2($parsed_config); + +=head1 DESCRIPTION + +Acts as a middleware between L and L. Most importantly it +implements the I and I callbacks of L. + +=head1 METHODS + +=cut +package Wireguard::WGmeta::Parser::Middleware; +use strict; +use warnings FATAL => 'all'; +use experimental qw(signatures); + +use Wireguard::WGmeta::Parser::Conf qw(INTERNAL_KEY_PREFIX parse_raw_wg_config); +use Wireguard::WGmeta::ValidAttributes; +use Wireguard::WGmeta::Utils; + +use base 'Exporter'; +our @EXPORT = qw(parse_wg_config2 create_wg_config2); + +our $VERSION = "0.3.0"; + +=head3 parse_wg_config2($config_file_content, $interface_name [, $wg_meta_prefix, $disabled_prefix, $use_checksum]) + +Using the I and I, this method enriches the parsed config by several artefacts: +I, I, and I. Considering this minimal config example: + + #+root_attr1 = value1 + + [Interface] + ListenPort = 12345 + + [Peer] + #+Alias = some_alias + PublicKey = peer_1 + +We end up with the following structure: + + { + 'root_attr1' => value1, + INT_PREFIX.'root_order' => [ + 'root_attr1' + ], + INT_PREFIX.'section_order' => [ + 'interface_1', + 'peer_1' + ], + INT_PREFIX.'n_peers' => 1, + INT_PREFIX.'observer_wg_meta_attrs => { + 'alias' => 1 + }, + INT_PREFIX.'alias_map => { + 'some_alias' => 'peer_1' + }, + 'interface_name => 'interface_name', + 'interface_1 => { + 'listen-port' => 12345, + INT_PREFIX.'type' => 'Interface', + INT_PREFIX.'order' => [ + 'listen-port', + ] + }, + 'peer_1' => { + 'alias' => 'some_alias', + INT_PREFIX.'type => 'Peer', + INT_PREFIX.'order => [ + 'alias', + ] + } + } + +B + +=over 1 + +=item * + +All attributes listed in L are referenced by their key. This means, if you want for +example access I the key would be I. Any attribute not present in L +is stored (and written back) as they appear in Config. + +=item * + +This method can be used as stand-alone in conjunction with the L<> + +=item * + +If the section is of type 'Peer' the identifier equals to its public-key, otherwise its the interface name again. + +=item * + +wg-meta attributes are always prefixed with C<$wg_meta_prefix>. + + +=back + +B + +=over 1 + +=item * + +C<$config_file_content> String containing the contents of a Wireguard configuration file. + +=item * + +C<$interface_name> Interface name + +=item * + +C<[$wg_meta_prefix = '#+']> wg-meta prefix. Must start with '#' or ';' + +=item * + +C<[$disabled_prefix = '#-']> disabled prefix. Must start with '#' or ';' + +=item * + +C<[$use_checksum = TRUE]> If set to False, checksum is not checked + + +=back + +B + +An exceptions if: + +=over 1 + +=item * + +If the parser ends up in an invalid state (e.g a section without information). Or An alias is defined twice. + +=back + +A warning: + +=over 1 + +=item * + +On a checksum mismatch + +=back + +B + +A reference to a hash with the structure described above. Or if the configuration file is not a Wireguard configuration: undef. + +=cut +sub parse_wg_config2($config_file_content, $interface_name, $wg_meta_prefix = '#+', $disabled_prefix = '#-', $use_checksum = 1) { + + return undef unless ($config_file_content =~ /\[Interface\]/); + + my %alias_map; + my %observed_wg_meta_attrs; + my $peer_count = 0; + my $alias_to_consume; + my $old_checksum; + + my $entry_handler = sub($raw_key, $raw_value, $is_wg_meta) { + my $final_key = $raw_key; + my $final_value = $raw_value; + + + # Convert known Keys to attr-name style + $final_key = NAME_2_KEYS_MAPPING->{$raw_key} if exists NAME_2_KEYS_MAPPING->{$raw_key}; + + $observed_wg_meta_attrs{$final_key} = 1 if $is_wg_meta; + # register alias to consume (if any) + $alias_to_consume = $raw_value if $raw_key eq 'Alias'; + + if ($raw_key eq 'Checksum') { + $old_checksum = $raw_value; + # discard old checksum + return undef, undef, 1; + } + + return $final_key, $final_value, 0; + }; + + my $new_section_handler = sub($identifier, $section_type, $is_active) { + $peer_count++ if $section_type eq 'Peer'; + + # Consume alias (if any) + if (defined $alias_to_consume) { + die "Alias `$alias_to_consume` is already defined on $interface_name" if exists $alias_map{$alias_to_consume}; + $alias_map{$alias_to_consume} = $identifier; + $alias_to_consume = undef; + } + + return ($section_type eq 'Interface') ? $interface_name : $identifier; + + }; + + my $parsed_config = parse_raw_wg_config($config_file_content, $entry_handler, $new_section_handler, 0, $wg_meta_prefix, $disabled_prefix); + $parsed_config->{INTERNAL_KEY_PREFIX . 'alias_map'} = \%alias_map; + $parsed_config->{INTERNAL_KEY_PREFIX . 'n_peers'} = $peer_count; + $parsed_config->{INTERNAL_KEY_PREFIX . 'interface_name'} = $interface_name; + $parsed_config->{INTERNAL_KEY_PREFIX . 'observed_wg_meta_attrs'} = \%observed_wg_meta_attrs; + + if ($use_checksum == 1 && defined $old_checksum) { + my $new_checksum = compute_md5_checksum(create_wg_config2($parsed_config, $wg_meta_prefix, $disabled_prefix, 1)); + warn("Checksum mismatch `$interface_name` has been altered in the meantime") if not $new_checksum eq $old_checksum; + } + + return $parsed_config; +} + +=head3 create_wg_config2($ref_interface_config [, $wg_meta_prefix, $disabled_prefix, $no_checksum]) + +Turns a reference of interface-config hash (just a single interface!) back into a wireguard config. + +B + +=over 1 + +=item * + +C<$ref_interface_config> Reference to hash containing B interface config. + +=item * + +C<[$wg_meta_prefix = '#+']> Has to start with a '#' or ';' character and is ideally the +same as in L + +=item * + +C<[$wg_meta_prefix = '#-']> Same restrictions as parameter C<$wg_meta_prefix> + +=item * + +C<[$no_checksum = FALSE]> If set to true, no header checksum is calculated and added to the output + +=back + +B + +A string, ready to be written down as a config file. + +=cut +sub create_wg_config2($ref_interface_config, $wg_meta_prefix = '#+', $disabled_prefix = '#-', $no_checksum = 0) { + my $new_config = ""; + + for my $identifier (@{$ref_interface_config->{INTERNAL_KEY_PREFIX . 'section_order'}}) { + if (not ref($ref_interface_config->{$identifier}) eq 'HASH') { + # We are in root section + $new_config .= _write_line($identifier, $ref_interface_config->{$identifier}, '', $wg_meta_prefix); + } + else { + # First lets check if the following section is active + my $is_disabled = (exists $ref_interface_config->{$identifier}{'disabled'} + and $ref_interface_config->{$identifier}{'disabled'} == 1) ? $disabled_prefix : ''; + + # Add [Interface] or [Peer] + $new_config .= "\n$is_disabled" . "[$ref_interface_config->{$identifier}{INTERNAL_KEY_PREFIX . 'type'}]\n"; + + # Add config lines + for my $attr_name (@{$ref_interface_config->{$identifier}{INTERNAL_KEY_PREFIX . 'order'}}) { + + my $is_wg_meta = (exists $ref_interface_config->{INTERNAL_KEY_PREFIX .'observed_wg_meta_attrs'}{$attr_name}) ? $wg_meta_prefix : ''; + $new_config .= _write_line($attr_name, $ref_interface_config->{$identifier}{$attr_name}, $is_disabled, $is_wg_meta); + } + } + } + if ($no_checksum == 0) { + return "#+Checksum = " . compute_md5_checksum($new_config) . "\n" . $new_config; + } + return $new_config; +} + +# internal method to create on config line +sub _write_line($attr_name, $attr_value, $is_disabled, $is_wg_meta) { + my $cfg_line = ''; + # if we have a comment + if (substr($attr_name, 0, 7) eq 'comment') { + $cfg_line .= $attr_value . "\n"; + } + else { + my $inconfig_name = exists KNOWN_ATTRIBUTES->{$attr_name} ? KNOWN_ATTRIBUTES->{$attr_name}{in_config_name} : $attr_name; + $cfg_line .= "$is_disabled$is_wg_meta$inconfig_name = $attr_value\n"; + } + return $cfg_line; +} + + +1; \ No newline at end of file diff --git a/lib/Wireguard/WGmeta/Parser/Show.pm b/lib/Wireguard/WGmeta/Parser/Show.pm index 7ea8d3f..3def996 100644 --- a/lib/Wireguard/WGmeta/Parser/Show.pm +++ b/lib/Wireguard/WGmeta/Parser/Show.pm @@ -28,13 +28,12 @@ use experimental 'signatures'; use base 'Exporter'; our @EXPORT = qw(wg_show_dump_parser); -our $VERSION = "0.0.0"; # do not change manually, this variable is updated when calling make +our $VERSION = "0.3.0"; # do not change manually, this variable is updated when calling make =head3 wg_show_dump_parser($input) -Parser for the output of C. Aims to be produce a similar structure as -L: +Parser for the output of C: { 'interface_name' => { diff --git a/lib/Wireguard/WGmeta/ValidAttributes.pm b/lib/Wireguard/WGmeta/ValidAttributes.pm index 725b8d9..7ac5b31 100644 --- a/lib/Wireguard/WGmeta/ValidAttributes.pm +++ b/lib/Wireguard/WGmeta/ValidAttributes.pm @@ -5,12 +5,18 @@ WGmeta::ValidAttributes - Supported attribute configurations =head1 DESCRIPTION In this module all supported attributes are configured (and as well their possible validation function). Attributes configured -here affect how the parser stores them and which attributes are supported by the -L. +here affect how the parser stores them =head1 SYNOPSIS -Add your own attributes to L + use Wireguard::WGmeta::ValidAttributes; + + my $attr_type = get_attr_type($attr_name); + + if ($attr_type == ATTR_TYPE_IS_WG_META){ + print "Yiii-haa"; + } + =cut @@ -21,6 +27,8 @@ use experimental 'signatures'; use Wireguard::WGmeta::Validator; +our $VERSION = "0.3.0"; + =head1 ATTRIBUTE TYPES =cut @@ -31,12 +39,7 @@ use Wireguard::WGmeta::Validator; =cut use constant ATTR_TYPE_IS_WG_META => 10; -=head3 ATTR_TYPE_IS_WG_META_CUSTOM - -Your custom wg-meta attributes -=cut -use constant ATTR_TYPE_IS_WG_META_CUSTOM => 11; =head3 ATTR_TYPE_IS_WG_QUICK wg-quick attribute @@ -68,174 +71,131 @@ use constant FALSE => 0; use base 'Exporter'; our @EXPORT = qw( ATTR_TYPE_IS_WG_META - ATTR_TYPE_IS_WG_META_CUSTOM ATTR_TYPE_IS_WG_QUICK ATTR_TYPE_IS_WG_ORIG_INTERFACE ATTR_TYPE_IS_WG_ORIG_PEER ATTR_TYPE_IS_UNKNOWN - get_attr_config - decide_attr_type - register_custom_attribute + NAME_2_KEYS_MAPPING + KNOWN_ATTRIBUTES + get_attr_type ); -=head1 ATTRIBUTE SETS - -General remark: If you want to add your own attributes add them to L - all other config sets -should only be modified on (possible) future changes in attribute configurations in Wireguard or wg-quick! -=head3 WG_META_DEFAULT +=head3 KNOWN_ATTRIBUTES -wg-meta default attributes. Do not make changes here, they are expected to be present! +Mapping of all known attributes =cut -use constant WG_META_DEFAULT => { - 'name' => { - 'in_config_name' => 'Name', - 'validator' => \&accept_any - }, - 'alias' => { + +use constant KNOWN_ATTRIBUTES => { + 'alias' => { + 'type' => ATTR_TYPE_IS_WG_META, 'in_config_name' => 'Alias', 'validator' => \&accept_any }, - 'description' => { - 'in_config_name' => 'Description', - 'validator' => \&accept_any - }, - 'disabled' => { + 'disabled' => { + 'type' => ATTR_TYPE_IS_WG_META, 'in_config_name' => 'Disabled', 'validator' => \&accept_any }, - 'fqdn' => { - 'in_config_name' => 'FQDN', + 'checksum' => { + 'type' => ATTR_TYPE_IS_WG_META, + 'in_config_name' => 'Checksum', 'validator' => \&accept_any - } -}; - -=head3 WG_META_ADDITIONAL - -Use L to register your own attributes - -=cut -use constant WG_META_ADDITIONAL => {}; - -=head3 WG_QUICK - -wg-quick attribute set - -=cut -use constant WG_QUICK => { - 'address' => { + }, + 'address' => { 'in_config_name' => 'Address', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any }, - 'dns' => { + 'dns' => { 'in_config_name' => 'DNS', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any }, - 'mtu' => { + 'mtu' => { 'in_config_name' => 'MTU', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any }, - 'table' => { + 'table' => { 'in_config_name' => 'Table', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any }, - 'pre-up' => { + 'pre-up' => { 'in_config_name' => 'PreUp', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any }, - 'post-up' => { + 'post-up' => { 'in_config_name' => 'PostUP', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any }, - 'pre-down' => { + 'pre-down' => { 'in_config_name' => 'PreDown', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any }, - 'post-down' => { + 'post-down' => { 'in_config_name' => 'PostDown', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any }, - 'save-config' => { + 'save-config' => { 'in_config_name' => 'SaveConfig', + 'type' => ATTR_TYPE_IS_WG_QUICK, 'validator' => \&accept_any - } -}; - -=head3 WG_ORIG_INTERFACE - -Attributes valid for Wireguard I<[Interface>] sections - -=cut -use constant WG_ORIG_INTERFACE => { - 'listen-port' => { + }, + 'listen-port' => { 'in_config_name' => 'ListenPort', + 'type' => ATTR_TYPE_IS_WG_ORIG_INTERFACE, 'validator' => \&is_number }, - 'fwmark' => { + 'fwmark' => { 'in_config_name' => 'Fwmark', + 'type' => ATTR_TYPE_IS_WG_ORIG_INTERFACE, 'validator' => \&accept_any }, - 'private-key' => { + 'private-key' => { 'in_config_name' => 'PrivateKey', + 'type' => ATTR_TYPE_IS_WG_ORIG_INTERFACE, 'validator' => \&accept_any - } -}; - -=head3 WG_ORIG_PEER - -Attributes valid for Wireguard I<[Peer>] sections - -=cut -use constant WG_ORIG_PEER => { + }, 'public-key' => { 'in_config_name' => 'PublicKey', + 'type' => ATTR_TYPE_IS_WG_ORIG_PEER, 'validator' => \&accept_any }, 'preshared-key' => { 'in_config_name' => 'PresharedKey', + 'type' => ATTR_TYPE_IS_WG_ORIG_PEER, 'validator' => \&accept_any }, 'endpoint' => { 'in_config_name' => 'Endpoint', + 'type' => ATTR_TYPE_IS_WG_ORIG_PEER, 'validator' => \&accept_any }, 'persistent-keepalive' => { - 'in_config_name' => 'PresistentKeepAlive', + 'in_config_name' => 'PresistentKeepalive', + 'type' => ATTR_TYPE_IS_WG_ORIG_PEER, 'validator' => \&accept_any }, 'allowed-ips' => { 'in_config_name' => 'AllowedIPs', + 'type' => ATTR_TYPE_IS_WG_ORIG_PEER, 'validator' => \&accept_any }, }; -# internal method to create mappings -sub _create_inverse_mapping() { - my $inv_map = {}; - map {$inv_map->{$_} = ATTR_TYPE_IS_WG_ORIG_PEER;} (keys %{+WG_ORIG_PEER}); - map {$inv_map->{$_} = ATTR_TYPE_IS_WG_ORIG_INTERFACE;} (keys %{+WG_ORIG_INTERFACE}); - map {$inv_map->{$_} = ATTR_TYPE_IS_WG_META;} (keys %{+WG_META_DEFAULT}); - map {$inv_map->{$_} = ATTR_TYPE_IS_WG_META_CUSTOM;} (keys %{+WG_META_ADDITIONAL}); - map {$inv_map->{$_} = ATTR_TYPE_IS_WG_QUICK;} (keys %{+WG_QUICK}); - return $inv_map; -} sub _create_inconfig_name_mapping() { my $names2key = {}; - map {$names2key->{WG_ORIG_PEER->{$_}{in_config_name}} = $_;} (keys %{+WG_ORIG_PEER}); - map {$names2key->{WG_ORIG_INTERFACE->{$_}{in_config_name}} = $_;} (keys %{+WG_ORIG_INTERFACE}); - map {$names2key->{WG_META_DEFAULT->{$_}{in_config_name}} = $_;} (keys %{+WG_META_DEFAULT}); - map {$names2key->{WG_META_ADDITIONAL->{$_}{in_config_name}} = $_;} (keys %{+WG_META_ADDITIONAL}); - map {$names2key->{WG_QUICK->{$_}{in_config_name}} = $_;} (keys %{+WG_QUICK}); + map {$names2key->{KNOWN_ATTRIBUTES->{$_}{in_config_name}} = $_;} (keys %{+KNOWN_ATTRIBUTES}); return $names2key; } -=head3 INVERSE_ATTR_TYPE_MAPPING - -[Generated] Static mapping from Iattr_key to I. - -=cut -use constant INVERSE_ATTR_TYPE_MAPPING => _create_inverse_mapping; =head3 NAME_2_KEYS_MAPPING @@ -244,140 +204,16 @@ use constant INVERSE_ATTR_TYPE_MAPPING => _create_inverse_mapping; =cut use constant NAME_2_KEYS_MAPPING => _create_inconfig_name_mapping; -=head1 METHODS - -=head2 get_attr_config($attr_type) - -Returns an attribute config set from L given a valid attr type. -Ideally obtained through L. -B +=head3 get_attr_type($attr_name) -=over 1 - -=item - -C<$attr_type> A valid attribute type. - -=back - -B - -Exception is type is invalid (not known). - -B - -If the type is valid, the corresponding attribute config map. - -=cut -sub get_attr_config($attr_type) { - for ($attr_type) { - $_ == ATTR_TYPE_IS_WG_ORIG_PEER && do { - return WG_ORIG_PEER; - }; - $_ == ATTR_TYPE_IS_WG_ORIG_INTERFACE && do { - return WG_ORIG_INTERFACE; - }; - $_ == ATTR_TYPE_IS_WG_META && do { - return WG_META_DEFAULT; - }; - $_ == ATTR_TYPE_IS_WG_META_CUSTOM && do { - return WG_META_ADDITIONAL; - }; - $_ == ATTR_TYPE_IS_WG_QUICK && do { - return WG_QUICK; - }; - } - die "Invalid attribute type `$attr_type`"; -} - -=head2 decide_attr_type($attr_name [, $allow_unknown = FALSE]) - -Returns the attribute type given an I. - -B - -=over 1 - -=item - -C<$attr_name> An attribute key as defined in one L. - -=item - -C<[$allow_unknown = FALSE]> If set to true, unknown attributes result in the type L. - -=back - -B - -An Exception, if the attribute is unknown (and C<$allow_unknown = FALSE>). - -B - -An attribute type from L +Shorthand for getting the attribute type =cut -sub decide_attr_type($attr_name, $allow_unknown = FALSE) { - if (exists INVERSE_ATTR_TYPE_MAPPING->{$attr_name}) { - return INVERSE_ATTR_TYPE_MAPPING->{$attr_name}; - } - else { - if ($allow_unknown == TRUE) { - return ATTR_TYPE_IS_UNKNOWN; - } - else { - die "Attribute `$attr_name` is not known"; - } - } -} - -=head3 register_custom_attribute($ref_attr_config) - -Register your custom attribute names. - -B - -=over 1 - -=item - -C<$ref_attr_config> A reference to your attribute description. Expected to be in the following format: - { - 'in_config_name' => 'in_config_attr_name', - 'validator' => 'Function reference to a validator function' - }, - -For the validator function you can either create your own or use one defined in L - -=back - -B - -Exception if C<$ref_attr_config> is malformed - -B - -1 on success, undef if the attribute is already defined - -=cut -sub register_custom_attribute($attr_key, $ref_attr_config) { - unless (decide_attr_type($attr_key, TRUE) != ATTR_TYPE_IS_UNKNOWN) { - if (exists $ref_attr_config->{in_config_name} && exists $ref_attr_config->{validator}) { - WG_META_ADDITIONAL->{$attr_key} = $ref_attr_config; - # update mappings - INVERSE_ATTR_TYPE_MAPPING->{$attr_key} = ATTR_TYPE_IS_WG_META_CUSTOM; - NAME_2_KEYS_MAPPING->{$ref_attr_config->{in_config_name}} = $attr_key; - } - else { - die "Malformed attribute config"; - } - } - else { - return undef; - } - return 1; +sub get_attr_type($attr_name) { + return KNOWN_ATTRIBUTES->{$attr_name}{type} if exists KNOWN_ATTRIBUTES->{$attr_name}; + return ATTR_TYPE_IS_UNKNOWN; } 1; \ No newline at end of file diff --git a/lib/Wireguard/WGmeta/Wrapper/Bridge.pm b/lib/Wireguard/WGmeta/Wrapper/Bridge.pm index 9370b8e..500be6e 100644 --- a/lib/Wireguard/WGmeta/Wrapper/Bridge.pm +++ b/lib/Wireguard/WGmeta/Wrapper/Bridge.pm @@ -31,11 +31,14 @@ Two strings: private-key and public-key =cut sub gen_keypair() { - my (@out_priv, undef) = run_external('wg genkey'); - my (@out_pub, undef) = run_external('wg pubkey', $out_priv[0]); - chomp @out_priv; - chomp @out_pub; - return $out_priv[0], $out_pub[0]; + unless (defined $ENV{WGmeta_NO_WG}){ + my (@out_priv, undef) = run_external('wg genkey'); + my (@out_pub, undef) = run_external('wg pubkey', $out_priv[0]); + chomp @out_priv; + chomp @out_pub; + return $out_priv[0], $out_pub[0]; + } + return 'dummy_private_key', 'dummy_public_key'; } =head2 get_pub_key($priv_key) @@ -57,9 +60,13 @@ A string containing the derived publickey. =cut sub get_pub_key($priv_key){ - my (@out, undef) = run_external('wg pubkey', $priv_key); - chomp @out; - return $out[0]; + unless (defined $ENV{WGmeta_NO_WG}){ + my (@out, undef) = run_external('wg pubkey', $priv_key); + chomp @out; + return $out[0]; + } + return 'dummy_pubkey_from_privkey'; + } =head2 get_wg_show() @@ -76,10 +83,14 @@ First array of STD_OUT =cut sub get_wg_show() { - my $cmd = 'wg show all dump'; - my (@out, undef) = run_external($cmd); - chomp @out; - return @out; + unless (defined $ENV{WGmeta_NO_WG}){ + my $cmd = 'wg show all dump'; + my (@out, undef) = run_external($cmd); + chomp @out; + return @out; + } + + } =head2 run_external($command_line [, $input, $soft_fail]) diff --git a/lib/Wireguard/WGmeta/Wrapper/Config.pm b/lib/Wireguard/WGmeta/Wrapper/Config.pm index 78726d3..17a7c3d 100644 --- a/lib/Wireguard/WGmeta/Wrapper/Config.pm +++ b/lib/Wireguard/WGmeta/Wrapper/Config.pm @@ -11,7 +11,7 @@ WGmeta::Wrapper::Config - Class for interfacing the wireguard configuration =head1 DESCRIPTION -This class provides wrapper-functions around a wireguard configuration parsed by L which +This class provides wrapper-functions around a wireguard configuration parsed by L which allow to edit, add and remove interfaces and peers. =head1 CONCURRENCY @@ -29,8 +29,8 @@ Please refer to L # set an alias for a peer wg_meta->set('wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'alias', 'some_fancy_alias'); - # disable peer (this comments out the peer in the configuration file - wg_meta->disable_by_alias('wg0', 'some_fancy_alias'); + # disable peer (this comments out the peer in the configuration file) + wg_meta->disable('wg0', 'some_fancy_alias'); # write config (if parameter is set to True, the config is overwritten, if set to False the resulting file is suffixed with '.not_applied' wg_meta->commit(1); @@ -45,11 +45,12 @@ use strict; use warnings; use experimental 'signatures'; use Wireguard::WGmeta::Wrapper::Bridge; -use Wireguard::WGmeta::Parser::Config; +use Wireguard::WGmeta::Parser::Middleware; use Wireguard::WGmeta::ValidAttributes; use Wireguard::WGmeta::Utils; +use Wireguard::WGmeta::Parser::Conf qw(INTERNAL_KEY_PREFIX); -our $VERSION = "0.0.0"; # do not change manually, this variable is updated when calling make +our $VERSION = "0.3.0"; # do not change manually, this variable is updated when calling make use constant FALSE => 0; use constant TRUE => 1; @@ -87,11 +88,9 @@ C<[$custom_attributes]> A reference to a hash defining custom attributes. Expect { 'attr_key' => { - 'in_config_name' => 'In config name', 'validator' => 'Ref to validation function' }, 'example' => { - 'in_config_name' => 'DNS', 'validator' => \&accept_any }, ... @@ -110,12 +109,6 @@ sub new($class, $wireguard_home, $wg_meta_prefix = '#+', $wg_meta_disabled_prefi die '`$wg_meta_prefix` and `$wg_meta_disabled_prefix` have to be different'; } - if (defined $custom_attributes) { - for my $attr_key (keys %{$custom_attributes}) { - register_custom_attribute($attr_key, $custom_attributes->{$attr_key}); - } - } - my $self = { 'wireguard_home' => $wireguard_home, 'wg_meta_prefix' => $wg_meta_prefix, @@ -124,6 +117,7 @@ sub new($class, $wireguard_home, $wg_meta_prefix = '#+', $wg_meta_disabled_prefi 'n_conf_files' => {}, 'parsed_config' => {}, 'reload_listeners' => {}, + 'custom_attributes' => defined $custom_attributes ? $custom_attributes : {} }; _read_configs_from_folder2($self); @@ -135,14 +129,13 @@ sub new($class, $wireguard_home, $wg_meta_prefix = '#+', $wg_meta_disabled_prefi sub _read_configs_from_folder2($self) { my ($all_dot_conf, $count) = get_all_conf_files($self->{wireguard_home}); for my $possible_config_path (@{$all_dot_conf}) { - my $contents = read_file($possible_config_path); my $interface = $possible_config_path; $interface =~ s/^\/|\\|.*\/|.*\\|.conf$//g; may_reload_from_disk($self, $interface, TRUE, TRUE, TRUE); } } -=head3 set($interface, $identifier, $attribute, $value [, $allow_non_meta, $forward_function]) +=head3 set($interface, $identifier, $attribute, $value [, $unknown_callback]) Sets a value on a specific interface section. If C == C<$value> this sub is essentially a No-Op. @@ -156,107 +149,109 @@ C<$interface> Valid interface identifier (e.g 'wg0') =item * -C<$identifier> If the target section is a peer, this is usually the public key of this peer. If target is an interface, -its again the interface name. +C<$identifier> Either an interface name, a public-key of a peer or an alias =item * -C<$attribute> Attribute name (Case does not not matter) +C<$attribute> Attribute name (case does matter!) =item * -C<[$allow_non_meta = FALSE]> If set to TRUE, non wg-meta attributes are not forwarded to C<$forward_function>. +C<[$unknown_callback = undef]> A reference to a callback function which is fired when a previously unknown attribute is set. +Expected signature: -=item * - -C<[$forward_function = undef]> A reference to a callback function when C<$allow_non_meta = TRUE>. The following signature -is expected: C + sub my_unknown_callback($attribute, $value) { + # Handling of this particular case + return $attribute, $value; + } =back B -Exception if either the interface or identifier is invalid. +Exception if: + +=over 1 + +=item * + +Value is not defined + +=item * + +Interface is invalid + +=item * + +Identifier is invalid (also if alias translation fails) + +=item * + +Attribute is not valid for target section (Interface, Peer) + +=item * + +Validation for the attribute value fails + +=back B None =cut -sub set($self, $interface, $identifier, $attribute, $value, $allow_non_meta = FALSE, $forward_function = undef) { - my $attr_type = decide_attr_type($attribute, TRUE); - unless (defined $value) { - warn "Undefined value for `$attribute` in interface `$interface` NOT SET"; +sub set($self, $interface, $identifier, $attribute, $value, $unknown_callback = undef, $force = 0) { + # Assertions + die "Undefined value for `$attribute` in interface `$interface` NOT SET" unless defined($value); + die "Invalid interface name `$interface`" unless $self->is_valid_interface($interface); + $identifier = $self->try_translate_alias($interface, $identifier); + die "Invalid identifier `$identifier` for interface `$interface`" unless $self->is_valid_identifier($interface, $identifier); + my $attr_type = get_attr_type($attribute); + if ($interface eq $identifier) { + # We have an interface + die "Attribute `$attribute` it not valid for the interface section" if $attr_type == ATTR_TYPE_IS_WG_ORIG_PEER; + } + else { + die "Attribute `$attribute` is not valid for an peer section" if $attr_type == ATTR_TYPE_IS_WG_ORIG_INTERFACE or $attr_type == ATTR_TYPE_IS_WG_QUICK; + } + + # skip if same value + if (exists $self->{parsed_config}{$interface}{$identifier}{$attribute} && $self->{parsed_config}{$interface}{$identifier}{$attribute} eq $value) { return; } - if ($self->is_valid_interface($interface)) { - if ($self->is_valid_identifier($interface, $identifier)) { - # skip if same value - if (exists $self->{parsed_config}{$interface}{$identifier}{$attribute} && $self->{parsed_config}{$interface}{$identifier}{$attribute} eq $value) { - return; - } - if ($attr_type == ATTR_TYPE_IS_WG_META || $attr_type == ATTR_TYPE_IS_WG_META_CUSTOM) { + die "Invalid attribute value `$value` for `$attribute`" unless $self->attr_value_is_valid($attribute, $value); - unless (attr_value_is_valid($attribute, $value, get_attr_config($attr_type))) { - die "Invalid attribute value `$value` for `$attribute`"; - } - unless (exists $self->{parsed_config}{$interface}{$identifier}{$attribute}) { - # the attribute does not (yet) exist in the configuration, lets add it to the list - push @{$self->{parsed_config}{$interface}{$identifier}{order}}, $attribute; - } - if ($attribute eq 'alias') { - $self->_update_alias_map($interface, $identifier, $value); - } - $self->{parsed_config}{$interface}{$identifier}{$attribute} = $value; + unless (exists $self->{parsed_config}{$interface}{$identifier}{$attribute}) { + + if (not exists $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'observed_wg_meta_attrs'}{$attribute}) { + if (exists KNOWN_ATTRIBUTES->{$attribute}) { + # we have to first occurrence of a known but yet unseen wg-meta attribute + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'observed_wg_meta_attrs'}{$attribute} = 1 if KNOWN_ATTRIBUTES->{$attribute}{type} == ATTR_TYPE_IS_WG_META; + } + elsif (exists $self->{custom_attributes}{$attribute}) { + # we have a registered custom attribute + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'observed_wg_meta_attrs'}{$attribute} = 1 } else { - if ($allow_non_meta == TRUE) { - if (_fits_wg_section($interface, $identifier, $attr_type)) { - unless (attr_value_is_valid($attribute, $value, get_attr_config($attr_type))) { - die "Invalid attribute value `$value` for `$attribute`"; - } - unless (exists $self->{parsed_config}{$interface}{$identifier}{$attribute}) { - # the attribute does not (yet) exist in the configuration, lets add it to the list - push @{$self->{parsed_config}{$interface}{$identifier}{order}}, $attribute; - } - # the attribute does already exist and therefore we just set it to the new value - $self->{parsed_config}{$interface}{$identifier}{$attribute} = $value; - } - else { - die "The supplied attribute `$attribute` is not valid for this section type (this most likely means you've tried to set a peer attribute in the interface section or vice-versa)"; - } + # we have a completely new, unknown attribute + if (defined $unknown_callback) { + ($attribute, $value) = &{$unknown_callback}($attribute, $value); } else { - if (defined($forward_function)) { - &{$forward_function}($interface, $identifier, $attribute, $value); - } - else { - die 'No forward function defined'; - } + warn "Attribute `$attribute` was previously not known on interface `$interface`" if $force == 0; } + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'observed_wg_meta_attrs'}{$attribute} = 1; } - $self->_set_changed($interface); - } - else { - die "Invalid identifier `$identifier` for interface `$interface`"; } + # the attribute does not (yet) exist in the configuration, lets add it to the list + push @{$self->{parsed_config}{$interface}{$identifier}{INTERNAL_KEY_PREFIX . 'order'}}, $attribute; } - else { - die "Invalid interface name `$interface`"; - } - -} - -# internal method to check if a non-meta attribute is valid for the target section -sub _fits_wg_section($interface, $identifier, $attr_type) { - # if we have an interface - if ($interface eq $identifier) { - return $attr_type == ATTR_TYPE_IS_WG_ORIG_INTERFACE || $attr_type == ATTR_TYPE_IS_WG_QUICK - } - else { - return $attr_type == ATTR_TYPE_IS_WG_ORIG_PEER; + if ($attribute eq 'alias') { + $self->_update_alias_map($interface, $identifier, $value); } + $self->{parsed_config}{$interface}{$identifier}{$attribute} = $value; + $self->_set_changed($interface); } =head3 attr_value_is_valid($attribute, $value, $ref_valid_attrs) @@ -286,32 +281,22 @@ B True if validation was successful, False if not =cut -sub attr_value_is_valid($attribute, $value, $ref_valid_attrs) { - return $ref_valid_attrs->{$attribute}{validator}($value); + +sub attr_value_is_valid($self, $attribute, $value) { + return &{KNOWN_ATTRIBUTES->{$attribute}{validator}}($value) if exists KNOWN_ATTRIBUTES->{$attribute}; + return &{$self->{custom_attributes}{$attribute}{validator}}($value) if exists $self->{custom_attributes}{$attribute}; + return 1; } sub _update_alias_map($self, $interface, $identifier, $alias) { - unless (exists $self->{parsed_config}{$interface}{alias_map}{$alias}) { - $self->{parsed_config}{$interface}{alias_map}{$alias} = $identifier; + unless (exists $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'alias_map'}{$alias}) { + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'alias_map'}{$alias} = $identifier; } else { die "Alias `$alias` is already defined on interface `$interface`"; } } -=head3 set_by_alias($interface, $alias, $attribute, $value [, $allow_non_meta = FALSE, $forward_function = undef]) - -Same as L - just with alias support. - -B - -Exception if alias is invalid - -=cut -sub set_by_alias($self, $interface, $alias, $attribute, $value, $allow_non_meta = FALSE, $forward_function = undef) { - my $identifier = $self->translate_alias($interface, $alias); - $self->set($interface, $identifier, $attribute, $value, $allow_non_meta, $forward_function); -} =head3 disable($interface, $identifier) @@ -350,40 +335,12 @@ sub enable($self, $interface, $identifier) { $self->_toggle($interface, $identifier, FALSE); } -=head3 disable_by_alias($interface, $alias) - -Same as L just with alias support - -B - -Exception if alias is invalid - -=cut -sub disable_by_alias($self, $interface, $alias,) { - $self->_toggle($interface, $self->translate_alias($interface, $alias), FALSE); -} - -=head3 disable_by_alias($interface, $alias) - -Same as Lust with alias support - -B - -Exception if alias is invalid - -=cut -sub enable_by_alias($self, $interface, $alias,) { - $self->_toggle($interface, $self->translate_alias($interface, $alias), TRUE); -} - # internal toggle method (DRY) sub _toggle($self, $interface, $identifier, $enable) { - if (exists $self->{parsed_config}{$interface}{$identifier}{Disabled}) { - if ($self->{parsed_config}{$interface}{$identifier}{Disabled} == "$enable") { - warn "Section `$identifier` in `$interface` is already $enable"; - } - } - $self->set($interface, $identifier, 'disabled', $enable); + $identifier = $self->try_translate_alias($interface, $identifier); + # we can bypass an "expensive" set() here + $self->{parsed_config}{$interface}{$identifier}{'disabled'} = $enable; + $self->_set_changed($interface); } =head3 is_valid_interface($interface) @@ -416,7 +373,7 @@ Simply checks if an alias is valid for spec =cut sub is_valid_alias($self, $interface, $alias) { - return exists $self->{parsed_config}{$interface}{alias_map}{$alias} + return exists $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'alias_map'}{$alias} } =head3 is_valid_identifier($interface, $identifier) @@ -446,46 +403,10 @@ sub is_valid_identifier($self, $interface, $identifier) { return (exists $self->{parsed_config}{$interface}{$identifier}); } -=head3 translate_alias($interface, $alias) - -Translates an alias to a valid identifier. - -B - -=over 1 - -=item * - -C<$interface> A valid interface name (e.g 'wg0'). - -=item * - -C<$alias> An alias to translate - -=back - -B - -Exception if alias is invalid - -B - -A valid identifier. - -=cut -sub translate_alias($self, $interface, $alias) { - if (exists $self->{parsed_config}{$interface}{alias_map}{$alias}) { - return $self->{parsed_config}{$interface}{alias_map}{$alias}; - } - else { - die "Invalid alias `$alias` on interface $interface"; - } -} =head3 try_translate_alias($interface, $may_alias) Tries to translate an identifier (which may be an alias). -However, unlike L, no -exception is thrown on failure, instead the C<$may_alias> is returned. +no exception is thrown on failure, instead the C<$may_alias> is returned. B @@ -507,8 +428,8 @@ If the alias is valid for the specified interface, the corresponding identifier =cut sub try_translate_alias($self, $interface, $may_alias) { - if (exists $self->{parsed_config}{$interface}{alias_map}{$may_alias}) { - return $self->{parsed_config}{$interface}{alias_map}{$may_alias}; + if (exists $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'alias_map'}{$may_alias}) { + return $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'alias_map'}{$may_alias}; } else { return $may_alias; @@ -544,7 +465,7 @@ sub get_all_conf_files($wireguard_home) { } -=head3 commit([$is_hot_config = FALSE, $plain = FALSE]) +=head3 commit([$is_hot_config = FALSE, $no_checksum = FALSE]) Writes down the parsed config to the wireguard configuration folder @@ -559,7 +480,7 @@ the suffix '.not_applied' is appended to the filename =item -C<[$plain = FALSE])> If set to TRUE, no header is generated +C<[$no_checksum = FALSE])> If set to TRUE, no checksum is written =back @@ -573,20 +494,20 @@ B None =cut -sub commit($self, $is_hot_config = FALSE, $plain = FALSE) { +sub commit($self, $is_hot_config = FALSE, $no_checksum = FALSE) { for my $interface (keys %{$self->{parsed_config}}) { if ($self->_has_changed($interface)) { - my $new_config = create_wg_config($self->{parsed_config}{$interface}, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix}, $plain); + my $new_config = create_wg_config2($self->{parsed_config}{$interface}, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix}, $no_checksum); my $fh; my $hot_path = $self->{wireguard_home} . $interface . '.conf'; my $safe_path = $self->{wireguard_home} . $interface . $self->{not_applied_suffix}; if ($is_hot_config == TRUE) { open $fh, '>', $hot_path or die $!; - $self->{parsed_config}->{$interface}{is_hot_config} = 1; + $self->{parsed_config}->{$interface}{INTERNAL_KEY_PREFIX . 'is_hot_config'} = 1; } else { open $fh, '>', $safe_path or die $!; - $self->{parsed_config}->{$interface}{is_hot_config} = 0; + $self->{parsed_config}->{$interface}{INTERNAL_KEY_PREFIX . 'is_hot_config'} = 0; } # write down to file print $fh $new_config; @@ -641,6 +562,7 @@ A hash containing the requested section. If the requested section/interface is n =cut sub get_interface_section($self, $interface, $identifier) { + $identifier = $self->try_translate_alias($interface, $identifier); if (exists $self->{parsed_config}{$interface}{$identifier}) { my %r = %{$self->{parsed_config}{$interface}{$identifier}}; return %r; @@ -671,41 +593,13 @@ A list of all sections of an interface. If interface is not present, an empty li =cut sub get_section_list($self, $interface) { if (exists $self->{parsed_config}{$interface}) { - return @{$self->{parsed_config}{$interface}{section_order}}; + return @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}; } else { return (); } } -=head3 get_interface_fqdn($interface) - -Returns the FQDN for an interface (if available) - -B - -=over 1 - -=item - -C<$interface> A valid interface name - -=back - -B - -Value of C attribute or empty string if unavailable. - -=cut -sub get_interface_fqdn($self, $interface) { - if ($self->is_valid_interface($interface) && exists $self->{parsed_config}{$interface}{fqdn}) { - return $self->{parsed_config}{$interface}{fqdn}; - } - else { - return ''; - } -} - sub get_wg_meta_prefix($self) { return $self->{wg_meta_prefix}; } @@ -756,23 +650,23 @@ sub add_interface($self, $interface_name, $ip_address, $listen_port, $private_ke die "Interface `$interface_name` already exists"; } my %interface = ( - 'Address' => $ip_address, - 'ListenPort' => $listen_port, - 'PrivateKey' => $private_key, - 'type' => 'Interface', - 'order' => [ 'Address', 'ListenPort', 'PrivateKey' ] + 'address' => $ip_address, + 'listen-port' => $listen_port, + 'private-key' => $private_key, + INTERNAL_KEY_PREFIX . 'type' => 'Interface', + INTERNAL_KEY_PREFIX . 'order' => [ 'address', 'listen-port', 'private-key' ] ); $self->{parsed_config}{$interface_name}{$interface_name} = \%interface; - $self->{parsed_config}{$interface_name}{alias_map} = {}; - $self->{parsed_config}{$interface_name}{section_order} = [ $interface_name ]; + $self->{parsed_config}{$interface_name}{INTERNAL_KEY_PREFIX . 'alias_map'} = {}; + $self->{parsed_config}{$interface_name}{INTERNAL_KEY_PREFIX . 'section_order'} = [ $interface_name ]; $self->{parsed_config}{$interface_name}{checksum} = 'none'; - $self->{parsed_config}{$interface_name}{mtime} = 0.0; - $self->{parsed_config}{$interface_name}{config_path} = $self->{wireguard_home} . $interface_name . '.conf'; + $self->{parsed_config}{$interface_name}{INTERNAL_KEY_PREFIX . 'mtime'} = 0.0; + $self->{parsed_config}{$interface_name}{INTERNAL_KEY_PREFIX . 'config_path'} = $self->{wireguard_home} . $interface_name . '.conf'; $self->{parsed_config}{$interface_name}{has_changed} = 1; } -=head3 add_peer($interface, $name, $ip_address, $public_key [, $alias, $preshared_key]) +=head3 add_peer($interface, $ip_address, $public_key [, $alias, $preshared_key]) Adds a peer to an exiting interface. @@ -786,10 +680,6 @@ C<$interface> A valid interface. =item * -C<$name> A name for this peer (wg-meta). - -=item * - C<$ip_address> A string describing the ip-address(es) of this this peer. =item * @@ -816,7 +706,7 @@ B A tuple consisting of the iface private-key and listen port =cut -sub add_peer($self, $interface, $name, $ip_address, $public_key, $alias = undef, $preshared_key = undef) { +sub add_peer($self, $interface, $ip_address, $public_key, $alias = undef, $preshared_key = undef) { # generate new key pair if not defined if ($self->is_valid_interface($interface)) { if ($self->is_valid_identifier($interface, $public_key)) { @@ -825,20 +715,19 @@ sub add_peer($self, $interface, $name, $ip_address, $public_key, $alias = undef, # generate peer config my %peer = (); $self->{parsed_config}{$interface}{$public_key} = \%peer; - $self->set($interface, $public_key, 'name', $name); - $self->set($interface, $public_key, 'public-key', $public_key, 1); - $self->set($interface, $public_key, 'allowed-ips', $ip_address, 1); + $self->set($interface, $public_key, 'public-key', $public_key); + $self->set($interface, $public_key, 'allowed-ips', $ip_address); if (defined $alias) { $self->set($interface, $public_key, 'alias', $alias); } if (defined $preshared_key) { $self->set($interface, $public_key, 'preshared-key', $preshared_key); } - + $self->enable($interface, $public_key); # set type to to Peer - $self->{parsed_config}{$interface}{$public_key}{type} = 'Peer'; + $self->{parsed_config}{$interface}{$public_key}{INTERNAL_KEY_PREFIX . 'type'} = 'Peer'; # add section to global section list - push @{$self->{parsed_config}{$interface}{section_order}}, $public_key; + push @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}, $public_key; return $self->{parsed_config}{$interface}{$interface}{'private-key'}, $self->{parsed_config}{$interface}{$interface}{'listen-port'}; } else { @@ -882,15 +771,15 @@ sub remove_peer($self, $interface, $identifier) { delete $self->{parsed_config}{$interface}{$identifier}; # delete from section list - $self->{parsed_config}{$interface}{section_order} = [ grep {$_ ne $identifier} @{$self->{parsed_config}{$interface}{section_order}} ]; + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'} = [ grep {$_ ne $identifier} @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}} ]; # decrease peer count - $self->{parsed_config}{$interface}{n_peers}--; + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'n_peers'}--; # delete alias (if exists) - while (my ($alias, $a_identifier) = each %{$self->{parsed_config}{$interface}{alias_map}}) { + while (my ($alias, $a_identifier) = each %{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'alias_map'}}) { if ($a_identifier eq $identifier) { - delete $self->{parsed_config}{$interface}{alias_map}{$alias}; + delete $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'alias_map'}{$alias}; } } $self->_set_changed($interface); @@ -931,7 +820,9 @@ sub remove_interface($self, $interface) { if ($self->is_valid_interface($interface)) { # delete interface delete $self->{parsed_config}{$interface}; - unlink "$self->{wireguard_home}$interface.conf" or warn "Could not delete `$self->{wireguard_home}$interface.conf` do you have the needed permissions?"; + if (-e "$self->{wireguard_home}$interface.conf") { + unlink "$self->{wireguard_home}$interface.conf" or warn "Could not delete `$self->{wireguard_home}$interface.conf` do you have the needed permissions?"; + } $self->{n_conf_files}--; } } @@ -959,12 +850,12 @@ Number of peers =cut sub get_peer_count($self, $interface = undef) { if (defined $interface && $self->is_valid_interface($interface)) { - return $self->{parsed_config}{$interface}{n_peers}; + return $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'n_peers'}; } else { my $count = 0; for ($self->get_interface_list()) { - $count += $self->{parsed_config}{$_}{n_peers}; + $count += $self->{parsed_config}{$_}{INTERNAL_KEY_PREFIX . 'n_peers'}; } return $count; } @@ -1019,16 +910,16 @@ sub may_reload_from_disk($self, $interface, $new = FALSE, $force = FALSE, $_init # There is however one exception: The local config is based on a not applied version and this file somehow # unexpectedly deleted (e.g by a sysadmin..) my $on_disk_mtime = get_mtime($config_path); - my $unexpected_delete = (exists $self->{parsed_config}{$interface}{is_hot_config} - && $self->{parsed_config}{$interface}{is_hot_config} == 0 - && $self->{parsed_config}{$interface}{mtime} > $on_disk_mtime); + my $unexpected_delete = (exists $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'is_hot_config'} + && $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'is_hot_config'} == 0 + && $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'mtime'} > $on_disk_mtime); - if ($force || $unexpected_delete || $self->{parsed_config}{$interface}{mtime} < $on_disk_mtime) { + if ($force || $unexpected_delete || $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'mtime'} < $on_disk_mtime) { my $contents = read_file($config_path); - $self->{parsed_config}{$interface} = parse_wg_config($contents, $interface, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix}, FALSE); - $self->{parsed_config}{$interface}{config_path} = $config_path; - $self->{parsed_config}{$interface}{mtime} = get_mtime($config_path); - $self->{parsed_config}{$interface}{is_hot_config} = ($config_path =~ /$self->{not_applied_suffix}/) ? 0 : 1; + $self->{parsed_config}{$interface} = parse_wg_config2($contents, $interface, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix}, FALSE); + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'config_path'} = $config_path; + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'mtime'} = get_mtime($config_path); + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'is_hot_config'} = ($config_path =~ /$self->{not_applied_suffix}/) ? 0 : 1; $self->_call_reload_listeners($interface) if $_init == FALSE; } } @@ -1040,13 +931,13 @@ sub may_reload_from_disk($self, $interface, $new = FALSE, $force = FALSE, $_init else { if (-e $config_path) { my $contents = read_file($config_path); - my $maybe_new_config = parse_wg_config($contents, $interface, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix}, FALSE); + my $maybe_new_config = parse_wg_config2($contents, $interface, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix}, FALSE); if (defined $maybe_new_config) { $self->{n_conf_files}++; $self->{parsed_config}{$interface} = $maybe_new_config; - $self->{parsed_config}{$interface}{config_path} = $config_path; - $self->{parsed_config}{$interface}{mtime} = get_mtime($config_path); - $self->{parsed_config}{$interface}{is_hot_config} = ($config_path =~ /$self->{not_applied_suffix}/) ? 0 : 1; + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'config_path'} = $config_path; + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'mtime'} = get_mtime($config_path); + $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'is_hot_config'} = ($config_path =~ /$self->{not_applied_suffix}/) ? 0 : 1; $self->_call_reload_listeners($interface) if $_init == FALSE;; } else { @@ -1061,20 +952,12 @@ sub may_reload_from_disk($self, $interface, $new = FALSE, $force = FALSE, $_init } -# internal method to add to hash if value is defined -sub _add_to_hash_if_defined($ref_hash, $key, $value) { - if (defined($value)) { - $ref_hash->{$key} = $value; - } - return $ref_hash; -} - # internal method to create a configuration file (this method exists primarily for testing purposes) -sub _create_config($self, $interface, $plain = FALSE) { - return create_wg_config( +sub create_config($self, $interface, $plain = FALSE) { + return create_wg_config2( $self->{parsed_config}{$interface}, $self->{wg_meta_prefix}, - $self->{disabled_prefix}, + $self->{wg_meta_disabled_prefix}, $plain = $plain) } diff --git a/lib/Wireguard/WGmeta/Wrapper/ConfigT.pm b/lib/Wireguard/WGmeta/Wrapper/ConfigT.pm index 338ac0a..2203fa5 100644 --- a/lib/Wireguard/WGmeta/Wrapper/ConfigT.pm +++ b/lib/Wireguard/WGmeta/Wrapper/ConfigT.pm @@ -105,7 +105,8 @@ use File::Basename; use experimental 'signatures'; use Wireguard::WGmeta::Wrapper::Config; -use Wireguard::WGmeta::Parser::Config; +use Wireguard::WGmeta::Parser::Middleware; +use Wireguard::WGmeta::Parser::Conf qw(INTERNAL_KEY_PREFIX); use Wireguard::WGmeta::ValidAttributes; use Wireguard::WGmeta::Utils; @@ -115,7 +116,7 @@ use constant FALSE => 0; use constant TRUE => 1; use constant INTEGRITY_HASH_SALT => 'wefnwioefh9032ur3'; -our $VERSION = "0.0.0"; # do not change manually, this variable is updated when calling make +our $VERSION = "0.3.0"; # do not change manually, this variable is updated when calling make =head3 is_valid_interface($interface) @@ -123,12 +124,12 @@ L =cut sub is_valid_interface($self, $interface) { - $self->_scan_for_new_interfaces(); + $self->_sync_interfaces(); return $self->SUPER::is_valid_interface($interface); } -sub is_valid_alias($self, $interface, $alias){ +sub is_valid_alias($self, $interface, $alias) { $self->may_reload_from_disk($interface); return $self->SUPER::is_valid_alias($interface, $alias); } @@ -143,16 +144,6 @@ sub is_valid_identifier($self, $interface, $identifier) { return $self->SUPER::is_valid_identifier($interface, $identifier); } -=head3 translate_alias($interface, $alias) - -L - -=cut -sub translate_alias($self, $interface, $alias) { - $self->may_reload_from_disk($interface); - return $self->SUPER::translate_alias($interface, $alias); -} - =head3 try_translate_alias($interface, $may_alias) L @@ -199,16 +190,6 @@ sub get_peer_count($self, $interface = undef) { return $self->SUPER::get_peer_count($interface); } -=head3 get_peer_count([$interface]) - -L - -=cut -sub get_interface_fqdn($self, $interface) { - $self->may_reload_from_disk($interface); - return $self->SUPER::get_interface_fqdn($interface); -} - sub _get_all_conf_files($wireguard_home) { my @config_files = read_dir($wireguard_home, qr/.*\.conf$/); if (@config_files == 0) { @@ -224,7 +205,7 @@ L =cut sub get_interface_list($self) { - $self->_scan_for_new_interfaces(); + $self->_sync_interfaces(); # $self->may_reload_from_disk(); return sort keys %{$self->{parsed_config}}; } @@ -292,7 +273,7 @@ sub commit($self, $is_hot_config = FALSE, $plain = FALSE, $ref_hash_integrity_ke open $fh, '+<', $file_name or die "Could not open $file_name: $!"; flock $fh, LOCK_EX; my $config_contents = read_file($fh, TRUE); - $on_disk_config = parse_wg_config($config_contents, $interface_name, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix}); + $on_disk_config = parse_wg_config2($config_contents, $interface_name, $self->{wg_meta_prefix}, $self->{wg_meta_disabled_prefix}); seek $fh, 0, 0; } else { @@ -301,15 +282,14 @@ sub commit($self, $is_hot_config = FALSE, $plain = FALSE, $ref_hash_integrity_ke $is_new = 1; } - my $new_config = $self->_create_wg_configT( + $self->_sync_changes( $interface_name, - $plain, $on_disk_config, $ref_hash_integrity_keys ); # write down to file truncate $fh, 0; - print $fh $new_config; + print $fh create_wg_config2($self->{parsed_config}{$interface_name}); $self->{parsed_config}{$interface_name}{mtime} = get_mtime($file_name); $self->{n_conf_files}++ if (defined $is_new); $self->_reset_changed($interface_name); @@ -320,15 +300,14 @@ sub commit($self, $is_hot_config = FALSE, $plain = FALSE, $ref_hash_integrity_ke } } -sub _create_wg_configT($self, $interface, $plain = FALSE, $ref_on_disk_config = undef, $ref_hash_integrity_keys = undef) { - my $new_config = ""; +sub _sync_changes($self, $interface, $ref_on_disk_config = undef, $ref_hash_integrity_keys = undef) { # first, we look for sections which are common (disk and internal), then we search for exclusive ones my @may_conflict; my @exclusive_disk; my @exclusive_internal; if (defined $ref_on_disk_config) { - for my $identifier_internal (@{$self->{parsed_config}{$interface}{section_order}}) { + for my $identifier_internal (@{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}) { if (exists $ref_on_disk_config->{$identifier_internal}) { push @may_conflict, $identifier_internal; } @@ -336,7 +315,7 @@ sub _create_wg_configT($self, $interface, $plain = FALSE, $ref_on_disk_config = push @exclusive_internal, $identifier_internal; } } - for my $identifier_ondisk (@{$ref_on_disk_config->{section_order}}) { + for my $identifier_ondisk (@{$ref_on_disk_config->{INTERNAL_KEY_PREFIX . 'section_order'}}) { unless (exists $self->{parsed_config}{$interface}{$identifier_ondisk}) { # if we have the latest data, we can safely assume the peer has been deleted if (!$self->_is_latest_data($interface)) { @@ -347,12 +326,10 @@ sub _create_wg_configT($self, $interface, $plain = FALSE, $ref_on_disk_config = } else { # if no on-disk reference is provided all sections are considered as exclusive internal - @exclusive_internal = @{$self->{parsed_config}{$interface}{section_order}}; + @exclusive_internal = @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}; } for my $identifier (@may_conflict) { - my $section_data = $self->{parsed_config}{$interface}{$identifier}; - # if the shas differ, the configuration on disk had been changed in the mean time my $on_disk_sha = _calculate_sha1_from_section($ref_on_disk_config->{$identifier}); my $internal_sha = _calculate_sha1_from_section($self->{parsed_config}{$interface}{$identifier}); @@ -365,77 +342,27 @@ sub _create_wg_configT($self, $interface, $plain = FALSE, $ref_on_disk_config = # if the on-disk sha differs from our integrity hash, this section has been changed by an other process or user. if ($on_disk_sha ne $ref_hash_integrity_keys->{$identifier}) { - $section_data = $ref_on_disk_config->{$identifier}; die "your changes for `$identifier` were not applied"; } } else { # take from disk (we have no integrity key for this section) - $section_data = $ref_on_disk_config->{$identifier} + $self->{parsed_config}{$interface}{$identifier} = $ref_on_disk_config->{$identifier}; } } else { # take from disk - $section_data = $ref_on_disk_config->{$identifier} + #$self->{parsed_config}{$identifier} = $ref_on_disk_config->{$identifier}; } - $new_config .= $self->_create_section($section_data); } - # exclusive mode - $new_config .= join '', map {$self->_create_section($self->{parsed_config}{$interface}{$_});} @exclusive_internal; - $new_config .= join '', map {$self->_create_section($ref_on_disk_config->{$_});} @exclusive_disk; - if ($plain == FALSE) { - my $new_hash = compute_md5_checksum($new_config); - my $config_header = "# This config is generated and maintained by wg-meta.\n" - . "# It is strongly recommended to edit this config only through a supporting wg-meta\n" - . "# implementation (e.g the wg-meta cli interface)\n" - . "#\n" - . "# Changes to this header are always overwritten, you can add normal comments in [Peer] and [Interface] section though.\n" - . "#\n" - . "# Support and issue tracker: https://github.com/sirtoobii/wg-meta\n" - . "#+Checksum = $new_hash\n\n"; - - return $config_header . $new_config; - } - else { - return $new_config; + for my $key (@exclusive_disk) { + $self->{parsed_config}{$interface}{$key} = $ref_on_disk_config->{$key}; + push @{$self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX . 'section_order'}}, $key; } } -sub _create_section($self, $section_data) { - my $new_config = ''; - if (_is_disabled($section_data)) { - $new_config .= $self->{wg_meta_disabled_prefix}; - } - # write down [section_type] - $new_config .= "[$section_data->{type}]\n"; - for my $attr_name (@{$section_data->{order}}) { - if (_is_disabled($section_data)) { - $new_config .= $self->{wg_meta_disabled_prefix}; - } - if (substr($attr_name, 0, 7) eq 'comment') { - $new_config .= $section_data->{$attr_name} . "\n"; - } - else { - my $attr_type = decide_attr_type($attr_name, TRUE); - my $meta_prefix = ''; - if ($attr_type == ATTR_TYPE_IS_WG_META_CUSTOM || $attr_type == ATTR_TYPE_IS_WG_META) { - $meta_prefix = $self->{wg_meta_prefix}; - } - unless ($attr_type == ATTR_TYPE_IS_UNKNOWN) { - $new_config .= $meta_prefix . get_attr_config($attr_type)->{$attr_name}{in_config_name} - . " = " . $section_data->{$attr_name} . "\n"; - } - else { - $new_config .= "$attr_name = $section_data->{$attr_name}\n"; - } - - } - } - $new_config .= "\n"; - return $new_config; -} =head3 may_reload_from_disk([$interface = undef]) @@ -504,7 +431,7 @@ None sub _get_my_mtime($self, $interface) { if (exists $self->{parsed_config}{$interface}) { - return $self->{parsed_config}{$interface}{mtime}; + return $self->{parsed_config}{$interface}{INTERNAL_KEY_PREFIX. 'mtime'}; } else { return 0; @@ -522,7 +449,7 @@ sub _is_latest_data($self, $interface) { return $self->_get_my_mtime($interface) ge get_mtime($hot_path); } -sub _scan_for_new_interfaces($self) { +sub _sync_interfaces($self) { # check if there's maybe a new interface by comparing the file counts my ($conf_files, $count) = _get_all_conf_files($self->{wireguard_home}); if ($self->{n_conf_files} != $count) { @@ -535,18 +462,17 @@ sub _scan_for_new_interfaces($self) { } } } -} - -sub _is_disabled($ref_parsed_config_section) { - if (exists $ref_parsed_config_section->{disabled}) { - return $ref_parsed_config_section->{disabled} == TRUE; + # scan for deleted interfaces + for my $internal_interface (keys %{$self->{parsed_config}}) { + if (not -e $self->{parsed_config}{$internal_interface}{INTERNAL_KEY_PREFIX. 'config_path'}) { + warn "Interface `$internal_interface` has been deleted in the meantime" if $self->_has_changed($internal_interface); + delete $self->{parsed_config}{$internal_interface}; + } } - return FALSE; } - sub _calculate_sha1_from_section($ref_to_hash) { my %h = %{$ref_to_hash}; - return sha1_hex INTEGRITY_HASH_SALT . join '', map {$h{$_}} @{$ref_to_hash->{order}}; + return sha1_hex INTEGRITY_HASH_SALT . join '', map {$h{$_}} @{$ref_to_hash->{INTERNAL_KEY_PREFIX . 'order'}}; } =head3 calculate_sha_from_internal($interface, $identifier) diff --git a/lib/Wireguard/WGmeta/Wrapper/Show.pm b/lib/Wireguard/WGmeta/Wrapper/Show.pm index ffab9d4..fa7fc1e 100644 --- a/lib/Wireguard/WGmeta/Wrapper/Show.pm +++ b/lib/Wireguard/WGmeta/Wrapper/Show.pm @@ -34,7 +34,7 @@ use warnings FATAL => 'all'; use experimental 'signatures'; -our $VERSION = "0.0.0"; +our $VERSION = "0.3.0"; use constant FALSE => 0; use constant TRUE => 1; diff --git a/t/Data/wg0.conf b/t/Data/wg0.conf index 3c8e368..3cace2f 100644 --- a/t/Data/wg0.conf +++ b/t/Data/wg0.conf @@ -1,11 +1,3 @@ -# This config is generated and maintained by wg-meta. -# It is strongly recommended to edit this config only through a supporting wg-meta -# implementation (e.g the wg-meta cli interface) -# -# Changes to this header are always overwritten, you can add normal comments in [Peer] and [Interface] section though. -# -# Support and issue tracker: https://github.com/sirtoobii/wg-meta -#+Checksum = 3977796461 [Interface] #+Name = Interface 1 @@ -13,21 +5,19 @@ Address = 10.0.0.2/24, fdc9:281f:04d7:9ee9::2/64 ListenPort = 51888 PrivateKey = WG_0_PEER_B_PRIVATE_KEY -[Peer] -#+Name = testero -PublicKey = WG_0_PEER_A_PUBLIC_KEY -#+Alias = IPv6_only1 -PresharedKey = PEER_A-PEER_B-PRESHARED_KEY -AllowedIPs = fdc9:281f:04d7:9ee9::1/128 -Endpoint = 198.51.100.101:51871 +#-[Peer] +#-#+name = testero +#-PublicKey = WG_0_PEER_A_PUBLIC_KEY +#-#+Alias = IPv6_only1 +#-PresharedKey = PEER_A-PEER_B-PRESHARED_KEY +#-AllowedIPs = fdc9:281f:04d7:9ee9::1/128 +#-Endpoint = 198.51.100.101:51871 [Peer] #Some other random comment at line 1 #+Name = testero2 -#+Alias = IPv6_only2 #Some more commentary going on here at line 5 PublicKey = DtGrNBijn+o91/tTNBx9EbDqC0552tuzruY14Ii5mi4= -PresharedKey = PEER_B-PEER_C-PRESHARED_KEY AllowedIPs = fdc9:281f:04d7:9ee9::3/128 [Peer] @@ -38,6 +28,5 @@ PublicKey = WG_0_PEER_D_PUBLIC_KEY # multiple lines (line 4,5) PresharedKey = PEER_B-PEER_C-PRESHARED_KEY AllowedIPs = fdc9:281f:04d7:9ee9::3/128 -#+Disabled = 0 #+Name = hellop - +#+wefwef = 12345+ diff --git a/t/cli_test.t b/t/cli_test.t index 9c37afd..2282f6c 100644 --- a/t/cli_test.t +++ b/t/cli_test.t @@ -22,7 +22,8 @@ my $initial_wg1 = read_file(TEST_DIR.'mini_wg1.conf'); # set command -my $expected = '[Interface] +my $expected = ' +[Interface] Address = 10.0.6.0/24 ListenPort = 51860 PrivateKey = WG_1_PEER_B_PRIVATE_KEY @@ -40,7 +41,6 @@ PublicKey = WG_1_PEER_B_PUBLIC_KEY PresharedKey = WG_1_PEER_B-PEER_B-PRESHARED_KEY AllowedIPs = 10.0.0.2/32 Endpoint = 198.51.100.102:51871 - '; # mixed cmd line my @cmd_line = qw(set mini_wg1 address 10.0.6.0/24 peer WG_1_PEER_A_PUBLIC_KEY endpoint test.tester.com:11234 allowed-ips 10.0.6.10/32); @@ -51,7 +51,8 @@ ok $actual eq $expected, 'set command mixed'; -$expected = '[Interface] +$expected = ' +[Interface] Address = 10.0.6.0/24 ListenPort = 51860 PrivateKey = WG_1_PEER_B_PRIVATE_KEY @@ -62,7 +63,7 @@ PublicKey = WG_1_PEER_A_PUBLIC_KEY PresharedKey = WG_1_PEER_A-PEER_B-PRESHARED_KEY AllowedIPs = 10.0.6.11/32 Endpoint = test.test1.com:8324 -#+Name = set_through_cli +#+name = set_through_cli [Peer] PublicKey = WG_1_PEER_B_PUBLIC_KEY @@ -70,16 +71,21 @@ PublicKey = WG_1_PEER_B_PUBLIC_KEY PresharedKey = WG_1_PEER_B-PEER_B-PRESHARED_KEY AllowedIPs = 10.0.0.2/32 Endpoint = 198.51.100.102:51871 - '; # just peers but also with alias @cmd_line = qw(set mini_wg1 peer WG_1_PEER_A_PUBLIC_KEY name set_through_cli allowed-ips 10.0.6.11/32 peer Alias1 endpoint test.test1.com:8324); +eval { + route_command(\@cmd_line); +} or ok 1, 'set unknown attribute w/o prefix'; + +@cmd_line = qw(set mini_wg1 peer WG_1_PEER_A_PUBLIC_KEY +name set_through_cli allowed-ips 10.0.6.11/32 peer Alias1 endpoint test.test1.com:8324); route_command(\@cmd_line); $actual = read_file(TEST_DIR . 'mini_wg1.conf'); -ok $actual eq $expected, 'set command peers with alias'; +ok $actual eq $expected, 'set command peers with alias and prefix'; -$expected = '[Interface] +$expected = ' +[Interface] Address = 10.0.6.0/24 ListenPort = 51860 PrivateKey = WG_1_PEER_B_PRIVATE_KEY @@ -90,8 +96,7 @@ PrivateKey = WG_1_PEER_B_PRIVATE_KEY #-PresharedKey = WG_1_PEER_A-PEER_B-PRESHARED_KEY #-AllowedIPs = 10.0.6.11/32 #-Endpoint = test.test1.com:8324 -#-#+Name = set_through_cli -#-#+Disabled = 1 +#-#+name = set_through_cli [Peer] PublicKey = WG_1_PEER_B_PUBLIC_KEY @@ -99,7 +104,6 @@ PublicKey = WG_1_PEER_B_PUBLIC_KEY PresharedKey = WG_1_PEER_B-PEER_B-PRESHARED_KEY AllowedIPs = 10.0.0.2/32 Endpoint = 198.51.100.102:51871 - '; # disable a peer @@ -109,7 +113,8 @@ route_command(\@cmd_line); $actual = read_file(TEST_DIR . 'mini_wg1.conf'); ok $actual eq $expected, 'disable peer'; -$expected = '[Interface] +$expected = ' +[Interface] Address = 10.0.6.0/24 ListenPort = 51860 PrivateKey = WG_1_PEER_B_PRIVATE_KEY @@ -120,8 +125,7 @@ PublicKey = WG_1_PEER_A_PUBLIC_KEY PresharedKey = WG_1_PEER_A-PEER_B-PRESHARED_KEY AllowedIPs = 10.0.6.11/32 Endpoint = test.test1.com:8324 -#+Name = set_through_cli -#+Disabled = 0 +#+name = set_through_cli [Peer] PublicKey = WG_1_PEER_B_PUBLIC_KEY @@ -129,7 +133,6 @@ PublicKey = WG_1_PEER_B_PUBLIC_KEY PresharedKey = WG_1_PEER_B-PEER_B-PRESHARED_KEY AllowedIPs = 10.0.0.2/32 Endpoint = 198.51.100.102:51871 - '; # enable a peer diff --git a/t/concurrency_test.t b/t/concurrency_test.t index f225a4b..871a356 100644 --- a/t/concurrency_test.t +++ b/t/concurrency_test.t @@ -1,6 +1,8 @@ #!/usr/bin/perl use strict; -use warnings FATAL => 'all'; +use warnings; +use Test::More; + use FindBin; use lib "$FindBin::Bin/../lib"; use lib "$FindBin::Bin/../thirdparty/lib/perl5"; @@ -11,361 +13,93 @@ use Time::HiRes qw(usleep); use Wireguard::WGmeta::Wrapper::ConfigT; use Wireguard::WGmeta::Utils; +my $unknown_handler = sub($attribute, $value) { + # Since unknown attribute handling is tested separately, we can safely ignore it + return $attribute, $value; +}; use constant TEST_DIR => $FindBin::Bin . '/test_data/'; -my $THREADS_PRESENT; -BEGIN { - eval { - require threads; - threads->import(); - require threads::shared; - threads::shared->import(); - $THREADS_PRESENT = 1; - }; -} - my $initial_wg0 = read_file(TEST_DIR . 'mini_wg0.conf'); my $initial_wg1 = read_file(TEST_DIR . 'mini_wg1.conf'); -my $wg_meta_outside = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - -$wg_meta_outside->add_peer('mini_wg1', 'added_outside_thread', '10.0.3.56/32', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD', 'alias_out'); -$wg_meta_outside->commit(1); - -my $sync :shared; -my $thread_no_error :shared = 1; - -sub thread_tester($test_name, $thread_name, $ref_function) { - eval { - # unfortunately sleep is needed here otherwise it could be the case that we're just too fast, - # which means the timestamps of the file stay the same (even with hires!) - usleep 50000; - &{$ref_function}(); - }; - if ($@) { - print "Test `$test_name` failed inside thread `$thread_name`:, $@ \n"; - $thread_no_error = 0; - return 1; - } -} - -if (defined $THREADS_PRESENT) { - - my $thr1 = threads->create(\&run_in_thread_1); - my $thr2 = threads->create(\&run_in_thread_2); - $thr1->join(); - $thr2->join(); - - sub run_in_thread_1 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - my %integrity_hashes; - thread_tester('modify peer merge', '1', (sub() { - %integrity_hashes = ( - 'WG_1_PEER_A_PUBLIC_KEY' => $wg_meta_t->calculate_sha_from_internal('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY') - ); - $wg_meta_t->set('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY', 'name', 'Name_set_in_thread_1'); - })); - cond_wait $sync; - thread_tester('delayed commit', '1', (sub() { - $wg_meta_t->commit(1, 0, \%integrity_hashes); - })); - - } - sub run_in_thread_2 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - my %integrity_hashes; - thread_tester('modify peer merge', '2', (sub() { - %integrity_hashes = ( - 'PUBLIC_KEY_PEER_OUTSIDE_THREAD' => $wg_meta_t->calculate_sha_from_internal('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD') - ); - $wg_meta_t->set('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD', 'name', 'Name_set_in_thread_2'); - $wg_meta_t->commit(1, 0, \%integrity_hashes); - })); - cond_signal $sync; - } - my $expected_after_thread = "# This config is generated and maintained by wg-meta. -# It is strongly recommended to edit this config only through a supporting wg-meta -# implementation (e.g the wg-meta cli interface) -# -# Changes to this header are always overwritten, you can add normal comments in [Peer] and [Interface] section though. -# -# Support and issue tracker: https://github.com/sirtoobii/wg-meta -#+Checksum = 974226613 - -[Interface] -Address = 10.0.0.2/24 -ListenPort = 51860 -PrivateKey = WG_1_PEER_B_PRIVATE_KEY - -[Peer] -PublicKey = WG_1_PEER_A_PUBLIC_KEY -#+Alias = Alias1 -PresharedKey = WG_1_PEER_A-PEER_B-PRESHARED_KEY -AllowedIPs = 10.0.0.1/32 -Endpoint = 198.51.100.101:51871 -#+Name = Name_set_in_thread_1 +my $wg_meta1 = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); +my $wg_meta2 = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); -[Peer] -PublicKey = WG_1_PEER_B_PUBLIC_KEY -#+Alias = Alias2 -PresharedKey = WG_1_PEER_B-PEER_B-PRESHARED_KEY -AllowedIPs = 10.0.0.2/32 -Endpoint = 198.51.100.102:51871 +$wg_meta1->add_peer('mini_wg1', '10.0.3.56/32', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD', 'alias_out'); +$wg_meta1->commit(1, 1); -[Peer] -#+Name = Name_set_in_thread_2 -PublicKey = PUBLIC_KEY_PEER_OUTSIDE_THREAD -AllowedIPs = 10.0.3.56/32 -#+Alias = alias_out +ok $wg_meta2->is_valid_identifier('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD'), 'added from other instance [Pub-Key]'; +ok $wg_meta2->is_valid_alias('mini_wg1', 'alias_out'), 'added from other instance [Alias]'; -"; - ok((read_file(TEST_DIR . 'mini_wg1.conf') eq $expected_after_thread) && $thread_no_error, 'Thread merge_modify'); - my $test_result :shared = 0; - $thread_no_error = 1; - my $thr3 = threads->create(\&run_in_thread_3); - my $thr4 = threads->create(\&run_in_thread_4); - $thr3->join(); - $thr4->join(); - ok $test_result, 'Thread modify conflict'; +# concurrent edit (non conflicting) +my %integrity_hashes1 = ( + 'PUBLIC_KEY_PEER_OUTSIDE_THREAD' => $wg_meta1->calculate_sha_from_internal('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD') +); +$wg_meta1->set('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD', 'name', 'Set by instance 1', $unknown_handler); +my %integrity_hashes2 = ( + 'WG_1_PEER_A_PUBLIC_KEY' => $wg_meta2->calculate_sha_from_internal('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY') +); +$wg_meta2->set('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY', 'name', 'Set by instance 2', $unknown_handler); +$wg_meta2->commit(1, 1, \%integrity_hashes2); +$wg_meta1->commit(1, 1, \%integrity_hashes1); - sub run_in_thread_3 { - local $SIG{__DIE__} = sub { - $test_result = 1; - }; - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - my %integrity_hashes; - thread_tester('merge conflict', '3', (sub() { - %integrity_hashes = ( - 'PUBLIC_KEY_PEER_OUTSIDE_THREAD' => $wg_meta_t->calculate_sha_from_internal('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD') - ); - $wg_meta_t->set('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD', 'name', 'Name_set_in_thread_3'); - })); - cond_wait $sync; - thread_tester('merge conflict commit', '3', (sub() { - eval { - $wg_meta_t->commit(1, 0, \%integrity_hashes); - }; - })); +ok { $wg_meta2->get_interface_section('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD') }->{name} eq 'Set by instance 1', 'concurrent edit [1]'; +ok { $wg_meta1->get_interface_section('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY') }->{name} eq 'Set by instance 2', 'concurrent edit [2]'; - } - sub run_in_thread_4 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - my %integrity_hashes; - thread_tester('merge conflict commit other', '4', (sub() { - %integrity_hashes = ( - 'PUBLIC_KEY_PEER_OUTSIDE_THREAD' => $wg_meta_t->calculate_sha_from_internal('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD') - ); - $wg_meta_t->set('mini_wg1', 'PUBLIC_KEY_PEER_OUTSIDE_THREAD', 'name', 'Name_set_in_thread_4'); - $wg_meta_t->commit(1, 0, \%integrity_hashes); - })); - cond_signal $sync; - } - $thread_no_error = 1; - my $thr5 = threads->create(\&run_in_thread_5); - my $thr6 = threads->create(\&run_in_thread_6); - $thr5->join(); - $thr6->join(); +# concurrent edit (conflict) +%integrity_hashes1 = ( + 'WG_1_PEER_A_PUBLIC_KEY' => $wg_meta1->calculate_sha_from_internal('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY') +); +$wg_meta1->set('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY', 'name', 'Set by instance 1 [2]'); - sub run_in_thread_5 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - cond_wait $sync; - thread_tester('the lost peer', '5', (sub() { - $wg_meta_t->commit(1); - })); - } - sub run_in_thread_6 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - thread_tester('the new peer', '5', (sub() { - $wg_meta_t->add_peer('mini_wg0', 'peer_added_in_thread_6', '10.0.5.56/32', 'PUBLIC_KEY_PEER_ADDED_6', 'alias_thread6'); - $wg_meta_t->commit(1); - })); - cond_signal $sync; - } +%integrity_hashes2 = ( + 'WG_1_PEER_A_PUBLIC_KEY' => $wg_meta2->calculate_sha_from_internal('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY') +); +$wg_meta2->set('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY', 'name', 'Set by instance 2 [2]'); +$wg_meta2->commit(1, 1, \%integrity_hashes2); +eval { + $wg_meta1->commit(1, 1, \%integrity_hashes1); +} or ok 1, 'concurrent edit [3]'; - my $expected_after_th56 = '# This config is generated and maintained by wg-meta. -# It is strongly recommended to edit this config only through a supporting wg-meta -# implementation (e.g the wg-meta cli interface) -# -# Changes to this header are always overwritten, you can add normal comments in [Peer] and [Interface] section though. -# -# Support and issue tracker: https://github.com/sirtoobii/wg-meta -#+Checksum = 2806865288 -[Interface] -Address = 10.0.0.2/24, fdc9:281f:04d7:9ee9::2/64 -ListenPort = 51888 -PrivateKey = WG_0_PEER_B_PRIVATE_KEY +# the story of the (not) lost peer... +$wg_meta1->add_peer('mini_wg0', '10.0.5.56/32', 'PUBLIC_KEY_PEER_ADDED_5', 'alias_peer5'); +$wg_meta2->add_peer('mini_wg0', '10.0.5.57/32', 'PUBLIC_KEY_PEER_ADDED_6', 'alias_peer6'); +$wg_meta1->commit(1); +$wg_meta2->commit(1); -[Peer] -PublicKey = WG_0_PEER_A_PUBLIC_KEY -PresharedKey = PEER_A-PEER_B-PRESHARED_KEY -#A normal comment -Custom_attr_from_very_custom_implementation = Some crazy value with spaces :D -AllowedIPs = fdc9:281f:04d7:9ee9::1/128 -Endpoint = 198.51.100.101:51871 +ok $wg_meta1->is_valid_identifier('mini_wg0', 'PUBLIC_KEY_PEER_ADDED_6'), 'the (not) lost peer [1]'; +ok $wg_meta2->is_valid_identifier('mini_wg0', 'PUBLIC_KEY_PEER_ADDED_5'), 'the (not) lost peer [2]'; -[Peer] -#+Name = peer_added_in_thread_6 -PublicKey = PUBLIC_KEY_PEER_ADDED_6 -AllowedIPs = 10.0.5.56/32 -#+Alias = alias_thread6 -'; +# ping pong - ok((read_file(TEST_DIR . 'mini_wg0.conf') eq $expected_after_th56 && $thread_no_error), 'Thread, adding peer'); +$wg_meta1->add_interface('thread_iface1', '192.168.1.0/24', 8123, 'THREAD_IFACE1_PRIV_KEY'); +$wg_meta1->commit(1); - my ($filename_1, $filename_2) = (TEST_DIR . 'mini_wg1.conf', TEST_DIR . 'mini_wg0.conf'); - write_file($filename_1, $initial_wg1); - write_file($filename_2, $initial_wg0); +my @actual = $wg_meta2->get_interface_list(); +ok eq_array(\@actual, [ 'mini_wg0', 'mini_wg1', 'thread_iface1' ]), 'add interface'; - $thread_no_error = 1; - my $thr7 = threads->create(\&run_in_thread_7); - my $thr8 = threads->create(\&run_in_thread_8); - $thr7->join(); - $thr8->join(); +$wg_meta2->add_peer('thread_iface1', '192.168.2.10/32', 'PEER_IFACE2_PUB_KEY', 'alias_thread_iface2'); +$wg_meta2->commit(1); - sub run_in_thread_7 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - thread_tester('add peer', '7', (sub() { - $wg_meta_t->add_peer('mini_wg0', 'peer_added_in_thread_7', '10.0.5.56/32', 'PUBLIC_KEY_PEER_ADDED_7', 'alias_thread7'); - })); - cond_wait $sync; - thread_tester('add peer commit', '7', (sub() { - $wg_meta_t->commit(1); - })); - } - sub run_in_thread_8 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - thread_tester('add peer and commit', '8', (sub() { - $wg_meta_t->add_peer('mini_wg0', 'peer_added_in_thread_8', '10.0.5.56/32', 'PUBLIC_KEY_PEER_ADDED_8', 'alias_thread8'); - $wg_meta_t->commit(1); - })); - cond_signal $sync; - } +@actual = $wg_meta1->get_section_list('thread_iface1'); +ok eq_array \@actual, [ 'thread_iface1', 'PEER_IFACE2_PUB_KEY' ], 'add peer to new interface'; - my $expected_after_th78 = '# This config is generated and maintained by wg-meta. -# It is strongly recommended to edit this config only through a supporting wg-meta -# implementation (e.g the wg-meta cli interface) -# -# Changes to this header are always overwritten, you can add normal comments in [Peer] and [Interface] section though. -# -# Support and issue tracker: https://github.com/sirtoobii/wg-meta -#+Checksum = 3707844997 +$wg_meta1->remove_peer('thread_iface1', 'PEER_IFACE2_PUB_KEY'); +$wg_meta1->commit(1); -[Interface] -Address = 10.0.0.2/24, fdc9:281f:04d7:9ee9::2/64 -ListenPort = 51888 -PrivateKey = WG_0_PEER_B_PRIVATE_KEY +@actual = $wg_meta2->get_section_list('thread_iface1'); +ok eq_array \@actual, [ 'thread_iface1' ], 'remove peer from new interface'; -[Peer] -PublicKey = WG_0_PEER_A_PUBLIC_KEY -PresharedKey = PEER_A-PEER_B-PRESHARED_KEY -#A normal comment -Custom_attr_from_very_custom_implementation = Some crazy value with spaces :D -AllowedIPs = fdc9:281f:04d7:9ee9::1/128 -Endpoint = 198.51.100.101:51871 +$wg_meta2->remove_interface('thread_iface1'); +$wg_meta2->commit(1); -[Peer] -#+Name = peer_added_in_thread_7 -PublicKey = PUBLIC_KEY_PEER_ADDED_7 -AllowedIPs = 10.0.5.56/32 -#+Alias = alias_thread7 - -[Peer] -#+Name = peer_added_in_thread_8 -PublicKey = PUBLIC_KEY_PEER_ADDED_8 -AllowedIPs = 10.0.5.56/32 -#+Alias = alias_thread8 - -'; - - ok((read_file(TEST_DIR . 'mini_wg0.conf') eq $expected_after_th78) && $thread_no_error, 'Thread, adding peer [both]'); - - my $ping_pong_result :shared = 1; - $thread_no_error = 1; - my $thr9 = threads->create(\&run_in_thread_9, 'Thread 9'); - my $thr10 = threads->create(\&run_in_thread_10, 'Thread 10'); - $thr9->join(); - $thr10->join(); - - sub run_in_thread_9 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - thread_tester('add interface 1', '9', (sub() { - $wg_meta_t->add_interface('thread_iface1', '192.168.1.0/24', 8123, 'THREAD_IFACE1_PRIV_KEY'); - $wg_meta_t->commit(1); - })); - cond_wait $sync; - thread_tester('add interface 2', '9', (sub() { - $wg_meta_t->add_interface('thread_iface2', '192.168.2.0/24', 8125, 'THREAD_IFACE2_PRIV_KEY'); - $wg_meta_t->commit(1); - })); - cond_signal $sync; - cond_wait $sync; - thread_tester('add peer to interface 2', '9', (sub() { - $wg_meta_t->add_peer('thread_iface2', 'PEER_THREAD_IFACE_2', '192.168.2.10/32', 'PEER_IFACE2_PUB_KEY', 'alias_thread_iface2'); - $wg_meta_t->commit(1); - })); - cond_signal $sync; - cond_wait $sync; - thread_tester('remove peer', '9', (sub() { - $wg_meta_t->remove_peer('thread_iface2', 'PEER_IFACE2_PUB_KEY'); - $wg_meta_t->commit(1); - })); - cond_signal $sync; - return 1; - } - - sub run_in_thread_10 { - lock $sync; - my $wg_meta_t = Wireguard::WGmeta::Wrapper::ConfigT->new(TEST_DIR); - thread_tester('get interface list 1', '10', (sub() { - my @actual = $wg_meta_t->get_interface_list(); - $ping_pong_result &= eq_array(\@actual, [ 'mini_wg0', 'mini_wg1', 'thread_iface1' ]); - })); - cond_signal $sync; - cond_wait $sync; - thread_tester('get interface list 2', '10', (sub() { - my @actual = $wg_meta_t->get_interface_list(); - $ping_pong_result &= eq_array(\@actual, [ "mini_wg0", "mini_wg1", "thread_iface1", "thread_iface2" ]); - })); - cond_signal $sync; - cond_wait $sync; - thread_tester('get section list', '10', (sub() { - my @actual = $wg_meta_t->get_section_list('thread_iface2'); - $ping_pong_result &= eq_array \@actual, [ 'thread_iface2', 'PEER_IFACE2_PUB_KEY' ]; - $ping_pong_result &= $wg_meta_t->try_translate_alias('thread_iface2', 'alias_thread_iface2') eq 'PEER_IFACE2_PUB_KEY'; - })); - cond_signal $sync; - cond_wait $sync; - thread_tester('get section list after remove', '10', (sub() { - my @actual = $wg_meta_t->get_section_list('thread_iface2'); - $ping_pong_result &= eq_array \@actual, [ 'thread_iface2' ]; - })); - return 1; - - } - - ok $ping_pong_result && $thread_no_error, 'Thread ping-pong'; - - $wg_meta_outside->remove_interface('thread_iface2'); - $wg_meta_outside->remove_interface('thread_iface1'); - -} -else { - ok 1, 'skip....no thread support present'; -} +@actual = $wg_meta1->get_interface_list(); +ok eq_array(\@actual, [ 'mini_wg0', 'mini_wg1' ]), 'removed interface'; # write back initial configs my ($filename_1, $filename_2) = (TEST_DIR . 'mini_wg1.conf', TEST_DIR . 'mini_wg0.conf'); @@ -374,5 +108,3 @@ write_file($filename_2, $initial_wg0); done_testing(); - - diff --git a/t/wrapper_custom_attrs.t b/t/wrapper_custom_attrs.t index 6acd0f7..d1824bc 100644 --- a/t/wrapper_custom_attrs.t +++ b/t/wrapper_custom_attrs.t @@ -23,18 +23,18 @@ sub dummy_val($value) { my $custom_attr_config = { 'email' => { - 'in_config_name' => 'Email', 'validator' => \&dummy_val } }; my $wg_meta = Wireguard::WGmeta::Wrapper::Config->new(TEST_DIR, '#+', '#-', '.not_applied', $custom_attr_config); -$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'email', 'test@test.com', 1); +$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'email', 'test@test.com'); ok $validator_called, 'validator called'; -my $expected = '[Interface] +my $expected = ' +[Interface] Address = 10.0.0.2/24, fdc9:281f:04d7:9ee9::2/64 ListenPort = 51888 PrivateKey = WG_0_PEER_B_PRIVATE_KEY @@ -46,11 +46,10 @@ PresharedKey = PEER_A-PEER_B-PRESHARED_KEY Custom_attr_from_very_custom_implementation = Some crazy value with spaces :D AllowedIPs = fdc9:281f:04d7:9ee9::1/128 Endpoint = 198.51.100.101:51871 -#+Email = test@test.com - +#+email = test@test.com '; -my $actual = $wg_meta->_create_config('mini_wg0', 1); +my $actual = $wg_meta->create_config('mini_wg0', 1); ok $actual eq $expected, 'generate config'; $wg_meta->commit(1,1); diff --git a/t/wrapper_secure_mode.t b/t/wrapper_secure_mode.t index ff23f0d..523a9ec 100644 --- a/t/wrapper_secure_mode.t +++ b/t/wrapper_secure_mode.t @@ -15,10 +15,14 @@ use constant TEST_DIR => $FindBin::Bin . '/test_data/'; my $initial_wg0 = read_file(TEST_DIR . 'mini_wg0.conf'); my $initial_wg1 = read_file(TEST_DIR . 'mini_wg1.conf'); +my $unknown_handler = sub($attribute, $value) { + # Since unknown attribute handling is tested separately, we can safely ignore it + return $attribute, $value; +}; my $wg_meta = Wireguard::WGmeta::Wrapper::Config->new(TEST_DIR); -$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'name', 'bli_blu'); +$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'name', 'bli_blu', $unknown_handler); $wg_meta->commit(0,1); $wg_meta->may_reload_from_disk('mini_wg0'); diff --git a/t/wrapper_test.t b/t/wrapper_test.t index 6f934a9..23f9304 100644 --- a/t/wrapper_test.t +++ b/t/wrapper_test.t @@ -6,6 +6,7 @@ use lib "$FindBin::Bin/../lib"; use lib "$FindBin::Bin/../thirdparty/lib/perl5"; use experimental 'signatures'; use Test::More; +use Data::Dumper; use Wireguard::WGmeta::Wrapper::Config; use Wireguard::WGmeta::Utils; @@ -17,6 +18,10 @@ my $initial_wg1 = read_file(TEST_DIR . 'mini_wg1.conf'); my $wg_meta = Wireguard::WGmeta::Wrapper::Config->new(TEST_DIR); +my $unknown_handler = sub($attribute, $value) { + # Since unknown attribute handling is tested separately, we can safely ignore it + return $attribute, $value; +}; # parser tests # interfaces @@ -45,7 +50,8 @@ ok $wg_meta->get_peer_count() == 3, 'peer count [all]'; ok $wg_meta->get_peer_count('mini_wg1') == 2, 'peer count [wg1]'; # set -my $expected = '[Interface] +my $expected = ' +[Interface] Address = 10.0.0.2/24, fdc9:281f:04d7:9ee9::2/64 ListenPort = 60000 PrivateKey = OHLK9lBHFqnu+9olAnyUN11pCeKP4uW6fwMAeRSy2F8= @@ -57,72 +63,73 @@ PresharedKey = PEER_A-PEER_B-PRESHARED_KEY Custom_attr_from_very_custom_implementation = Some crazy value with spaces :D AllowedIPs = fdc9:281f:04d7:9ee9::1/128 Endpoint = 198.51.100.101:60001 -#+Name = Name_by_test1 +#+name = Name_by_test1 #+Alias = alias2 - '; # normal attributes (mixed type) -$wg_meta->set('mini_wg0', 'mini_wg0', 'listen-port', 60000, 1); -$wg_meta->set('mini_wg0', 'mini_wg0', 'private-key', 'OHLK9lBHFqnu+9olAnyUN11pCeKP4uW6fwMAeRSy2F8=', 1); -$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'endpoint', '198.51.100.101:60001', 1); +$wg_meta->set('mini_wg0', 'mini_wg0', 'listen-port', 60000); +$wg_meta->set('mini_wg0', 'mini_wg0', 'private-key', 'OHLK9lBHFqnu+9olAnyUN11pCeKP4uW6fwMAeRSy2F8='); +$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'endpoint', '198.51.100.101:60001'); # wg-meta attrs -$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'name', 'Name_by_test1'); +$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'name', 'Name_by_test1', $unknown_handler); $wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'alias', 'alias1'); # wg-meta update alias by alias -$wg_meta->set_by_alias('mini_wg0', 'alias1', 'alias', 'alias2'); +$wg_meta->set('mini_wg0', 'alias1', 'alias', 'alias2'); -my $actual = $wg_meta->_create_config('mini_wg0', 1); +my $actual = $wg_meta->create_config('mini_wg0', 1); ok $actual eq $expected, 'set valid attrs'; # add peer -my ($iface_privkey, $iface_listen) = $wg_meta->add_peer('mini_wg0', 'added_peer', '10.0.0.9/32', 'sa9sXzMC5h4oE+38M38D1bcakH7nQBChAN1ib30lODc='); +my ($iface_privkey, $iface_listen) = $wg_meta->add_peer('mini_wg0', '10.0.0.9/32', 'sa9sXzMC5h4oE+38M38D1bcakH7nQBChAN1ib30lODc='); ok $iface_privkey eq 'OHLK9lBHFqnu+9olAnyUN11pCeKP4uW6fwMAeRSy2F8=', 'add peer, priv-key'; ok $iface_listen eq '60000', 'add peer, listen-port'; -$expected .= '[Peer] -#+Name = added_peer +$expected .= ' +[Peer] PublicKey = sa9sXzMC5h4oE+38M38D1bcakH7nQBChAN1ib30lODc= AllowedIPs = 10.0.0.9/32 #+Alias = new_peer - '; # and set an alias on this new peer $wg_meta->set('mini_wg0', 'sa9sXzMC5h4oE+38M38D1bcakH7nQBChAN1ib30lODc=', 'alias', 'new_peer'); - -$actual = $wg_meta->_create_config('mini_wg0', 1); +$actual = $wg_meta->create_config('mini_wg0', 1); ok $actual eq $expected, 'add peer, content'; -$expected = '[Interface] +$expected = ' +[Interface] Address = 10.0.0.2/24 ListenPort = 51860 PrivateKey = WG_1_PEER_B_PRIVATE_KEY -[Peer] -PublicKey = WG_1_PEER_B_PUBLIC_KEY -#+Alias = Alias2 -PresharedKey = WG_1_PEER_B-PEER_B-PRESHARED_KEY -AllowedIPs = 10.0.0.2/32 -Endpoint = 198.51.100.102:51871 - +#-[Peer] +#-PublicKey = WG_1_PEER_B_PUBLIC_KEY +#-#+Alias = Alias2 +#-PresharedKey = WG_1_PEER_B-PEER_B-PRESHARED_KEY +#-AllowedIPs = 10.0.0.2/32 +#-Endpoint = 198.51.100.102:51871 '; +# enable disable peer +$wg_meta->disable('mini_wg1','Alias2'); +$wg_meta->enable('mini_wg1', 'WG_1_PEER_B_PUBLIC_KEY'); +$wg_meta->disable('mini_wg1','WG_1_PEER_B_PUBLIC_KEY'); + # remove peer $wg_meta->remove_peer('mini_wg1', 'WG_1_PEER_A_PUBLIC_KEY'); -$actual = $wg_meta->_create_config('mini_wg1', 1); +$actual = $wg_meta->create_config('mini_wg1', 1); ok $actual eq $expected, 'removed peer, content'; # check peer count after removal ok $wg_meta->get_peer_count('mini_wg1') == 1, 'peer count after removal [wg1]'; # test if the alias got removed too -does_throw('access deleted alias', (sub(@args) {$wg_meta->translate_alias(@args)}), ('mini_wg1', 'Alias1')); - +ok !$wg_meta->is_valid_alias('mini_wg1', 'Alias1'), 'access deleted alias'; # try to an alias which is already present -does_throw('alias already known', \&set_wrapper, ('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'alias', 'alias1', 1)); +does_throw('alias already known', \&set_wrapper, ('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'alias', 'alias1')); # remove interface $wg_meta->remove_interface('mini_wg1'); @@ -130,12 +137,6 @@ $wg_meta->remove_interface('mini_wg1'); my @expected = ('mini_wg0'); ok eq_array(\@output, \@expected), 'remove interface'; -# forwarder test -$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'listen-port', 12345, 0, \&_forward); -sub _forward($interface, $identifier, $attribute, $value) { - ok $interface eq 'mini_wg0' && $identifier eq 'WG_0_PEER_A_PUBLIC_KEY' && $attribute eq 'listen-port' && $value == 12345, 'set forward_fun'; -} - # reload listener test my $listener_result = 0; sub _my_reload_listener($interface, $ref_args) { @@ -149,11 +150,16 @@ $wg_meta->may_reload_from_disk('mini_wg0', 0, 1); ok $listener_result, 'Reload listener'; -# no forwarder test -does_throw('no-meta w/o forwarder', \&set_wrapper, ('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'listen-port', 12345, 0)); +# Unknown attribute callback +my $h_res = 0; +$wg_meta->set('mini_wg0', 'mini_wg0', 't1', 'tv', sub($attr, $val){$h_res = 1 if ($attr eq 't1' and $val eq 'tv');}); -# invalid attr name -does_throw('invalid attr name', \&set_wrapper, ('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'invalid_name', 12345, 1)); +ok $h_res, 'Unknown attr callback [Interface]'; + +$h_res = 0; +$wg_meta->set('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 't2', 'tv', sub($attr, $val){$h_res = 1 if ($attr eq 't2' and $val eq 'tv');}); + +ok $h_res, 'Unknown attr callback [Peer]'; # try to set a non peer attribute on a peer does_throw('non peer attribute on peer', \&set_wrapper, ('mini_wg0', 'WG_0_PEER_A_PUBLIC_KEY', 'listen-port', 5000, 1)); @@ -185,9 +191,6 @@ write_file($filename_2, $initial_wg0); sub set_wrapper(@args) { $wg_meta->set(@args); } -sub set_alias_wrapper(@args) { - $wg_meta->set_by_alias(@args); -} sub does_throw($test_name, $fun, @args) { my $ok = 0;