| # |
| # Copyright 2021 - The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import time |
| |
| from acts import asserts |
| from acts.controllers.openwrt_ap import MOBLY_CONTROLLER_CONFIG_NAME as OPENWRT |
| from acts.test_decorators import test_tracker_info |
| from acts_contrib.test_utils.net import connectivity_const as cconst |
| from acts_contrib.test_utils.net import connectivity_test_utils as cutils |
| from acts_contrib.test_utils.net.net_test_utils import start_tcpdump |
| from acts_contrib.test_utils.net.net_test_utils import stop_tcpdump |
| from acts_contrib.test_utils.wifi import wifi_test_utils as wutils |
| from acts_contrib.test_utils.wifi.WifiBaseTest import WifiBaseTest |
| from scapy.all import rdpcap |
| from scapy.all import Scapy_Exception |
| from scapy.all import TCP |
| from scapy.all import UDP |
| |
| |
| DEFAULT_DNS_TIMEOUT = 5 |
| |
| |
| class DnsOverHttpsTest(WifiBaseTest): |
| """Tests for DnsoverHttps feature.""" |
| |
| def setup_class(self): |
| """Setup devices and OpenWrt for DnsoverHttps test and unpack params.""" |
| |
| self.dut = self.android_devices[0] |
| if len(self.android_devices) > 1: |
| self.dut_b = self.android_devices[1] |
| for ad in self.android_devices: |
| wutils.reset_wifi(ad) |
| ad.droid.setPrivateDnsMode(True) |
| req_params = ("ping_hosts",) |
| opt_params = ("wifi_network", "configure_OpenWrt", |
| "ipv4_only_network", "ipv4_ipv6_network") |
| self.unpack_userparams(req_param_names=req_params, |
| opt_param_names=opt_params) |
| if OPENWRT in self.user_params: |
| self.openwrt = self.access_points[0] |
| if hasattr(self, "configure_OpenWrt") and self.configure_OpenWrt == "skip": |
| self.dut.log.info("Skip configure Wifi interface due to config setup.") |
| else: |
| self.configure_openwrt_ap_and_start(wpa_network=True) |
| self.tcpdump_pid = None |
| self.default_dns = None |
| self.default_dns_v6 = None |
| self._setup_doh(self.dut, self.get_wifi_network()) |
| |
| def teardown_test(self): |
| wutils.reset_wifi(self.dut) |
| if OPENWRT in self.user_params: |
| self.openwrt.network_setting.del_default_dns(self.default_dns) |
| self.default_dns = None |
| self.default_dns_v6 = None |
| |
| def teardown_class(self): |
| for ad in self.android_devices: |
| ad.droid.setPrivateDnsMode(True) |
| self._setup_doh(ad, self.get_wifi_network(), enable=False) |
| |
| def on_fail(self, test_name, begin_time): |
| self.dut.take_bug_report(test_name, begin_time) |
| |
| def get_wifi_network(self, ipv6_supported=False): |
| """Return fit network for conditions. |
| |
| Args: |
| ipv6_supported: Boolean for select network. |
| Returns: |
| A dict for network object for connect wifi. |
| """ |
| if OPENWRT in self.user_params: |
| if ipv6_supported: |
| self.openwrt.network_setting.enable_ipv6() |
| self.openwrt.network_setting.setup_ipv6_bridge() |
| else: |
| self.openwrt.network_setting.disable_ipv6() |
| self.openwrt.network_setting.remove_ipv6_bridge() |
| if self.default_dns: |
| self.openwrt.network_setting.add_default_dns(self.default_dns) |
| if self.default_dns_v6: |
| for ipv6_dns in self.default_dns_v6: |
| self.openwrt.network_setting.add_default_v6_dns(ipv6_dns) |
| if hasattr(self, "configure_OpenWrt") and self.configure_OpenWrt == "skip": |
| return self.wifi_network |
| return self.openwrt.get_wifi_network() |
| if ipv6_supported: |
| return self.ipv4_ipv6_network |
| return self.ipv4_only_network |
| |
| def _verify_doh_queries(self, pcap_file, over_https): |
| """Verify if DNS queries were over https or not. |
| |
| Args: |
| pcap_file: tcpdump file |
| over_https: True if excepted all dns go through doh. |
| """ |
| try: |
| packets = rdpcap(pcap_file) |
| except Scapy_Exception: |
| asserts.fail("Not a valid pcap file") |
| |
| for pkt in packets: |
| summary = "%s" % pkt.summary() |
| for host in self.ping_hosts: |
| host = host.split(".")[-2] |
| if UDP in pkt and pkt[UDP].sport == 53 and host in summary: |
| if over_https: |
| asserts.fail("Found query to port 53: %s" % summary) |
| else: |
| self.dut.log.info("Found query to port 53: %s" % summary) |
| if TCP in pkt and pkt[TCP].sport == 853: |
| asserts.fail("Found query to port 853: %s" % summary) |
| |
| def _test_public_doh_mode(self, ad, net, dns_mode, hostname=None): |
| """Test step for DoH. |
| |
| Args: |
| ad: android device object. |
| net: wifi network to connect to, LTE network if None. |
| dns_mode: private DNS mode. |
| hostname: private DNS hostname to set to. |
| """ |
| |
| # set private dns mode |
| if dns_mode: |
| cutils.set_private_dns(self.dut, dns_mode, hostname) |
| # connect to wifi |
| wutils.start_wifi_connection_scan_and_ensure_network_found( |
| self.dut, net[wutils.WifiEnums.SSID_KEY]) |
| wutils.wifi_connect(self.dut, net) |
| self._verify_tls_completed() |
| |
| # start tcpdump on the device |
| self.tcpdump_pid = start_tcpdump(self.dut, self.test_name) |
| |
| # ping hosts should pass |
| for host in self.ping_hosts: |
| self.log.info("Pinging %s" % host) |
| status = wutils.validate_connection(self.dut, host) |
| asserts.assert_true(status, "Failed to ping host %s" % host) |
| self.log.info("Ping successful") |
| |
| # stop tcpdump |
| pcap_file = stop_tcpdump(self.dut, self.tcpdump_pid, self.test_name) |
| |
| # verify DNS queries |
| overhttps = dns_mode != cconst.PRIVATE_DNS_MODE_OFF |
| self._verify_doh_queries(pcap_file, overhttps) |
| |
| # reset wifi |
| wutils.reset_wifi(self.dut) |
| |
| def _verify_tls_completed(self, retry_count=5): |
| """Verify tls finish verification process. |
| |
| Expect all private dns server status, should be |
| "success", or "fail". |
| |
| Args: |
| retry_count: int for retry times. |
| Raises: |
| TimeoutError: if TLS verification stuck in processing. |
| """ |
| for attempt in range(retry_count): |
| out = self.dut.adb.shell("dumpsys dnsresolver") |
| if "status{in_process}" in out: |
| if attempt + 1 < retry_count: |
| self.dut.log.info("DoT still validating, retrying...") |
| time.sleep(DEFAULT_DNS_TIMEOUT) |
| else: |
| return |
| raise TimeoutError("Fail to completed TLS verification.") |
| |
| def _setup_doh(self, ad, net, enable=True): |
| """Enable/Disable DoH option. |
| |
| Args: |
| ad: android devies. |
| net: network as wifi. |
| enable: if True, sets the 'doh' experiment flag. |
| """ |
| if enable: |
| ad.adb.shell("setprop persist.device_config.netd_native.doh 1") |
| else: |
| ad.adb.shell("setprop persist.device_config.netd_native.doh 0") |
| wutils.wifi_connect(ad, net) |
| wutils.reset_wifi(ad) |
| out = ad.adb.shell("dumpsys dnsresolver |grep doh") |
| ad.log.debug(out) |
| |
| def test_mix_server_ipv4_only_wifi_network_with_dns_strict_mode(self): |
| """Test doh flag with below situation. |
| |
| - Android device in strict mode |
| - DNS server supporting both Dns, DoT and DoH protocols |
| - IPv4-only network |
| """ |
| self._test_public_doh_mode(self.dut, self.get_wifi_network(), |
| cconst.PRIVATE_DNS_MODE_STRICT, |
| hostname=cconst.DNS_GOOGLE_HOSTNAME) |
| |
| def test_mix_server_ipv4_ipv6_wifi_network_with_dns_strict_mode(self): |
| """Test doh flag with below situation. |
| |
| - Android device in strict mode |
| - DNS server supporting both Dns, DoT and DoH protocols |
| - IPv4-IPv6 network |
| """ |
| self._test_public_doh_mode(self.dut, self.get_wifi_network(), |
| cconst.PRIVATE_DNS_MODE_STRICT, |
| hostname=cconst.DNS_GOOGLE_HOSTNAME) |
| |
| def test_pure_server_ipv4_only_wifi_network_with_dns_strict_mode(self): |
| """Test doh flag with below situation. |
| |
| - Android device in strict mode |
| - DNS server only supporting DoH protocols |
| - IPv4-only network |
| """ |
| self._test_public_doh_mode(self.dut, self.get_wifi_network(), |
| cconst.PRIVATE_DNS_MODE_STRICT, |
| hostname=cconst.DOH_CLOUDFLARE_HOSTNAME) |
| |
| def test_pure_server_ipv4_ipv6_wifi_network_with_dns_strict_mode(self): |
| """Test doh flag with below situation. |
| |
| - Android device in strict mode |
| - DNS server only supporting DoH protocols |
| - IPv4-IPv6 network |
| """ |
| self._test_public_doh_mode(self.dut, self.get_wifi_network(), |
| cconst.PRIVATE_DNS_MODE_STRICT, |
| hostname=cconst.DOH_CLOUDFLARE_HOSTNAME) |
| |
| def test_mix_server_ipv4_only_wifi_network_with_dns_opportunistic_mode(self): |
| """Test doh flag with below situation. |
| |
| - Android device in opportunistic mode |
| - DNS server supporting both Dns, DoT and DoH protocols |
| - IPv4-only network |
| """ |
| self.default_dns = cconst.DNS_GOOGLE_ADDR_V4 |
| self._test_public_doh_mode(self.dut, self.get_wifi_network(), |
| cconst.PRIVATE_DNS_MODE_OPPORTUNISTIC) |
| |
| def test_mix_server_ipv4_ipv6_wifi_network_with_dns_opportunistic_mode(self): |
| """Test doh flag with below situation. |
| |
| - Android device in opportunistic mode |
| - DNS server supporting both Dns, DoT and DoH protocols |
| - IPv4-IPv6 network |
| """ |
| self.default_dns = cconst.DNS_GOOGLE_ADDR_V4 |
| self.default_dns_v6 = cconst.DNS_GOOGLE_ADDR_V6 |
| self._test_public_doh_mode(self.dut, self.get_wifi_network(), |
| cconst.PRIVATE_DNS_MODE_OPPORTUNISTIC) |
| |
| def test_mix_server_ipv4_only_wifi_network_with_dns_off_mode(self): |
| """Test doh with below situation. |
| |
| - Android device in dns off mode |
| - DNS server supporting both Dns, DoT and DoH protocols |
| - IPv4-only network |
| """ |
| self.default_dns = cconst.DNS_GOOGLE_ADDR_V4 |
| self._test_public_doh_mode(self.dut, self.get_wifi_network(), |
| cconst.PRIVATE_DNS_MODE_OFF) |
| |
| def test_mix_server_ipv4_ipv6_wifi_network_with_dns_off_mode(self): |
| """Test doh with below situation. |
| |
| - Android device in dns off mode |
| - DNS server supporting both Dns, DoT and DoH protocols |
| - IPv4-IPv6 network |
| """ |
| self.default_dns = cconst.DNS_GOOGLE_ADDR_V4 |
| self.default_dns_v6 = cconst.DNS_GOOGLE_ADDR_V6 |
| self._test_public_doh_mode(self.dut, self.get_wifi_network(), |
| cconst.PRIVATE_DNS_MODE_OFF) |