From 4ea58fba75dc86169a8fef6ea8932b38854c1612 Mon Sep 17 00:00:00 2001 From: Johan Wennerberg Date: Sat, 8 Jan 2022 14:13:50 +0100 Subject: [PATCH] nmcli: Add wireguard connection type support (#3985) * nmcli: add wireguard connection type * nmcli: fix wireguard unit tests * nmcli: set ipv4.method to disabled if ip4 not set Method 'auto' is not supported for WireGuard * nmcli: add wireguard documentation * nmcli: clean up wireguard documentation * nmcli: add wireguard changelog fragment * nmcli: fix wireguard documentation * Apply suggestions from code review Co-authored-by: Andrew Pantuso Co-authored-by: Andrew Pantuso --- ...85-nmcli-add-wireguard-connection-type.yml | 2 + plugins/modules/net_tools/nmcli.py | 85 ++++++++++- .../plugins/modules/net_tools/test_nmcli.py | 134 ++++++++++++++++++ 3 files changed, 218 insertions(+), 3 deletions(-) create mode 100644 changelogs/fragments/3985-nmcli-add-wireguard-connection-type.yml diff --git a/changelogs/fragments/3985-nmcli-add-wireguard-connection-type.yml b/changelogs/fragments/3985-nmcli-add-wireguard-connection-type.yml new file mode 100644 index 0000000000..84bf7cf727 --- /dev/null +++ b/changelogs/fragments/3985-nmcli-add-wireguard-connection-type.yml @@ -0,0 +1,2 @@ +minor_changes: + - nmcli - add ``wireguard`` connection type (https://github.com/ansible-collections/community.general/pull/3985). diff --git a/plugins/modules/net_tools/nmcli.py b/plugins/modules/net_tools/nmcli.py index bb656b3a23..de00a1ea10 100644 --- a/plugins/modules/net_tools/nmcli.py +++ b/plugins/modules/net_tools/nmcli.py @@ -55,8 +55,10 @@ options: - Type C(generic) is added in Ansible 2.5. - Type C(infiniband) is added in community.general 2.0.0. - Type C(gsm) is added in community.general 3.7.0. + - Type C(wireguard) is added in community.general 4.3.0 type: str - choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi, gsm ] + choices: [ bond, bond-slave, bridge, bridge-slave, dummy, ethernet, generic, gre, infiniband, ipip, sit, team, team-slave, vlan, vxlan, wifi, gsm, + wireguard ] mode: description: - This is the type of device or network connection that you wish to create for a bond or bridge. @@ -754,6 +756,62 @@ options: - The username used to authenticate with the network, if required. - Many providers do not require a username, or accept any username. - But if a username is required, it is specified here. + wireguard: + description: + - The configuration of the Wireguard connection. + - Note the list of suboption attributes may vary depending on which version of NetworkManager/nmcli is installed on the host. + - 'An up-to-date list of supported attributes can be found here: + U(https://networkmanager.dev/docs/api/latest/settings-wireguard.html).' + - 'For instance to configure a listen port: + C({listen-port: 12345}).' + type: dict + version_added: 4.3.0 + suboptions: + fwmark: + description: + - The 32-bit fwmark for outgoing packets. + - The use of fwmark is optional and is by default off. Setting it to 0 disables it. + - Note that I(wireguard.ip4-auto-default-route) or I(wireguard.ip6-auto-default-route) enabled, implies to automatically choose a fwmark. + type: int + ip4-auto-default-route: + description: + - Whether to enable special handling of the IPv4 default route. + - If enabled, the IPv4 default route from I(wireguard.peer-routes) will be placed to a dedicated routing-table and two policy + routing rules will be added. + - The fwmark number is also used as routing-table for the default-route, and if fwmark is zero, an unused fwmark/table is chosen + automatically. This corresponds to what wg-quick does with Table=auto and what WireGuard calls "Improved Rule-based Routing" + type: bool + ip6-auto-default-route: + description: + - Like I(wireguard.ip4-auto-default-route), but for the IPv6 default route. + type: bool + listen-port: + description: The WireGuard connection listen-port. If not specified, the port will be chosen randomly when the + interface comes up. + type: int + mtu: + description: + - If non-zero, only transmit packets of the specified size or smaller, breaking larger packets up into multiple fragments. + - If zero a default MTU is used. Note that contrary to wg-quick's MTU setting, this does not take into account the current routes + at the time of activation. + type: int + peer-routes: + description: + - Whether to automatically add routes for the AllowedIPs ranges of the peers. + - If C(true) (the default), NetworkManager will automatically add routes in the routing tables according to C(ipv4.route-table) and + C(ipv6.route-table). Usually you want this automatism enabled. + - If C(false), no such routes are added automatically. In this case, the user may want to configure static routes in C(ipv4.routes) + and C(ipv6.routes), respectively. + - Note that if the peer's AllowedIPs is C(0.0.0.0/0) or C(::/0) and the profile's C(ipv4.never-default) or C(ipv6.never-default) + setting is enabled, the peer route for this peer won't be added automatically. + type: bool + private-key: + description: The 256 bit private-key in base64 encoding. + type: str + private-key-flags: + description: C(NMSettingSecretFlags) indicating how to handle the I(wireguard.private-key) property. + type: int + choices: [ 0, 1, 2 ] ''' EXAMPLES = r''' @@ -1126,6 +1184,17 @@ EXAMPLES = r''' autoconnect: true state: present +- name: Create a wireguard connection + community.general.nmcli: + type: wireguard + conn_name: my-wg-provider + ifname: mywg0 + wireguard: + listen-port: 51820 + private-key: my-private-key + autoconnect: true + state: present + ''' RETURN = r"""# @@ -1236,10 +1305,11 @@ class Nmcli(object): self.wifi = module.params['wifi'] self.wifi_sec = module.params['wifi_sec'] self.gsm = module.params['gsm'] + self.wireguard = module.params['wireguard'] if self.method4: self.ipv4_method = self.method4 - elif self.type == 'dummy' and not self.ip4: + elif self.type in ('dummy', 'wireguard') and not self.ip4: self.ipv4_method = 'disabled' elif self.ip4: self.ipv4_method = 'manual' @@ -1248,7 +1318,7 @@ class Nmcli(object): if self.method6: self.ipv6_method = self.method6 - elif self.type == 'dummy' and not self.ip6: + elif self.type in ('dummy', 'wireguard') and not self.ip6: self.ipv6_method = 'disabled' elif self.ip6: self.ipv6_method = 'manual' @@ -1404,6 +1474,12 @@ class Nmcli(object): options.update({ 'gsm.%s' % name: value, }) + elif self.type == 'wireguard': + if self.wireguard: + for name, value in self.wireguard.items(): + options.update({ + 'wireguard.%s' % name: value, + }) # Convert settings values based on the situation. for setting, value in options.items(): setting_type = self.settings_type(setting) @@ -1445,6 +1521,7 @@ class Nmcli(object): 'vlan', 'wifi', 'gsm', + 'wireguard', ) @property @@ -1834,6 +1911,7 @@ def main(): 'vxlan', 'wifi', 'gsm', + 'wireguard', ]), ip4=dict(type='list', elements='str'), gw4=dict(type='str'), @@ -1907,6 +1985,7 @@ def main(): wifi=dict(type='dict'), wifi_sec=dict(type='dict', no_log=True), gsm=dict(type='dict'), + wireguard=dict(type='dict'), ), mutually_exclusive=[['never_default4', 'gw4']], required_if=[("type", "wifi", [("ssid")])], diff --git a/tests/unit/plugins/modules/net_tools/test_nmcli.py b/tests/unit/plugins/modules/net_tools/test_nmcli.py index 499ea1e5ad..ead1551729 100644 --- a/tests/unit/plugins/modules/net_tools/test_nmcli.py +++ b/tests/unit/plugins/modules/net_tools/test_nmcli.py @@ -92,6 +92,12 @@ TESTCASE_CONNECTION = [ 'state': 'absent', '_ansible_check_mode': True, }, + { + 'type': 'wireguard', + 'conn_name': 'non_existent_nw_device', + 'state': 'absent', + '_ansible_check_mode': True, + }, ] TESTCASE_GENERIC = [ @@ -900,6 +906,49 @@ gsm.sim-operator-id: -- gsm.mtu: auto """ +TESTCASE_WIREGUARD = [ + { + 'type': 'wireguard', + 'conn_name': 'non_existent_nw_device', + 'ifname': 'wg_non_existant', + 'wireguard': { + 'listen-port': '51820', + 'private-key': '', + }, + 'method4': 'manual', + 'ip4': '10.10.10.10/24', + 'method6': 'manual', + 'ip6': '2001:db8::1/128', + 'state': 'present', + '_ansible_check_mode': False, + } +] + +TESTCASE_WIREGUARD_SHOW_OUTPUT = """\ +connection.id: non_existent_nw_device +connection.type: wireguard +connection.interface-name: wg_non_existant +connection.autoconnect: yes +ipv4.method: manual +ipv4.addresses: 10.10.10.10/24 +ipv4.never-default: no +ipv4.may-fail: yes +ipv4.ignore-auto-dns: no +ipv4.ignore-auto-routes: no +ipv6.method: manual +ipv6.addresses: 2001:db8::1/128 +ipv6.ignore-auto-dns: no +ipv6.ignore-auto-routes: no +wireguard.private-key: +wireguard.private-key-flags: 0 (none) +wireguard.listen-port: 51820 +wireguard.fwmark: 0x0 +wireguard.peer-routes: yes +wireguard.mtu: 0 +wireguard.ip4-auto-default-route: -1 (default) +wireguard.ip6-auto-default-route: -1 (default) +""" + def mocker_set(mocker, connection_exists=False, @@ -1201,6 +1250,13 @@ def mocked_gsm_connection_unchanged(mocker): execute_return=(0, TESTCASE_GSM_SHOW_OUTPUT, "")) +@pytest.fixture +def mocked_wireguard_connection_unchanged(mocker): + mocker_set(mocker, + connection_exists=True, + execute_return=(0, TESTCASE_WIREGUARD_SHOW_OUTPUT, "")) + + @pytest.mark.parametrize('patch_ansible_module', TESTCASE_BOND, indirect=['patch_ansible_module']) def test_bond_connection_create(mocked_generic_connection_create, capfd): """ @@ -2828,3 +2884,81 @@ def test_ethernet_connection_static_with_mulitple_ip4_addresses_unchanged(mocked results = json.loads(out) assert not results.get('failed') assert not results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_WIREGUARD, indirect=['patch_ansible_module']) +def test_create_wireguard(mocked_generic_connection_create, capfd): + """ + Test : Create wireguard connection with static IP configuration + """ + + with pytest.raises(SystemExit): + nmcli.main() + + assert nmcli.Nmcli.execute_command.call_count == 1 + arg_list = nmcli.Nmcli.execute_command.call_args_list + add_args, add_kw = arg_list[0] + + assert add_args[0][0] == '/usr/bin/nmcli' + assert add_args[0][1] == 'con' + assert add_args[0][2] == 'add' + assert add_args[0][3] == 'type' + assert add_args[0][4] == 'wireguard' + assert add_args[0][5] == 'con-name' + assert add_args[0][6] == 'non_existent_nw_device' + + add_args_text = list(map(to_text, add_args[0])) + for param in ['connection.interface-name', 'wg_non_existant', + 'ipv4.method', 'manual', + 'ipv4.addresses', '10.10.10.10/24', + 'ipv6.method', 'manual', + 'ipv6.addresses', '2001:db8::1/128', + 'wireguard.listen-port', '51820', + 'wireguard.private-key', '']: + assert param in add_args_text + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_WIREGUARD, indirect=['patch_ansible_module']) +def test_wireguard_connection_unchanged(mocked_wireguard_connection_unchanged, capfd): + """ + Test : Wireguard connection with static IP configuration unchanged + """ + with pytest.raises(SystemExit): + nmcli.main() + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert not results['changed'] + + +@pytest.mark.parametrize('patch_ansible_module', TESTCASE_WIREGUARD, indirect=['patch_ansible_module']) +def test_wireguard_mod(mocked_generic_connection_modify, capfd): + """ + Test : Modify wireguard connection + """ + with pytest.raises(SystemExit): + nmcli.main() + + assert nmcli.Nmcli.execute_command.call_count == 1 + arg_list = nmcli.Nmcli.execute_command.call_args_list + args, kwargs = arg_list[0] + + assert args[0][0] == '/usr/bin/nmcli' + assert args[0][1] == 'con' + assert args[0][2] == 'modify' + assert args[0][3] == 'non_existent_nw_device' + + args_text = list(map(to_text, args[0])) + for param in ['wireguard.listen-port', '51820']: + assert param in args_text + + out, err = capfd.readouterr() + results = json.loads(out) + assert not results.get('failed') + assert results['changed']