From 93e509b533fa2c3ca4ff63abcce490f12999a5b6 Mon Sep 17 00:00:00 2001 From: Wen Liang Date: Sun, 28 Mar 2021 17:36:45 -0400 Subject: [PATCH] Fix: support dns_search and dns_options for all address family `dns_search` and `dns_options` should not be specific to the address family. Previously, `dns_search` and `dns_options` were only supported for IPv4 nameservers, so we also need to support `dns_search` and `dns_options` for IPv6 nameservers. Signed-off-by: Wen Liang --- README.md | 16 +++-- library/network_connections.py | 25 ++++--- .../network_lsr/argument_validator.py | 53 ++++++++++++++ tests/playbooks/tests_eth_dns_support.yml | 16 ++++- tests/unit/test_network_connections.py | 72 +++++++++++++++++++ 5 files changed, 165 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 19ca5dc..d4939dd 100644 --- a/README.md +++ b/README.md @@ -364,21 +364,25 @@ The IP configuration supports the following options: - `dns_search` - `dns_search` is only supported for IPv4 nameservers. Manual DNS configuration can - be specified via a list of domains to search given in the `dns_search` option. + Manual DNS configuration can be specified via a list of domains to search given in + the `dns_search` option. - `dns_options` - `dns_options` is only supported for the NetworkManager provider and IPv4 - nameservers. Manual DNS configuration via a list of DNS options can be given in the - `dns_options`. The list of supported DNS options for IPv4 nameservers is described - in [man 5 resolv.conf](https://man7.org/linux/man-pages/man5/resolv.conf.5.html). + `dns_options` is only supported for the NetworkManager provider. Manual DNS + configuration via a list of DNS options can be given in the `dns_options`. The list + of supported DNS options for IPv4 nameservers is described in + [man 5 resolv.conf](https://man7.org/linux/man-pages/man5/resolv.conf.5.html). Currently, the list of supported DNS options is: - `attempts:n` - `debug` - `edns0` + - `inet6` + - `ip6-bytestring` + - `ip6-dotint` - `ndots:n` - `no-check-names` + - `no-ip6-dotint` - `no-reload` - `no-tld-query` - `rotate` diff --git a/library/network_connections.py b/library/network_connections.py index fc113b7..83d0054 100644 --- a/library/network_connections.py +++ b/library/network_connections.py @@ -1023,14 +1023,14 @@ class NMUtil: s_ip4.set_property( NM.SETTING_IP_CONFIG_ROUTE_METRIC, ip["route_metric4"] ) - for d in ip["dns"]: - if d["family"] == socket.AF_INET: - s_ip4.add_dns(d["address"]) - for s in ip["dns_search"]: - s_ip4.add_dns_search(s) + for nameserver in ip["dns"]: + if nameserver["family"] == socket.AF_INET: + s_ip4.add_dns(nameserver["address"]) + for search_domain in ip["dns_search"]: + s_ip4.add_dns_search(search_domain) s_ip4.clear_dns_options(True) - for s in ip["dns_options"]: - s_ip4.add_dns_option(s) + for option in ip["dns_options"]: + s_ip4.add_dns_option(option) if ip["ipv6_disabled"]: s_ip6.set_property(NM.SETTING_IP_CONFIG_METHOD, "disabled") @@ -1056,9 +1056,14 @@ class NMUtil: s_ip6.set_property( NM.SETTING_IP_CONFIG_ROUTE_METRIC, ip["route_metric6"] ) - for d in ip["dns"]: - if d["family"] == socket.AF_INET6: - s_ip6.add_dns(d["address"]) + for nameserver in ip["dns"]: + if nameserver["family"] == socket.AF_INET6: + s_ip6.add_dns(nameserver["address"]) + for search_domain in ip["dns_search"]: + s_ip6.add_dns_search(search_domain) + s_ip6.clear_dns_options(True) + for option in ip["dns_options"]: + s_ip6.add_dns_option(option) if ip["route_append_only"] and connection_current: for r in self.setting_ip_config_get_routes( diff --git a/module_utils/network_lsr/argument_validator.py b/module_utils/network_lsr/argument_validator.py index ff38027..f6c9253 100644 --- a/module_utils/network_lsr/argument_validator.py +++ b/module_utils/network_lsr/argument_validator.py @@ -534,8 +534,12 @@ class ArgValidator_DictIP(ArgValidatorDict): r"^attempts:([1-9]\d*|0)$", r"^debug$", r"^edns0$", + r"^inet6$", + r"^ip6-bytestring$", + r"^ip6-dotint$", r"^ndots:([1-9]\d*|0)$", r"^no-check-names$", + r"^no-ip6-dotint$", r"^no-reload$", r"^no-tld-query$", r"^rotate$", @@ -1759,6 +1763,13 @@ class ArgValidator_ListConnections(ArgValidatorList): VALIDATE_ONE_MODE_INITSCRIPTS = "initscripts" def validate_connection_one(self, mode, connections, idx): + def _ipv4_enabled(connection): + has_addrs4 = any( + address["family"] == socket.AF_INET + for address in connection["ip"]["address"] + ) + return connection["ip"]["dhcp4"] or has_addrs4 + connection = connections[idx] if "type" not in connection: return @@ -1832,3 +1843,45 @@ class ArgValidator_ListConnections(ArgValidatorList): idx, "ip.ipv6_disabled is not supported by initscripts.", ) + # Setting ip.dns is not allowed when corresponding IP method for that + # nameserver is disabled + for nameserver in connection["ip"]["dns"]: + if nameserver["family"] == socket.AF_INET and not _ipv4_enabled(connection): + raise ValidationError.from_connection( + idx, + "IPv4 needs to be enabled to support IPv4 nameservers.", + ) + if ( + nameserver["family"] == socket.AF_INET6 + and connection["ip"]["ipv6_disabled"] + ): + raise ValidationError.from_connection( + idx, + "IPv6 needs to be enabled to support IPv6 nameservers.", + ) + # when IPv4 and IPv6 are disabled, setting ip.dns_options or + # ip.dns_search is not allowed + if connection["ip"]["dns_search"] or connection["ip"]["dns_options"]: + if not _ipv4_enabled(connection) and connection["ip"]["ipv6_disabled"]: + raise ValidationError.from_connection( + idx, + "Setting 'dns_search' or 'dns_options' is not allowed when " + "both IPv4 and IPv6 are disabled.", + ) + # DNS options 'inet6', 'ip6-bytestring', 'ip6-dotint', 'no-ip6-dotint' are only + # supported for IPv6 configuration, so raise errors when IPv6 is disabled + if any( + option in connection["ip"]["dns_options"] + for option in [ + "inet6", + "ip6-bytestring", + "ip6-dotint", + "no-ip6-dotint", + ] + ): + if connection["ip"]["ipv6_disabled"]: + raise ValidationError.from_connection( + idx, + "Setting DNS options 'inet6', 'ip6-bytestring', 'ip6-dotint', " + "'no-ip6-dotint' is not allowed when IPv6 is disabled.", + ) diff --git a/tests/playbooks/tests_eth_dns_support.yml b/tests/playbooks/tests_eth_dns_support.yml index 0fff6ae..4b4beb0 100644 --- a/tests/playbooks/tests_eth_dns_support.yml +++ b/tests/playbooks/tests_eth_dns_support.yml @@ -36,6 +36,7 @@ dns: - 192.0.2.2 - 198.51.100.5 + - 2001:db8::20 dns_search: - example.com - example.org @@ -64,7 +65,7 @@ route_append_only: no rule_append_only: yes - - name: Verify nmcli connection DNS entry + - name: Verify nmcli connection DNS entry for IPv4 shell: | set -euxo pipefail nmcli connection show {{ interface }} | grep ipv4.dns @@ -72,11 +73,20 @@ ignore_errors: yes changed_when: false + - name: Verify nmcli connection DNS entry for IPv6 + shell: | + set -euxo pipefail + nmcli connection show {{ interface }} | grep ipv6.dns + register: ipv6_dns + ignore_errors: yes + changed_when: false + - name: "Assert that DNS addresses are configured correctly" assert: that: - "'192.0.2.2' in ipv4_dns.stdout" - "'198.51.100.5' in ipv4_dns.stdout" + - "'2001:db8::20' in ipv6_dns.stdout" msg: "DNS addresses are configured incorrectly" - name: "Assert that DNS search domains are configured correctly" @@ -84,6 +94,8 @@ that: - "'example.com' in ipv4_dns.stdout" - "'example.org' in ipv4_dns.stdout" + - "'example.com' in ipv6_dns.stdout" + - "'example.org' in ipv6_dns.stdout" msg: "DNS search domains are configured incorrectly" - name: "Assert that DNS options are configured correctly" @@ -91,6 +103,8 @@ that: - "'rotate' in ipv4_dns.stdout" - "'timeout:1' in ipv4_dns.stdout" + - "'rotate' in ipv6_dns.stdout" + - "'timeout:1' in ipv6_dns.stdout" msg: "DNS options are configured incorrectly" - import_playbook: down_profile.yml diff --git a/tests/unit/test_network_connections.py b/tests/unit/test_network_connections.py index 1b24193..f7ec527 100644 --- a/tests/unit/test_network_connections.py +++ b/tests/unit/test_network_connections.py @@ -3321,6 +3321,78 @@ class TestValidator(unittest.TestCase): validator.validate(true_testcase_12)["dns_options"], ["use-vc"] ) + def test_ipv4_dns_without_ipv4_config(self): + """ + Test that configuring IPv4 DNS is not allowed when IPv4 is disabled. + """ + validator = network_lsr.argument_validator.ArgValidator_ListConnections() + ipv4_dns_without_ipv4_config = [ + { + "name": "test_ipv4_dns", + "type": "ethernet", + "ip": { + "auto6": True, + "dhcp4": False, + "dns": ["198.51.100.5"], + }, + } + ] + self.assertRaises( + ValidationError, + validator.validate_connection_one, + "nm", + validator.validate(ipv4_dns_without_ipv4_config), + 0, + ) + + def test_ipv6_dns_without_ipv6_config(self): + """ + Test that configuring IPv6 DNS is not allowed when IPv6 is disabled. + """ + validator = network_lsr.argument_validator.ArgValidator_ListConnections() + ipv6_dns_without_ipv6_config = [ + { + "name": "test_ipv6_dns", + "type": "ethernet", + "ip": { + "ipv6_disabled": True, + "dhcp4": True, + "dns": ["2001:db8::20"], + }, + } + ] + self.assertRaises( + ValidationError, + validator.validate_connection_one, + "nm", + validator.validate(ipv6_dns_without_ipv6_config), + 0, + ) + + def test_ipv6_dns_options_without_ipv6_config(self): + """ + Test that configuring IPv6 DNS options is not allowed when IPv6 is disabled. + """ + validator = network_lsr.argument_validator.ArgValidator_ListConnections() + ipv6_dns_options_without_ipv6_config = [ + { + "name": "test_ipv6_dns", + "type": "ethernet", + "ip": { + "ipv6_disabled": True, + "dhcp4": True, + "dns_options": ["ip6-bytestring"], + }, + } + ] + self.assertRaises( + ValidationError, + validator.validate_connection_one, + "nm", + validator.validate(ipv6_dns_options_without_ipv6_config), + 0, + ) + def test_set_deprecated_master(self): """ When passing the deprecated "master" it is updated to "controller".