diff --git a/src/tests/multihost/ad/test_dyndns.py b/src/tests/multihost/ad/test_dyndns.py deleted file mode 100644 index 88741276c99..00000000000 --- a/src/tests/multihost/ad/test_dyndns.py +++ /dev/null @@ -1,569 +0,0 @@ -"""Automation tests for dynamic DNS - -:requirement: dyndns -:casecomponent: sssd -:subsystemteam: sst_idm_sssd -:upstream: yes -+:status: approved -""" -# flake8: noqa: W605 - -import pytest -import time -import random -from sssd.testlib.common.utils import sssdTools -from sssd.testlib.common.utils import ADDNS - - -@pytest.fixture(scope="class") -def change_client_hostname(session_multihost, request): - """ Change client hostname to client[XX].domain.com """ - cmd = session_multihost.client[0].run_command('hostname', raiseonerr=False) - old_hostname = cmd.stdout_text.rstrip() - ad_domain = session_multihost.ad[0].domainname - new_hostname = "client" + str(random.randrange(99)) + "." + ad_domain - session_multihost.client[0].run_command(f'hostnamectl set-hostname \"{new_hostname}\"') - - def restore_hostname(): - """ Restore hostname """ - session_multihost.client[0].run_command(f'hostnamectl set-hostname {old_hostname}') - - request.addfinalizer(restore_hostname) - - -@pytest.fixture(scope="class") -def disable_dns_forwarders(session_multihost, request): - """ Disables recursive lookups on DNS server """ - session_multihost.ad[0].run_command('dnscmd.exe /config /norecursion 1', raiseonerr=False) - - def enable_dns_forwarders(): - """ Enables recursion lookups on DNS servers """ - session_multihost.ad[0].run_command('dnscmd.exe /config /norecursion 0', raiseonerr=False) - - request.addfinalizer(enable_dns_forwarders) - - -@pytest.fixture(scope="class") -def reverse_zone(session_multihost, request): - """ Creates reverse zones """ - dns = ADDNS(session_multihost.ad[0]) - ip = session_multihost.ad[0].ip.split(".") - ip1 = session_multihost.client[0].ip.split(".") - network = str(f'{ip[2]}.{ip[1]}.{ip[0]}.in-addr.arpa') - dns.add_zone(network) - if ip[2] != ip1[2]: - network1 = str(f'{ip1[2]}.{ip1[1]}.{ip1[0]}.in-addr.arpa') - assert dns.add_zone(network1) - - def remove_reverse_zone(): - """ Delete reverse zones """ - dns.del_zone(network) - if ip[2] != ip1[2]: - dns.del_zone(network1) - - request.addfinalizer(remove_reverse_zone) - - -@pytest.fixture(scope="function") -def extra_network(session_multihost, request): - """ Create reverse zone """ - dns = ADDNS(session_multihost.ad[0]) - network = "1.168.192.in-addr.arpa" - assert dns.add_zone(network) - - def remove_extra_network(): - """ Delete reverse zone""" - dns.del_zone(network) - - request.addfinalizer(remove_extra_network) - - -@pytest.fixture(scope="function") -def extra_interface(session_multihost, request): - """ Create extra interface """ - extra_interface = 'ibm' + str(random.randrange(99)) - extra_ip = '192.168.1.' + str(random.randrange(2, 255)) - session_multihost.client[0].run_command('ip link add ' + extra_interface + ' type dummy') - session_multihost.client[0].run_command('ip addr add ' + extra_ip + ' dev ' + extra_interface) - - def remove_interface(): - """ Delete interface """ - session_multihost.client[0].run_command('ip addr flush dev ' + extra_interface) - session_multihost.client[0].run_command('ip link del ' + extra_interface) - - request.addfinalizer(remove_interface) - return extra_interface, extra_ip - - -@pytest.mark.usefixtures("reverse_zone", "disable_dns_forwarders", "change_client_hostname") -@pytest.mark.dyndns -@pytest.mark.tier2 -class TestDynDns(object): - - @staticmethod - @pytest.mark.c_ares - def test_0001_verify_with_default_setting(multihost, adjoin): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: verify with default settings - :id: 183cc040-0b8c-47af-8b19-8bcb72c3f001 - :steps: - 1. Join client to AD domain - 2. Perform DNS lookup for hostname - 3. Perform DNS lookup for IP - :expectedresults: - 1. Client joins domain - 2. Hostname resolves correctly - 3. IP resolves correctly - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - ip = multihost.client[0].ip - - client.clear_sssd_cache() - # Update function with IPV6 support is added to output what failed - assert dns.find_a(hostname, ip) - assert dns.find_ptr(hostname, ip) - - @staticmethod - @pytest.mark.c_ares - def test_0002_verify_when_dyndns_update_set_to_false(multihost, adjoin): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: verify when dyndns update set to false - :id: 0cdbd0fe-4c67-4192-b44b-04e4227b5858 - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Perform DNS lookup for hostname - 3. Perform DNS lookup for IP - :expectedresults: - 1. Client joins domain - 2. Hostname resolves incorrectly - 3. IP resolves incorrectly - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - domain = multihost.ad[0].domainname - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - ip = str(multihost.client[0].ip) - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_update': 'false'} - client.sssd_conf(domain_section, sssd_params) - dns.del_record(hostname) - dns.del_record(ip) - client.clear_sssd_cache() - assert dns.find_a(hostname, ip) is not True - assert dns.find_ptr(hostname, ip) is not True - - @staticmethod - @pytest.mark.c_ares - def test_0003_verify_with_dyndns_ttl_functionality(multihost, adjoin): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: verify with dyndns ttl functionality - :id: ffb8ee9b-2a82-4917-82b0-d87fd066a24c - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Configure sssd.conf with dyndns_ttl = 9200 - 3. Perform DNS lookup for hostname - 4. Perform DNS lookup for IP - :expectedresults: - 1. Client joins domain - 2. Should succeed - 3. Hostname resolves with 9200 ttl - 4. IP resolves with 9200 ttl - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - domain = multihost.ad[0].domainname - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - ip = multihost.client[0].ip - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_update': 'true', 'dyndns_ttl': '9200'} - client.sssd_conf(domain_section, sssd_params) - dns.del_record(hostname) - dns.del_record(ip) - client.clear_sssd_cache() - - assert dns.find_a(hostname, ip) - assert dns.find_ptr(hostname, ip) - assert '9200' in dns.print_zone(domain) - - @staticmethod - def test_0004_check_dyndns_iface_with_existing_interfaces( - multihost, adjoin, extra_network, extra_interface): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: check dyndns iface with existing interface - :id: 166ee0a0-6f19-4be0-b5db-7373dde82862 - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Create extra interface - 3. Perform DNS lookup for hostname on extra interface - 4. Perform DNS lookup for IP on extra interface - :expectedresults: - 1. Client joins domain - 2. Should succeed - 3. Hostname resolves correctly on extra interface - 4. IP resolves correctly on extra interface - """ - adjoin(membersw='adcli') - (extra_interface, extra_ip) = extra_interface - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - domain = multihost.ad[0].domainname - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - ip = multihost.client[0].ip - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_iface': extra_interface} - client.sssd_conf(domain_section, sssd_params) - dns.del_record(hostname) - dns.del_record(ip) - dns.del_record(extra_ip) - client.clear_sssd_cache() - - assert dns.find_a(hostname, extra_ip) - assert dns.find_ptr(hostname, extra_ip) - assert not dns.find_ptr(hostname, ip) - - @staticmethod - @pytest.mark.c_ares - def test_0005_check_dyndns_iface_with_non_existing_interfaces(multihost, adjoin): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: check dyndns iface with non-existing interfaces - :id: 412f684a-5a68-472d-86b1-93019bb53e6f - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Perform DNS lookup for hostname on bogus interface - 3. Perform DNS lookup for IP on bogus interface - :expectedresults: - 1. Client joins domain - 2. Hostname does not resolve correctly on bogus interface - 3. IP does not resolve correctly on bogus interface - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - domain = multihost.ad[0].domainname - ip = multihost.client[0].ip - dns.del_record(hostname) - dns.del_record(ip) - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_iface': 'non_existent'} - client.sssd_conf(domain_section, sssd_params) - client.clear_sssd_cache() - - assert dns.find_a(hostname, ip) is not True - assert dns.find_ptr(hostname, ip) is not True - - @staticmethod - @pytest.mark.c_ares - def test_0006_check_with_dyndns_refresh_interval(multihost, adjoin, extra_network, extra_interface): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: check with dyndns refresh interval - :id: 54765188-b21f-4ba1-890b-953eec9b696d - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Create extra interface and assign IP - 3. Set refresh interval and dyndns iface in sssd.conf - 4. Perform DNS lookup for hostname on extra interface - 5. Perform DNS lookup for IP on extra interface - 6. Change IP on extra interface - 7. Let SSSD update DNS - 8. Perform DNS lookup for hostname on extra interface - 9. Perform DNS lookup for IP on extra interface - :expectedresults: - 1. Client joins domain - 2. Should succeed - 3. Should succeed - 4. Hostname resolves correctly on extra interface - 5. IP resolves correctly on extra interface - 6. New IP address is assigned for extra interface - 7. SSSD updates DNS - 8. Hostname resolves correctly on extra interface with new IP - 9. IP resolves correctly on extra interface with new hostname - """ - (extra_interface, extra_ip) = extra_interface - extra_ip_after_refresh = '192.168.1.' + str(random.randrange(2, 255)) - while extra_ip == extra_ip_after_refresh: - extra_ip_after_refresh = '192.168.1.' + str(random.randrange(2, 255)) - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - domain = multihost.ad[0].domainname - ip = multihost.client[0].ip - dns.del_record(hostname) - dns.del_record(ip) - - adjoin(membersw='adcli') - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_refresh_interval': '81', 'dyndns_iface': extra_interface} - client.sssd_conf(domain_section, sssd_params) - client.clear_sssd_cache() - assert dns.find_a(hostname, extra_ip) - assert dns.find_ptr(hostname, extra_ip) - - multihost.client[0].run_command('ip addr flush dev ' + extra_interface) - multihost.client[0].run_command('ip addr change ' + extra_ip_after_refresh + ' dev ' + extra_interface) - time.sleep(83) - - assert dns.find_a(hostname, extra_ip_after_refresh) - assert dns.find_ptr(hostname, extra_ip_after_refresh) - - @staticmethod - @pytest.mark.c_ares - def test_0007_set_dyndns_update_ptr_false_ptr_records_are_absent(multihost, adjoin): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: set dyndns update ptr false ptr records are absent - Will test parameter dyndns_update_ptr = false when no records exists - NOTE: dyndns_refresh_interval lowest value is 60 seconds, with a 21 second timeout so the value is 81 - :id: e7bac85a-0504-4487-b26e-129a99121810 - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Delete both A and PTR records - 3. Set dyndns_update_ptr = false in sssd.conf - 4. Start SSSD - 5. Use DNS to check the host and IP - :expectedresults: - 1. Client joins domain - 2. Host records are removed from the DNS server - 3. SSSD is configured - 4. SSSD is started - 5. Host record can be resolved and the PTR record cannot - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - hostname_cmd = multihost.client[0].run_command('hostname') - hostname = hostname_cmd.stdout_text.rstrip() - domain = multihost.ad[0].domainname - ip = multihost.client[0].ip - - dns.del_record(hostname) - dns.del_record(ip) - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_refresh_interval': '81', 'dyndns_update_ptr': 'false'} - client.sssd_conf(domain_section, sssd_params) - client.clear_sssd_cache() - - assert dns.find_a(hostname, ip) - assert dns.find_ptr(hostname, ip) is not True - - @staticmethod - @pytest.mark.c_ares - def test_0008_set_dyndns_update_ptr_to_false_ptr_records_are_present( - multihost, adjoin, extra_interface, extra_network): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: set dyndns update ptr to false ptr records are present - Will test parameter dyndns_update_ptr = false when records exits - NOTE: dyndns_refresh_interval lowest value is 60 seconds, with a 21 second timeout so the value is 81 - :id: 7c96e4d2-b1da-4391-9a7c-ff632667b1bc - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Delete both A and PTR records - 3. Start SSSD - 4. Use DNS to check the host and IP - 5. Set dyndns_update_ptr = false and dyndns_refresh_interval = 81 in sssd.conf - 6. Update IP on extra interface and start SSSD - 7. Use DNS to check the host and IP - :expectedresults: - 1. Client joins domain - 2. Host records are removed from the DNS server - 3. SSSD is started - 4. Host and IP is resolvable - 5. SSSD is stopped and sssd.conf has been updated - 6. IP on dummy has been changed - 7. Host and IP is resolvable but the PTR record is the old IP - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - domain = multihost.ad[0].domainname - (extra_int, ip) = extra_interface - ptr = str(random.randrange(2, 255)) - while ptr == str(ip.split(".")[3]): - ptr = str(random.randrange(2, 255)) - new_ip = str(ip.split(".")[0]) + "." + str(ip.split(".")[1]) + "." + str(ip.split(".")[2]) + "." + ptr - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_refresh_interval': '81', 'dyndns_update': 'true', 'dyndns_iface': extra_int} - client.sssd_conf(domain_section, sssd_params) - dns.del_record(hostname) - dns.del_record(ip) - client.clear_sssd_cache() - assert dns.find_a(hostname, ip) - assert dns.find_ptr(hostname, ip) - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_update_ptr': 'false'} - client.sssd_conf(domain_section, sssd_params) - multihost.client[0].run_command('ip addr flush dev ' + extra_int) - multihost.client[0].run_command('ip link del ' + extra_int) - multihost.client[0].run_command('ip link add ' + extra_int + ' type dummy') - multihost.client[0].run_command('ip addr add ' + new_ip + ' dev ' + extra_int) - client.clear_sssd_cache() - - assert dns.find_a(hostname, new_ip) - assert dns.find_ptr(hostname, ip) - assert dns.find_ptr(hostname, new_ip) is not True - - @staticmethod - @pytest.mark.c_ares - def test_0009_check_with_dyndns_force_tcp(multihost, adjoin): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: check with dyndns force tcp - Test SSSD to use TCP for dynamic DNS. Blocking all TCP traffic will test that no other protocols will - be used to update DNS - :id: 0de1b45c-cecb-451f-b625-823761f255b2 - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Set ldap_purge_cache_timeout = 0, krb5_auth_timeout = 12, dyndns_force_tcp = true - 3. Update firewall to block TCP traffic on port 53 - 4. Start SSSD - 5. Check DNS records - 6. Unblock traffic - 7. Check DNS records again - :expectedresults: - 1. Client joins domain - 2. Configure SSSD - 3. Traffic is being blocked and dyndns is only using tcp - 4. SSSD starts - 5. No A or PTR records exists - 6. Traffic is flowing - 7. DNS records are now updating properly - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - hostname_cmd = multihost.client[0].run_command('hostname') - hostname = hostname_cmd.stdout_text.rstrip() - domain = multihost.ad[0].domainname - ip = multihost.client[0].ip - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'ldap_purge_cache_timeout': '0', 'krb5_auth_timeout': '12', 'dyndns_force_tcp': 'true'} - client.sssd_conf(domain_section, sssd_params) - dns.del_record(hostname) - dns.del_record(ip) - multihost.client[0].run_command('which iptables || yum install -y iptables', raiseonerr=False) - multihost.client[0].run_command(f'iptables -A INPUT -p tcp --dport 53 -s {multihost.ad[0].ip} -j DROP; ' - f'iptables -A OUTPUT -p tcp --dport 53 -d {multihost.ad[0].ip} -j DROP') - client.clear_sssd_cache() - assert dns.find_a(hostname, ip) is not True - assert dns.find_ptr(hostname, ip) is not True - - multihost.client[0].run_command('iptables -F', raiseonerr=False) - client.clear_sssd_cache() - assert dns.find_a(hostname, ip) - assert dns.find_ptr(hostname, ip) - - @staticmethod - @pytest.mark.c_ares - def test_0010_check_with_combination_of_addresses( - multihost, adjoin, extra_interface, extra_network): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: check with combination of addresses - Check DynDNS when the client has several interfaces, testing that the right interface updates DNS. - NOTE: The original test includes IPV6, however CI does not support IPV6, so this is done with just IPV4 - :id: fc7e3b4d-5cd3-4206-b16d-d90c96acca14 - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Add extra interface and network - 3. Configure SSSD with dyndns_iface = extra_interface - 4. Start SSSD - 5. Check DNS records - :expectedresults: - 1. Client joins domain - 2. Extra interface exists on client machine - 3. SSSD is configured - 4. SSSD starts - 5. DNS server will update with only the A and PTR records from the extra interface - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - domain = multihost.ad[0].domainname - (extra_int, extra_ip) = extra_interface - ip = multihost.client[0].ip - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_iface': f'{extra_int}'} - client.sssd_conf(domain_section, sssd_params) - dns.del_record(hostname) - dns.del_record(ip) - client.clear_sssd_cache() - - assert dns.find_a(hostname, extra_ip) - assert dns.find_ptr(hostname, extra_ip) - assert dns.find_ptr(hostname, ip) is not True - - @staticmethod - def test_0011_verify_use_after_free_in_dyndns_code_bz1132361(multihost, adjoin): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: verify use after free in dyndns code bz1132361 - bz1132361 is an OOM error that is recreated by - setting TALLOC_FREE_FILL - :id: 72d5e4ad-fdb9-4278-860f-57ff563a20d7 - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Add extra interface - 3. Set TALLOC_FREE_FILL=253 in /etc/sysconfig/sssd - 4. Start SSSD - :expectedresults: - 1. Client joins domain - 2. Extra interface is created - 3. Configuration files updated - 4. SSSD starts and does not segfault - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - multihost.client[0].run_command('echo "TALLOC_FREE_FILL=253" >> /etc/sysconfig/sssd') - client.clear_sssd_cache() - - ps_cmd = multihost.client[0].run_command('ps aux | grep sssd') - assert 'sssd_be' in ps_cmd.stdout_text - assert 'sssd_nss' in ps_cmd.stdout_text - assert 'sssd_pam' in ps_cmd.stdout_text - assert 'sssd_pac' in ps_cmd.stdout_text - multihost.client[0].run_command('sed -i "/TALLOC_FREE_FILL/d" /etc/sysconfig/sssd') - - @staticmethod - def test_0012_set_dyndns_update_ptr_when_dyndns_server_equals_ad_server(multihost, adjoin): - """ - :title: IDM-SSSD-TC: ad_provider: dyndns: set dyndns update ptr when dyndns server equals ad server - Testing parameter ad_server = $AD_SERVER - :id: afa91b9b-4943-423a-a2e2-54f6c6204107 - :steps: - 1. Join client to AD domain and delete all client DNS records - 2. Configure SSSD to contain dyndns_server = AD_SERVER - 3. Start SSSD - 4. Check DNS records - :expectedresults: - 1. Client joins domain - 2. SSSD is configured - 3. SSSD is started - 4. Both A and PTR records are found - """ - adjoin(membersw='adcli') - client = sssdTools(multihost.client[0], multihost.ad[0]) - dns = ADDNS(multihost.ad[0]) - hostname = multihost.client[0].run_command('hostname').stdout_text.rstrip() - domain = multihost.ad[0].domainname - adserver = multihost.ad[0].hostname - ip = multihost.client[0].ip - - domain_section = 'domain/{}'.format(domain) - sssd_params = {'dyndns_server': f'{adserver}'} - client.sssd_conf(domain_section, sssd_params) - dns.del_record(hostname) - dns.del_record(ip) - client.clear_sssd_cache() - assert dns.find_a(hostname, ip) - assert dns.find_ptr(hostname, ip) diff --git a/src/tests/system/tests/test_dyndns.py b/src/tests/system/tests/test_dyndns.py new file mode 100644 index 00000000000..2286ee6b39b --- /dev/null +++ b/src/tests/system/tests/test_dyndns.py @@ -0,0 +1,386 @@ +""" +SSSD Dynamic DNS Test Cases + +SSSD supports dynamic DNS updates, allowing the client to automatically register itself so it can be +resolved by name. This feature is enabled by default with the 'ad_provider', which creates the DNS record +when SSSD starts. In contrast, the 'ipa_provider' has dynamic DNS updates disabled, since the record is +already created during the initial 'ipa-client-install'. + +For these tests to run, we have the following conditional forwarders configured in the dns container. + - 10.255.250.0/24 for AD + - 10.255.251.0/24 for IPA + - 10.255.252.0/24 for Samba + +The default interface 'eth0' is not used for testing. Instead, 'dummy0' interface is created on the client with +an IP in the correct subnet for the provider, Last octet 40 is used for the client, *.*.*.40. This allows us to test +dynamic DNS updates without affecting the default network configuration of the client and each provider can be +authoritative for their network. + +Note the assertions, dig results can return multiple records, so we check that the expected record is in the results. + +:requirement: dyndns +""" + +from __future__ import annotations + +import time + +import pytest +from sssd_test_framework.misc import ip_to_ptr +from sssd_test_framework.roles.ad import AD +from sssd_test_framework.roles.client import Client +from sssd_test_framework.roles.generic import GenericProvider +from sssd_test_framework.roles.ipa import IPA +from sssd_test_framework.roles.samba import Samba +from sssd_test_framework.topology import KnownTopologyGroup + + +def create_ptr_zones(client: Client, provider: GenericProvider, device: str = "dummy0") -> None: + """ + Create PTR zones. + :param client: Client object. + :type client: Client. + :param provider: Provider object. + :type provider: GenericProvider + :param device: Network device name. + :type device: str + """ + provider.dns().clear_forwarders() + address = client.net.ip(device).addresses + for i in address: + if i is not None and ip_to_ptr(i) not in provider.dns().list_zones(): + provider.dns().zone(ip_to_ptr(i)).create() + + +def create_dummy_interface( + client: Client, provider: GenericProvider, device: str | None = "dummy0", ipv6: str = "2001:db8::1" +) -> tuple[str, str | None, str | None]: + """ + Create a dummy network interface on the client. + :param client: Client object. + :type client: Client. + :param provider: GenericProvider object. + :type provider: GenericProvider + :param device: Name of the dummy network interface. + :type device: str | None + :param ipv6: IPv6 address, optional + :type ipv6: str, default 2001:db8::1 + :return: IP, IP6, and device name. + :rtype: tuple[str, str | None, str | None] + """ + if isinstance(provider, AD): + ip = "10.255.250.40" + elif isinstance(provider, IPA): + ip = "10.255.251.40" + elif isinstance(provider, Samba): + ip = "10.255.252.40" + else: + raise ValueError("Invalid provider!") + + client.net.ip(device).add_device(ip) + + return ip, None, device + + +def check_records(client: Client, provider: GenericProvider, hostname: str, device: str = "dummy0") -> None: + """ + Clean host dns records and restart services. + + :description: This helper method is mostly for IPA since there guaranteed to have a forward record. + :param client: Client object. + :type client: Client. + :param provider: GenericProvider object. + :type provider: GenericProvider + :param hostname: Hostname to clean A and PTR records for. + :type hostname: str + :param device: Name of the dummy network interface. + :type device: str, default "dummy0" + """ + dns_zone = provider.dns().zone(provider.domain) + a_records = client.net.dig(hostname, provider.server, attempts=2, delay=2) + ptr_records = [] + + for i in client.net.ip(device).addresses: + if i is not None: + records = client.net.dig(i, provider.server, attempts=2, delay=2) + if records is not None: + ptr_records.extend(records) + + if a_records is not None or ptr_records: + if a_records is not None: + for record in a_records: + dns_zone.delete_record(record["name"]) + + if ptr_records: + for record in ptr_records: + dns_zone.delete_record(record["data"]) + + if isinstance(provider, IPA): + provider.host.conn.run("ipactl restart") + elif isinstance(provider, AD): + provider.host.conn.run("Restart-Service -Name NTDS -Force") + elif isinstance(provider, Samba): + provider.host.conn.run("systemctl restart samba") + else: + raise ValueError("Invalid provider!") + + +@pytest.mark.topology(KnownTopologyGroup.AnyDC) +@pytest.mark.importance("critical") +def test_dyndns__update_is_set_to_true(client: Client, provider: GenericProvider): + """ + :title: Dynamic DNS update is set to true + :setup: + 1. Create a dummy interface + 2. Create reverse zones + 3. Check for existing records, clean if found + 4. Configure and start SSSD + :steps: + 1. Lookup hostname + 2. Lookup ipv4 address + :expectedresults: + 1. Hostname is found + 2. IP address is found + :customerscenario: True + """ + hostname = client.hostnameutils.fqdn + ip, _, _ = create_dummy_interface(client, provider) + + create_ptr_zones(client, provider) + check_records(client, provider, hostname) + client.sssd.common.dyndns() + client.sssd.start() + + a_records = client.net.dig(hostname, provider.server) + assert a_records, f"Host {hostname} was not found!" + assert any(r["data"] == ip for r in a_records), f"Host {hostname} found with invalid ip!" + + ptr_records = client.net.dig(ip, provider.server) + assert ptr_records, f"PTR record for {ip} was not found!" + assert any(r["data"] == hostname for r in ptr_records), f"IP {ip} found pointing to the wrong hostname!" + + +@pytest.mark.topology(KnownTopologyGroup.AnyDC) +@pytest.mark.importance("critical") +def test_dyndns__update_is_set_to_false(client: Client, provider: GenericProvider): + """ + :title: Dynamic DNS is disabled + :setup: + 1. Create a dummy interface + 2. Create reverse zones + 3. Check for existing records + 4. Configure and start SSSD with dyndns_update = False and dyndns_update_ptr = False + :steps: + 1. Lookup hostname + 2. Lookup ipv4 address + :expectedresults: + 1. Hostname is not found + 2. IP address is not found + :customerscenario: True + """ + hostname = client.hostnameutils.fqdn + ip, _, _ = create_dummy_interface(client, provider) + + create_ptr_zones(client, provider) + check_records(client, provider, hostname) + client.sssd.common.dyndns() + + client.sssd.domain["dyndns_update"] = "False" + client.sssd.domain["dyndns_update_ptr"] = "False" + client.sssd.start() + + assert client.net.dig(hostname, provider.server, attempts=2, delay=2) is None, f"Host {hostname} was found!" + assert client.net.dig(ip, provider.server, attempts=2, delay=2) is None, f"PTR record for {ip} was found!" + + +@pytest.mark.topology(KnownTopologyGroup.AnyDC) +@pytest.mark.importance("critical") +@pytest.mark.parametrize( + "interfaces", + [("eth0", False), ("eth0, dummy0", True)], + ids=["dyndns_iface = eth0", "dyndns_iface = eth0, dummy0"], +) +def test_dyndns__update_with_address_filtering( + client: Client, provider: GenericProvider, interfaces: tuple[str, bool] +): + """ + :title: Dynamic DNS updates filtered address + + If dyndns_iface is not set, it defaults to the interface that SSSD uses to communicate with the provider, + in this case 'eth0'. Only when the filtered IP, when the device is specified 'dummy0', dns will update. + + :setup: + 1. Create dummy interfaces + 2. Check for existing records + 3. Configure SSSD and specify one ip to filter and start + :steps: + 1. Look up record + :expectedresults: + 1. Hostname is found only when 'dummy0' is configured + :customerscenario: True + """ + hostname = client.hostnameutils.fqdn + ip, _, _ = create_dummy_interface(client, provider) + + check_records(client, provider, hostname) + + client.sssd.domain["dyndns_update"] = "True" + client.sssd.domain["dyndns_update_ptr"] = "True" + client.sssd.domain["dyndns_refresh_interval"] = "1" + client.sssd.domain["dyndns_refresh_interval_offset"] = "5" + client.sssd.domain["dyndns_address"] = f"{'.'.join(ip.split('.')[:-1] + ['0'])}/24" + client.sssd.domain["dyndns_iface"] = interfaces[0] + client.sssd.start() + + if interfaces[-1]: + a_records = client.net.dig(hostname, provider.server) + assert a_records, f"Host {hostname} was not found!" + assert any(r["data"] == ip for r in a_records), f"Host {hostname} found with invalid ip!" + else: + assert client.net.dig(hostname, provider.server, attempts=2, delay=2) is None, f"Host {hostname} was found!" + + +@pytest.mark.topology(KnownTopologyGroup.AnyDC) +@pytest.mark.importance("critical") +def test_dyndns__update_configured_ttl_value(client: Client, provider: GenericProvider): + """ + :title: Dynamic DNS update creates record with configured TTL value + :setup: + 1. Create a dummy interface + 2. Create reverse zones + 3. Check for existing records, clean if found + 4. Configure 'dyndns_ttl' and start SSSD + :steps: + 1. Lookup hostname + :expectedresults: + 1. Hostname is found with specified TTL value + :customerscenario: True + """ + hostname = client.hostnameutils.fqdn + ip, _, _ = create_dummy_interface(client, provider) + + check_records(client, provider, hostname) + client.sssd.common.dyndns() + client.sssd.domain["dyndns_ttl"] = "9999" + client.sssd.start() + + a_records = client.net.dig(hostname, provider.server) + assert a_records, f"Host {hostname} was not found!" + assert any(r["data"] == ip for r in a_records), f"Host {hostname} found with invalid ip!" + assert "9999" == str(a_records[0]["ttl"]), "TTL was not set to 9999!" + + +@pytest.mark.topology(KnownTopologyGroup.AnyDC) +@pytest.mark.importance("critical") +def test_dyndns__update_over_tcp_only(client: Client, provider: GenericProvider): + """ + :title: Dynamic DNS update is configured to use TCP only + :setup: + 1. Create a dummy interface + 2. Check for existing records, clean if found + 3. Configure 'dyndns_force_tcp = True' and start SSSD + :steps: + 1. Wait 15 seconds and check the logs + :expectedresults: + 1. Logs contain message indicating that TCP is set to on + :customerscenario: True + """ + hostname = client.hostnameutils.fqdn + ip, _, _ = create_dummy_interface(client, provider) + + check_records(client, provider, hostname) + + client.sssd.common.dyndns() + client.sssd.domain["dyndns_force_tcp"] = "True" + client.sssd.start() + + # This was changed from a functional system test to a sanity test that asserts the logs. + # NSUpdate failed connect in CI over the TCP but does work locally. The sleep is required + # allow nsupdate to run. + + time.sleep(15) + logs = client.fs.read(client.sssd.logs.domain()) + assert "TCP is set to on" in logs, "No message indicating that tcp nsupdate is on!" + + +@pytest.mark.topology(KnownTopologyGroup.AnyDC) +@pytest.mark.importance("critical") +@pytest.mark.parametrize( + "auth_mech", + [("None", 1), ("GSS-TSIG", 0)], + ids=["dyndns_auth = None", "dyndns_auth = GSS-TSIG"], +) +def test_dyndns__update_authentication_mechanism( + client: Client, provider: GenericProvider, auth_mech: tuple[str, int] +): + """ + :title: Dynamic DNS updates authentication mechanism + :setup: + 1. Create dummy interfaces + 2. Check for existing records + 3. Configure and start SSSD + :steps: + 1. Look up record + :expectedresults: + 1. Hostname is found only when GSS-TSIG is configured + :customerscenario: True + """ + hostname = client.hostnameutils.fqdn + ip, _, _ = create_dummy_interface(client, provider) + + check_records(client, provider, hostname) + + client.sssd.common.dyndns() + client.sssd.domain["dyndns_auth"] = auth_mech[0] + client.sssd.start() + + if auth_mech[-1] == 0: + a_records = client.net.dig(hostname, provider.server) + assert a_records, f"Host {hostname} was not found!" + assert any(r["data"] == ip for r in a_records), f"Host {hostname} found with invalid ip!" + elif auth_mech[-1] == 1: + assert client.net.dig(hostname, provider.server) is None, f"Host {hostname} was found!" + + +@pytest.mark.topology(KnownTopologyGroup.AnyDC) +@pytest.mark.importance("critical") +@pytest.mark.ticket(bz=1132361) +def test_dyndns__update_does_not_contain_use_after_free_bugs(client: Client, provider: GenericProvider): + """ + :title: Prevent use-after-free during DNS updates + :description: + Ensures the dyndns update code does not contain use-after-free bugs that manifest + under low memory conditions (TALLOC_FREE_FILL). + :setup: + 1. Create a dummy interface + 2. Create reverse zones + 3. Check for existing records, clean if found + 4. Configure 'talloc_free_fill=253' to force error + 5. Configure and start SSSD + :steps: + 1. Lookup hostname + 2. Lookup ipv4 address + :expectedresults: + 1. Hostname is found + 2. IP address is found + :customerscenario: True + """ + hostname = client.hostnameutils.fqdn + ip, _, _ = create_dummy_interface(client, provider) + + create_ptr_zones(client, provider) + check_records(client, provider, hostname) + + client.fs.backup("/etc/sysconfig/sssd") + client.fs.append("/etc/sysconfig/sssd", "TALLOC_FREE_FILL=253") + + client.sssd.common.dyndns() + client.sssd.start() + + a_records = client.net.dig(hostname, provider.server) + assert a_records, f"Host {hostname} was not found!" + assert any(r["data"] == ip for r in a_records), f"Host {hostname} found with invalid ip!" + + ptr_records = client.net.dig(ip, provider.server) + assert ptr_records, f"PTR record for {ip} was not found!" + assert any(r["data"] == hostname for r in ptr_records), f"IP {ip} found pointing to the wrong hostname!"