├── ansible ├── inventory ├── .gitignore ├── group_vars │ └── all ├── roles │ ├── git │ │ ├── files │ │ │ └── .gitignore │ │ └── tasks │ │ │ └── main.yml │ ├── users │ │ ├── files │ │ │ └── sudoers_xv_leak_test │ │ └── tasks │ │ │ └── main.yml │ ├── vpn_applications │ │ └── tasks │ │ │ └── main.yml │ ├── selenium_drivers │ │ └── tasks │ │ │ └── main.yml │ ├── python │ │ └── tasks │ │ │ └── main.yml │ └── homebrew │ │ └── tasks │ │ └── main.yml └── macos.yml ├── test ├── __init__.py ├── support │ ├── __init__.py │ └── output_directory.py └── test_device │ ├── __init__.py │ └── folder_to_push_pull │ ├── top_level_file.txt │ └── subfolder │ └── sub_level_file.txt ├── configs ├── __init__.py └── case_studies │ ├── vanilla_leaks.py │ ├── bittorrent_leaks.py │ ├── vpn_process_crashes_leaks.py │ └── vpn_server_rechability_leaks.py ├── devices └── __init__.py ├── generic_tests ├── __init__.py ├── test_fail.py └── test_pass.py ├── desktop_local_tests ├── __init__.py ├── linux │ ├── __init__.py │ ├── test_linux_ip_responder_disrupt_service.py │ ├── test_linux_ip_responder_disrupt_interface.py │ ├── test_linux_ip_responder_disrupt_enable_new_service.py │ ├── linux_service_disrupter.py │ ├── linux_enable_new_service_disrupter.py │ ├── linux_interface_disrupter.py │ └── linux_dns_force_public_resolv_conf.py ├── macos │ ├── __init__.py │ ├── macos_wifi_power_disrupter.py │ ├── macos_service_disrupter.py │ ├── macos_interface_disrupter.py │ ├── test_macos_public_ip_disrupt_wifi_power.py │ ├── test_macos_ip_responder_disrupt_wifi_power.py │ ├── test_macos_dns_disrupt_wifi_power.py │ ├── test_macos_public_ip_disrupt_reorder_services.py │ ├── macos_reorder_services_disrupter.py │ ├── test_macos_ip_responder_disrupt_reorder_services.py │ ├── test_macos_dns_disrupt_reorder_services.py │ ├── macos_enable_new_service_disrupter.py │ ├── test_macos_packet_capture_disrupt_reorder_services.py │ ├── test_macos_packet_capture_disrupt_wifi_power.py │ └── test_macos_public_ip_disrupt_enable_new_service.py ├── windows │ ├── __init__.py │ ├── test_windows_custom_dns_reorder_adapters.py │ ├── test_windows_packet_capture_disrupt_force_public_dns_servers.py │ ├── test_windows_packet_capture_disrupt_enable_new_adapter_generate_traffic.py │ ├── windows_wifi_power_disrupter.py │ ├── windows_adapter_disrupter.py │ ├── windows_enable_new_adapter_disrupter.py │ ├── test_windows_public_ip_disrupt_wifi_power.py │ ├── windows_dns_force_public_dns_servers_disrupter.py │ ├── test_windows_ip_responder_disrupt_wifi_power.py │ ├── test_windows_dns_disrupt_wifi_power.py │ ├── test_windows_packet_capture_disrupt_wifi_power.py │ ├── test_windows_public_ip_disrupt_reorder_adapters.py │ ├── test_windows_dns_disrupt_reorder_adapters.py │ ├── test_windows_public_ip_disrupt_enable_new_adapter.py │ └── test_windows_packet_capture_disrupt_reorder_adapters.py ├── support │ └── ice_lookup │ │ ├── README.md │ │ ├── ice_lookup_no_perms.html │ │ └── ice_lookup_ask_perms.html ├── test_custom_dns_disrupt_cable.py ├── test_packet_capture_disrupt_vpn_connection_generate_traffic.py ├── disrupter_kill_vpn_process.py ├── local_custom_dns_test_case_with_disrupter.py ├── local_ip_responder_test_case_with_disrupter.py ├── local_packet_capture_test_case_with_disrupter.py ├── vpn_connection_disrupter.py ├── dns_helper.py ├── test_ip_responder_vanilla.py ├── test_public_ip_disrupt_kill_vpn_process.py ├── test_packet_capture_vanilla.py ├── test_dns_disrupt_kill_vpn_process.py ├── test_packet_capture_disrupt_kill_vpn_process.py ├── test_ip_responder_disrupt_kill_vpn_process.py ├── test_packet_capture_disrupt_kill_vpn_process_generate_traffic.py ├── local_packet_capture_test_case_with_disrupter_and_generator.py ├── test_public_ip_disrupt_cable.py ├── disrupter_cable.py ├── test_public_ip_disrupt_vpn_connection.py ├── local_test_case.py ├── test_ip_responder_disrupt_cable.py ├── test_dns_disrupt_vpn_connection.py ├── test_dns_disrupt_cable.py └── test_packet_capture_manual.py ├── docs ├── architecture.md ├── how_to_guides │ ├── how_to_add_components.md │ ├── how_to_write_device_inventories.md │ ├── how_to_write_test_configurations.md │ └── how_to_write_tests.md ├── setting_up_test_machines.md ├── setting_up_linux.md └── setting_up_macos.md ├── multimachine_tests ├── __init__.py ├── test_generic_pcap.py ├── multimachine_test_case.py ├── test_data_wifi.py ├── test_eth_wifi.py ├── test_wifi_data.py ├── test_wifi_eth.py ├── test_wifi1_to_wifi2.py ├── test_wifi_off_wifi.py └── test_wifi_off_upstream_wifi.py ├── xv_leak_tools ├── network │ ├── __init__.py │ ├── linux │ │ └── __init__.py │ ├── macos │ │ ├── __init__.py │ │ └── network.py │ ├── windows │ │ ├── __init__.py │ │ └── network.py │ └── common.py ├── test_device │ ├── __init__.py │ ├── device_discoverers │ │ ├── __init__.py │ │ ├── device_discoverer.py │ │ └── static_discoverer.py │ ├── mobile_device.py │ ├── connector.py │ ├── router_device.py │ ├── desktop_device.py │ ├── shell_connector_helper.py │ ├── dummy_connector.py │ ├── create_device.py │ ├── windows_local_shell_connector.py │ ├── ios_device.py │ ├── device_discovery.py │ └── local_shell_connector.py ├── test_components │ ├── __init__.py │ ├── route │ │ ├── linux │ │ │ ├── __init__.py │ │ │ └── linux_route.py │ │ ├── macos │ │ │ ├── __init__.py │ │ │ └── macos_route.py │ │ ├── windows │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── route_builder.py │ ├── cleanup │ │ ├── macos │ │ │ ├── __init__.py │ │ │ └── macos_cleanup.py │ │ ├── __init__.py │ │ ├── ios │ │ │ └── ios_cleanup.py │ │ ├── cleanup.py │ │ ├── linux │ │ │ └── linux_cleanup.py │ │ ├── android │ │ │ └── android_cleanup.py │ │ ├── windows │ │ │ └── windows_cleanup.py │ │ └── cleanup_builder.py │ ├── dns_tool │ │ ├── android │ │ │ ├── __init__.py │ │ │ └── android_dns_tool.py │ │ ├── __init__.py │ │ ├── linux │ │ │ └── linux_dns_tool.py │ │ ├── macos │ │ │ └── macos_dns_tool.py │ │ ├── dns_tool_builder.py │ │ └── windows │ │ │ └── windows_dns_tool.py │ ├── firewall │ │ ├── macos │ │ │ └── __init__.py │ │ ├── windows │ │ │ ├── __init__.py │ │ │ └── windows_firewall.py │ │ ├── __init__.py │ │ ├── firewall.py │ │ └── firewall_builder.py │ ├── network_tool │ │ ├── linux │ │ │ ├── __init__.py │ │ │ └── linux_network_tool.py │ │ ├── macos │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── network_tool_builder.py │ ├── packet_capturer │ │ ├── posix │ │ │ └── __init__.py │ │ ├── windows │ │ │ └── __init__.py │ │ ├── bin │ │ │ ├── xv_packet_capture │ │ │ └── xv_packet_capture.exe │ │ ├── __init__.py │ │ ├── ios │ │ │ └── rvi.sh │ │ └── packet_capturer_builder.py │ ├── vpn_application │ │ ├── ios │ │ │ ├── __init__.py │ │ │ └── ios_vpn_application.py │ │ ├── linux │ │ │ ├── __init__.py │ │ │ └── linux_vpn_application.py │ │ ├── macos │ │ │ ├── __init__.py │ │ │ └── macos_vpn_application.py │ │ ├── android │ │ │ ├── __init__.py │ │ │ └── android_vpn_application.py │ │ ├── windows │ │ │ └── __init__.py │ │ ├── mobile_vpn_application.py │ │ ├── __init__.py │ │ └── vpn_application_builder.py │ ├── network_configuration │ │ ├── steps │ │ │ ├── __init__.py │ │ │ ├── ensure_ipv6.py │ │ │ └── ensure_local_dns.py │ │ ├── __init__.py │ │ ├── network_configuration_step.py │ │ ├── network_configuration_builder.py │ │ └── network_configuration.py │ ├── settings │ │ ├── ios_settings.py │ │ ├── __init__.py │ │ ├── settings_builder.py │ │ ├── settings.py │ │ └── android_settings.py │ ├── git │ │ ├── __init__.py │ │ ├── git_builder.py │ │ └── git.py │ ├── ip_tool │ │ ├── __init__.py │ │ ├── ip_tool_builder.py │ │ ├── icanhazip.py │ │ └── dyndns.py │ ├── open_wrt │ │ ├── __init__.py │ │ ├── open_wrt_builder.py │ │ └── open_wrt.py │ ├── webdriver │ │ ├── __init__.py │ │ └── webdriver_builder.py │ ├── webserver │ │ ├── __init__.py │ │ └── webserver_builder.py │ ├── ip_responder │ │ ├── __init__.py │ │ └── ip_responder_builder.py │ ├── torrent_client │ │ ├── __init__.py │ │ └── torrent_client_builder.py │ ├── component.py │ └── local_component.py ├── test_execution │ ├── __init__.py │ └── test_run_context.py ├── test_framework │ ├── __init__.py │ └── test_factory.py ├── test_templating │ └── __init__.py ├── test_documentation │ ├── __init__.py │ ├── docstring_helpers.py │ └── test_docstring_reader.py ├── scriptlets │ ├── remote_mac_ver.py │ ├── command_line_for_pid.py │ ├── remote_os_kill.py │ ├── macos_open_app.py │ ├── remote_mkstemp.py │ ├── pgrep.py │ ├── remote_mkdir.py │ ├── remote_makedirs.py │ └── wrap_scriptlet.py ├── __init__.py ├── exception.py └── object_parser.py ├── .gitignore ├── servers └── ip_responder │ ├── watch.sh │ └── ip_responder.service ├── requirements_windows.txt ├── requirements_linux.txt ├── requirements_macos.txt ├── activate ├── setup_python.sh ├── tools ├── windows_adapter_info.py ├── fake_device.py ├── test_firewall.py ├── test_docs.py ├── test_simple_ssh.py └── evaluate_test_template.py ├── run_tests.sh ├── Makefile ├── LICENSE └── default_context.py /ansible/inventory: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /configs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /devices/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /generic_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/support/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/test_device/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ansible/.gitignore: -------------------------------------------------------------------------------- 1 | *.retry 2 | -------------------------------------------------------------------------------- /ansible/group_vars/all: -------------------------------------------------------------------------------- 1 | --- 2 | -------------------------------------------------------------------------------- /desktop_local_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/architecture.md: -------------------------------------------------------------------------------- 1 | TODO 2 | -------------------------------------------------------------------------------- /multimachine_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/support/output_directory.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/network/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /desktop_local_tests/linux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/how_to_guides/how_to_add_components.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/network/linux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/network/macos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_execution/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_framework/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_templating/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_documentation/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/how_to_guides/how_to_write_device_inventories.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/route/linux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/route/macos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ansible/roles/git/files/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /docs/how_to_guides/how_to_write_test_configurations.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/macos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/dns_tool/android/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/firewall/macos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/firewall/windows/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/route/windows/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/device_discoverers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_tool/linux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_tool/macos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/packet_capturer/posix/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/ios/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/linux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/macos/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/packet_capturer/windows/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/android/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/windows/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_configuration/steps/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ansible/roles/users/files/sudoers_xv_leak_test: -------------------------------------------------------------------------------- 1 | xv_leak_test ALL = (ALL) NOPASSWD: ALL -------------------------------------------------------------------------------- /ansible/roles/vpn_applications/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # TODO: Install all VPN apps 3 | -------------------------------------------------------------------------------- /test/test_device/folder_to_push_pull/top_level_file.txt: -------------------------------------------------------------------------------- 1 | 1 2 | 2 3 | 3 4 | 4 5 | 5 6 | -------------------------------------------------------------------------------- /test/test_device/folder_to_push_pull/subfolder/sub_level_file.txt: -------------------------------------------------------------------------------- 1 | 5 2 | 4 3 | 3 4 | 2 5 | 1 6 | -------------------------------------------------------------------------------- /docs/how_to_guides/how_to_write_tests.md: -------------------------------------------------------------------------------- 1 | # How to document tests 2 | 3 | doc string and decsribe 4 | -------------------------------------------------------------------------------- /xv_leak_tools/network/windows/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.network.windows.windows_network import WindowsNetwork 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .pythonlocation 3 | python3 4 | output 5 | *.pyc 6 | no_git 7 | *.swp 8 | *.pid 9 | *.log 10 | -------------------------------------------------------------------------------- /servers/ip_responder/watch.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | journalctl --lines 0 --follow _SYSTEMD_UNIT=ip_responder.service 4 | 5 | -------------------------------------------------------------------------------- /requirements_windows.txt: -------------------------------------------------------------------------------- 1 | colorlog 2 | ipaddress 3 | mock 4 | netaddr 5 | netifaces 6 | parameterized 7 | paramiko 8 | pylint 9 | selenium 10 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/settings/ios_settings.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.settings.settings import Settings 2 | 3 | class IOSSettings(Settings): 4 | pass 5 | -------------------------------------------------------------------------------- /requirements_linux.txt: -------------------------------------------------------------------------------- 1 | colorlog 2 | ipaddress 3 | mock 4 | netaddr 5 | netifaces 6 | parameterized 7 | paramiko 8 | psutil 9 | pylint 10 | selenium 11 | python-networkmanager 12 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/dns_tool/android/android_dns_tool.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.component import Component 2 | 3 | class AndroidDNSTool(Component): 4 | pass 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/git/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.git.git_builder import GitBuilder 2 | 3 | def register(factory): 4 | factory.register(GitBuilder()) 5 | -------------------------------------------------------------------------------- /ansible/macos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: macos 3 | roles: 4 | - { role: users } 5 | - { role: homebrew } 6 | - { role: git } 7 | - { role: python } 8 | - { role: selenium_drivers } 9 | -------------------------------------------------------------------------------- /requirements_macos.txt: -------------------------------------------------------------------------------- 1 | colorlog 2 | ipaddress 3 | mock 4 | netaddr 5 | netifaces 6 | parameterized 7 | paramiko 8 | psutil 9 | pylint 10 | selenium 11 | pyobjc-framework-SystemConfiguration 12 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/route/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.route.route_builder import RouteBuilder 2 | 3 | def register(factory): 4 | factory.register(RouteBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/ip_tool/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.ip_tool.ip_tool_builder import IPToolBuilder 2 | 3 | def register(factory): 4 | factory.register(IPToolBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/packet_capturer/bin/xv_packet_capture: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expressvpn/expressvpn_leak_testing/HEAD/xv_leak_tools/test_components/packet_capturer/bin/xv_packet_capture -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.cleanup.cleanup_builder import CleanupBuilder 2 | 3 | def register(factory): 4 | factory.register(CleanupBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/dns_tool/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.dns_tool.dns_tool_builder import DNSToolBuilder 2 | 3 | def register(factory): 4 | factory.register(DNSToolBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/open_wrt/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.open_wrt.open_wrt_builder import OpenWRTBuilder 2 | 3 | def register(factory): 4 | factory.register(OpenWRTBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/packet_capturer/bin/xv_packet_capture.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/expressvpn/expressvpn_leak_testing/HEAD/xv_leak_tools/test_components/packet_capturer/bin/xv_packet_capture.exe -------------------------------------------------------------------------------- /xv_leak_tools/test_components/firewall/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.firewall.firewall_builder import FirewallBuilder 2 | 3 | def register(factory): 4 | factory.register(FirewallBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.settings.settings_builder import SettingsBuilder 2 | 3 | def register(factory): 4 | factory.register(SettingsBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/webdriver/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.webdriver.webdriver_builder import WebdriverBuilder 2 | 3 | def register(factory): 4 | factory.register(WebdriverBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/webserver/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.webserver.webserver_builder import WebserverBuilder 2 | 3 | def register(factory): 4 | factory.register(WebserverBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/ip_responder/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.ip_responder.ip_responder_builder import IPResponderBuilder 2 | 3 | def register(factory): 4 | factory.register(IPResponderBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_tool/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.network_tool.network_tool_builder import NetworkToolBuilder 2 | 3 | def register(factory): 4 | factory.register(NetworkToolBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/mobile_vpn_application.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.vpn_application.vpn_application import VPNApplication 2 | 3 | class MobileVPNApplication(VPNApplication): 4 | pass 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/torrent_client/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.torrent_client.torrent_client_builder import TorrentClientBuilder 2 | 3 | def register(factory): 4 | factory.register(TorrentClientBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/packet_capturer/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.packet_capturer.packet_capturer_builder import PacketCaptureBuilder 2 | 3 | def register(factory): 4 | factory.register(PacketCaptureBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.vpn_application.vpn_application_builder import VPNApplicationBuilder 2 | 3 | def register(factory): 4 | factory.register(VPNApplicationBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/ios/ios_vpn_application.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.vpn_application.mobile_vpn_application import MobileVPNApplication 2 | 3 | class IOSVPNApplication(MobileVPNApplication): 4 | pass 5 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/remote_mac_ver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import platform 3 | import sys 4 | 5 | from wrap_scriptlet import wrap_scriptlet 6 | 7 | def run(): 8 | return platform.mac_ver() 9 | 10 | sys.exit(wrap_scriptlet(run)) 11 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/android/android_vpn_application.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.vpn_application.mobile_vpn_application import MobileVPNApplication 2 | 3 | class AndroidVPNApplication(MobileVPNApplication): 4 | pass 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_configuration/__init__.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.network_configuration.network_configuration_builder import NetworkConfigurationBuilder 2 | 3 | def register(factory): 4 | factory.register(NetworkConfigurationBuilder()) 5 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/ios/ios_cleanup.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.test_components.cleanup.cleanup import Cleanup 3 | 4 | class IOSCleanup(Cleanup): 5 | 6 | def cleanup(self): 7 | L.warning("No cleanup implemented for iOS yet!") 8 | -------------------------------------------------------------------------------- /xv_leak_tools/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pwd 3 | 4 | def tools_root(): 5 | return os.path.normpath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..")) 6 | 7 | def tools_user(): 8 | uid = os.stat(tools_root()).st_uid 9 | return uid, pwd.getpwuid(uid)[0] 10 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/cleanup.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from xv_leak_tools.test_components.component import Component 4 | 5 | class Cleanup(Component, metaclass=ABCMeta): 6 | 7 | @abstractmethod 8 | def cleanup(self): 9 | pass 10 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/linux/linux_cleanup.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.test_components.cleanup.cleanup import Cleanup 3 | 4 | class LinuxCleanup(Cleanup): 5 | 6 | def cleanup(self): 7 | L.warning("No cleanup implemented for Linux yet!") 8 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/android/android_cleanup.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.test_components.cleanup.cleanup import Cleanup 3 | 4 | class AndroidCleanup(Cleanup): 5 | 6 | def cleanup(self): 7 | L.warning("No cleanup implemented for Android yet!") 8 | -------------------------------------------------------------------------------- /servers/ip_responder/ip_responder.service: -------------------------------------------------------------------------------- 1 | # /etc/systemd/system/ip_responder.service 2 | [Unit] 3 | Description=IP Responder service 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | ExecStart=/usr/bin/env python3 -u /path/to/ip_responder/server.py 9 | StandardOutput=syslog 10 | StandardError=syslog 11 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_configuration/network_configuration_step.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_framework.assertions import Assertions 2 | 3 | class NetworkConfigurationStep(Assertions): 4 | 5 | def setup(self, device): 6 | pass 7 | 8 | def teardown(self, device): 9 | pass 10 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/mobile_device.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from xv_leak_tools.test_device.device import Device 3 | 4 | class MobileDevice(Device): 5 | 6 | @abstractmethod 7 | def wakeup(self): 8 | pass 9 | 10 | @abstractmethod 11 | def sleep(self): 12 | pass 13 | -------------------------------------------------------------------------------- /desktop_local_tests/support/ice_lookup/README.md: -------------------------------------------------------------------------------- 1 | The files in the folder provide a stand-alone, local webpage for testing WebRTC leaks. The page is 2 | serverless as IP discovery can be done locally via ICE. 3 | 4 | > TODO: Add STUN/TURN servers. Make sure the tests account for this. Perhaps we do this optionally 5 | or do it by providing two index.html files. 6 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/connector.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | class Connector(metaclass=ABCMeta): 3 | 4 | @abstractmethod 5 | def execute(self, cmd, root=False): 6 | pass 7 | 8 | @abstractmethod 9 | def push(self, src, dst): 10 | pass 11 | 12 | @abstractmethod 13 | def pull(self, src, dst): 14 | pass 15 | -------------------------------------------------------------------------------- /ansible/roles/selenium_drivers/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Install chromedriver" 3 | homebrew: 4 | name: chromedriver 5 | state: latest 6 | 7 | - name: "Install Geckodriver" 8 | homebrew: 9 | name: geckodriver 10 | state: latest 11 | 12 | # - name: "Install Operadriver" 13 | # TODO: How? https://github.com/operasoftware/operachromiumdriver/releases 14 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/router_device.py: -------------------------------------------------------------------------------- 1 | import platform 2 | 3 | from xv_leak_tools.log import L 4 | from xv_leak_tools.test_device.device import Device 5 | 6 | class RouterDevice(Device): 7 | 8 | def os_name(self): 9 | # TODO: Make this dynamic 10 | return 'linux' 11 | 12 | def os_version(self): 13 | return " ".join(platform.linux_distribution()) 14 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/webserver/webserver_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.webserver.webserver import WebServer 3 | 4 | class WebserverBuilder(Builder): 5 | 6 | @staticmethod 7 | def name(): 8 | return 'webserver' 9 | 10 | def build(self, device, config): 11 | return WebServer(device, config) 12 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/ip_responder/ip_responder_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.ip_responder.ip_responder import IPResponder 3 | 4 | class IPResponderBuilder(Builder): 5 | 6 | @staticmethod 7 | def name(): 8 | return 'ip_responder' 9 | 10 | def build(self, device, config): 11 | return IPResponder(device, config) 12 | -------------------------------------------------------------------------------- /desktop_local_tests/test_custom_dns_disrupt_cable.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_custom_dns_test_case_with_disrupter import LocalCustomDNSTestCaseWithDisrupter 2 | from desktop_local_tests.disrupter_cable import DisrupterCable 3 | 4 | class TestCustomDNSDisruptCable(LocalCustomDNSTestCaseWithDisrupter): 5 | 6 | def __init__(self, devices, parameters): 7 | super().__init__(DisrupterCable, devices, parameters) 8 | -------------------------------------------------------------------------------- /activate: -------------------------------------------------------------------------------- 1 | # Wrapper for activating virtual env. 2 | 3 | if [ ! -f .pythonlocation ]; then 4 | echo "Please run setup_python.sh first to configure python" 5 | # Run exit in a subshell to set the last exit code to 1. This file gets sourced so running 6 | # exit directly exits the current shell. 7 | `exit 1` 8 | else 9 | pythonlocation=`cat .pythonlocation` 10 | . $pythonlocation/bin/activate 11 | fi 12 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/torrent_client/torrent_client_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.torrent_client.torrent_client import TorrentClient 3 | 4 | class TorrentClientBuilder(Builder): 5 | 6 | @staticmethod 7 | def name(): 8 | return 'torrent_client' 9 | 10 | def build(self, device, config): 11 | return TorrentClient(device, config) 12 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/linux/linux_vpn_application.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | from xv_leak_tools.test_components.vpn_application.desktop_vpn_application import DesktopVPNApplication 3 | 4 | # TODO: Placeholder for now. Doesn't do anything special 5 | class LinuxVPNApplication(DesktopVPNApplication): 6 | 7 | def __init__(self, app_path, device, config): 8 | super().__init__(app_path, device, config) 9 | -------------------------------------------------------------------------------- /generic_tests/test_fail.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_framework.test_case import TestCase 2 | from xv_leak_tools.log import L 3 | 4 | class TestFail(TestCase): 5 | """Dummy test case for testing library functionality. This test does nothing other than 6 | deliberately fail.""" 7 | 8 | def test(self): 9 | L.info("This test does nothing but fail") 10 | L.describe("Deliberately fail test") 11 | self.assertTrue(False) 12 | -------------------------------------------------------------------------------- /generic_tests/test_pass.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_framework.test_case import TestCase 2 | from xv_leak_tools.log import L 3 | 4 | class TestPass(TestCase): 5 | """Dummy test case for testing library functionality. This test does nothing other than 6 | deliberately pass.""" 7 | 8 | def test(self): 9 | L.info("This test does nothing but pass") 10 | L.describe("Deliberately pass test") 11 | self.assertTrue(True) 12 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/command_line_for_pid.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import sys 4 | 5 | import psutil 6 | 7 | from wrap_scriptlet import wrap_scriptlet 8 | 9 | def run(): 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('pid') 12 | args = parser.parse_args(sys.argv[1:]) 13 | 14 | process = psutil.Process(int(args.pid)) 15 | return process.cmdline() 16 | 17 | sys.exit(wrap_scriptlet(run)) 18 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/remote_os_kill.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os 4 | import sys 5 | 6 | from wrap_scriptlet import wrap_scriptlet 7 | 8 | def run(): 9 | parser = argparse.ArgumentParser() 10 | parser.add_argument('pid') 11 | parser.add_argument('signal') 12 | args = parser.parse_args(sys.argv[1:]) 13 | 14 | os.kill(int(args.pid), int(args.signal)) 15 | return None 16 | 17 | sys.exit(wrap_scriptlet(run)) 18 | -------------------------------------------------------------------------------- /ansible/roles/python/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install pip 3 | shell: "easy_install pip" 4 | become: true 5 | 6 | - name: Install virtualenv 7 | pip: 8 | name: virtualenv 9 | state: latest 10 | executable: /usr/local/bin/pip 11 | become: true 12 | 13 | - name: Setup python virtualenv for the test suite 14 | command: "./setup_python.sh /Users/{{xv_leak_test_user}}/xv_leak_test_python" 15 | args: 16 | chdir: "{{ xv_leak_testing_directory }}" 17 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_configuration/network_configuration_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.network_configuration.network_configuration import NetworkConfiguration 3 | 4 | class NetworkConfigurationBuilder(Builder): 5 | 6 | @staticmethod 7 | def name(): 8 | return 'network_configuration' 9 | 10 | def build(self, device, config): 11 | return NetworkConfiguration(device, config) 12 | -------------------------------------------------------------------------------- /ansible/roles/users/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Setup sudoers file" 3 | lineinfile: 4 | dest: /etc/sudoers 5 | state: present 6 | line: "#includedir /etc/sudoers.d" 7 | become: true 8 | 9 | - name: "Make /etc/sudoers.d directory" 10 | file: 11 | path: /etc/sudoers.d 12 | state: directory 13 | become: true 14 | 15 | - name: "Add sudoers include file" 16 | copy: 17 | src: sudoers_xv_leak_test 18 | dest: /etc/sudoers.d/sudoers_xv_leak_test 19 | become: true 20 | -------------------------------------------------------------------------------- /desktop_local_tests/linux/test_linux_ip_responder_disrupt_service.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case_with_disrupter import LocalIPResponderTestCaseWithDisrupter 2 | from desktop_local_tests.linux.linux_service_disrupter import LinuxServiceDisrupter 3 | 4 | class TestLinuxIPResponderDisruptService(LocalIPResponderTestCaseWithDisrupter): 5 | 6 | # TODO: Docs 7 | 8 | def __init__(self, devices, parameters): 9 | super().__init__(LinuxServiceDisrupter, devices, parameters) 10 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/desktop_device.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | from xv_leak_tools.test_device.device import Device 3 | 4 | class DesktopDevice(Device): 5 | 6 | @abstractmethod 7 | def os_name(self): 8 | pass 9 | 10 | @abstractmethod 11 | def os_version(self): 12 | pass 13 | 14 | def _check_config(self, extra_keys=None): 15 | super()._check_config(['tools_root']) 16 | 17 | def tools_root(self): 18 | return self._config['tools_root'] 19 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_custom_dns_reorder_adapters.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_custom_dns_test_case_with_disrupter import LocalCustomDNSTestCaseWithDisrupter 2 | from desktop_local_tests.windows.windows_reorder_adapters_disrupter import WindowsReorderAdaptersDisrupter 3 | 4 | class TestWindowsCustomDNSDisruptReorderAdapters(LocalCustomDNSTestCaseWithDisrupter): 5 | 6 | def __init__(self, devices, parameters): 7 | super().__init__(WindowsReorderAdaptersDisrupter, devices, parameters) 8 | -------------------------------------------------------------------------------- /setup_python.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | UNAMESTR=`uname` 4 | if [[ $UNAMESTR == *"CYGWIN"* ]]; then 5 | # Explicitly specify the interpretor on Windows else we can end up using a Windows version of 6 | # python (if the user has one) which will fail due to missing posix modules. 7 | PYTHON=/usr/bin/python3 8 | PIP=/usr/bin/pip3 9 | else 10 | PYTHON=python3 11 | PIP=pip3 12 | fi 13 | 14 | $PYTHON -m ensurepip 15 | 16 | $PIP install virtualenv 17 | 18 | $PYTHON tools/setup_python.py $@ 19 | -------------------------------------------------------------------------------- /desktop_local_tests/linux/test_linux_ip_responder_disrupt_interface.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case_with_disrupter import LocalIPResponderTestCaseWithDisrupter 2 | from desktop_local_tests.linux.linux_interface_disrupter import LinuxInterfaceDisrupter 3 | 4 | class TestLinuxIPResponderDisruptInterface(LocalIPResponderTestCaseWithDisrupter): 5 | 6 | # TODO: Docs 7 | 8 | def __init__(self, devices, parameters): 9 | super().__init__(LinuxInterfaceDisrupter, devices, parameters) 10 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/firewall/firewall.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from xv_leak_tools.test_components.component import Component 4 | 5 | class Firewall(Component, metaclass=ABCMeta): 6 | 7 | '''Super simple class for now. It's very focused on us blocking the VPN server IP. Nothing 8 | more complex needed yet.''' 9 | 10 | @abstractmethod 11 | def block_ip(self, ip): 12 | pass 13 | 14 | @abstractmethod 15 | def unblock_ip(self, ip): 16 | pass 17 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/shell_connector_helper.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.process import XVProcessException 2 | 3 | class ShellConnectorHelper: 4 | 5 | # pylint: disable=too-few-public-methods 6 | 7 | def __init__(self, device): 8 | self._device = device 9 | 10 | def check_command(self, cmd, root=False): 11 | ret, stdout, stderr = self._device.connector().execute(cmd, root) 12 | if ret: 13 | raise XVProcessException(cmd, ret, stdout, stderr) 14 | return ret, stdout, stderr 15 | -------------------------------------------------------------------------------- /tools/windows_adapter_info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # TODO: Remove this once the sys.path.append is gone 4 | # pylint: disable=wrong-import-position 5 | 6 | import os 7 | import sys 8 | 9 | sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) 10 | 11 | from xv_leak_tools.network.windows.windows_network import WindowsNetwork 12 | 13 | for adapter in WindowsNetwork.adapters_in_priority_order(): 14 | print(adapter.pretty_string()) 15 | # print(adapter.all_string()) 16 | print("-" * 80) 17 | -------------------------------------------------------------------------------- /desktop_local_tests/linux/test_linux_ip_responder_disrupt_enable_new_service.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case_with_disrupter import LocalIPResponderTestCaseWithDisrupter 2 | from desktop_local_tests.linux.linux_enable_new_service_disrupter import LinuxEnableNewServiceDisrupter 3 | 4 | class TestLinuxIPResponderDisruptEnableNewService(LocalIPResponderTestCaseWithDisrupter): 5 | 6 | # TODO: Docs 7 | 8 | def __init__(self, devices, config): 9 | super().__init__(LinuxEnableNewServiceDisrupter, devices, config) 10 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/macos_open_app.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import subprocess 4 | import sys 5 | import time 6 | 7 | from wrap_scriptlet import wrap_scriptlet 8 | 9 | def run(): 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('bundle_path') 12 | args = parser.parse_args(sys.argv[1:]) 13 | 14 | subprocess.call(['open', args.bundle_path]) 15 | # Pause briefly to allow app to open 16 | # TODO: not ideal 17 | time.sleep(1) 18 | return 0 19 | 20 | sys.exit(wrap_scriptlet(run)) 21 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/component.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.exception import XVEx 2 | 3 | class ComponentNotSupported(XVEx): 4 | pass 5 | 6 | class Component: # pylint: disable=too-few-public-methods 7 | 8 | def __init__(self, device, config): 9 | self._device = device 10 | self._config = config 11 | 12 | def report_info(self): 13 | return "No info available for Component {}".format(self.__class__.__name__) 14 | 15 | def setup(self): 16 | pass 17 | 18 | def teardown(self): 19 | pass 20 | -------------------------------------------------------------------------------- /xv_leak_tools/exception.py: -------------------------------------------------------------------------------- 1 | # Base exception for all exceptions which we explicitly throw. 2 | class XVEx(Exception): 3 | 4 | UNSPECIFIED = 0 5 | 6 | def __init__(self, message, code=UNSPECIFIED): 7 | super().__init__(message) 8 | self._code = code 9 | 10 | def __str__(self): 11 | code_msg = '' 12 | if self._code != 0: 13 | code_msg += "code: {} ".format(self.code()) 14 | return "XVEx: {}{}".format(code_msg, Exception.__str__(self)) 15 | 16 | def code(self): 17 | return self._code 18 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/dns_tool/linux/linux_dns_tool.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.test_components.dns_tool.dns_lookup_tool import DNSLookupTool 3 | 4 | class LinuxDNSTool(DNSLookupTool): 5 | 6 | def known_servers(self): 7 | L.warning( 8 | 'TODO: Implement DNS server discovery for Linux. Currently we just return the DNS ' 9 | 'server used for a DNSserver') 10 | dns_servers = [self.lookup()[0]] 11 | self._check_current_dns_server_is_known(dns_servers) 12 | return dns_servers 13 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/git/git_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.git.git import Git 3 | 4 | class GitBuilder(Builder): 5 | 6 | @staticmethod 7 | def name(): 8 | return "git" 9 | 10 | def build(self, device, config): 11 | # LocalComponent will handle this not working on non-desktop devices. 12 | 13 | # TODO: This needs to work remotely as we may want to git checkout other desktop machines 14 | # which are being used in tests 15 | return Git(device, config) 16 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_configuration/steps/ensure_ipv6.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.network_configuration.network_configuration_step import NetworkConfigurationStep 2 | 3 | class EnsureIPv6(NetworkConfigurationStep): 4 | 5 | def setup(self, device): 6 | # TODO: Use a different exception type to distinguish unable to run from test failures. 7 | self.assertNotEmpty( 8 | device["ip_tool"].public_ipv6_addresses(), 9 | "This test requires an IPv6 connection") 10 | 11 | # Expose the step 12 | Step = EnsureIPv6 13 | -------------------------------------------------------------------------------- /desktop_local_tests/test_packet_capture_disrupt_vpn_connection_generate_traffic.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter_and_generator import LocalPacketCaptureTestCaseWithDisrupterAndGenerator 2 | from desktop_local_tests.vpn_connection_disrupter import VPNConnectionDisrupter 3 | 4 | class TestPacketCaptureDisruptVPNConnectionAndGenerateTraffic( 5 | LocalPacketCaptureTestCaseWithDisrupterAndGenerator): 6 | 7 | # TODO: Docs 8 | 9 | def __init__(self, devices, parameters): 10 | super().__init__(VPNConnectionDisrupter, devices, parameters) 11 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/dummy_connector.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_device.connector import Connector 2 | from xv_leak_tools.log import L 3 | 4 | class DummyConnector(Connector): 5 | 6 | def __init__(self): 7 | L.warning('Using dummy connector') 8 | 9 | @staticmethod 10 | def push(src, dst): 11 | L.debug('{} --> {}'.format(src, dst)) 12 | 13 | def pull(self, src, dst): 14 | self.push(dst, src) 15 | 16 | @staticmethod 17 | def execute(cmd, root=False): 18 | L.debug('Run {}{}'.format(' '.join(cmd), root * ' as root')) 19 | return 0, '', '' 20 | -------------------------------------------------------------------------------- /tools/fake_device.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from xv_leak_tools.import_helpers import import_by_filename 4 | from xv_leak_tools.test_device.device_discoverers.localhost_discoverer import LocalhostDiscoverer 5 | from xv_leak_tools.test_execution.test_run_context import TestRunContext 6 | 7 | def get_fake_device(): 8 | context_dict = import_by_filename('default_context.py').CONTEXT 9 | context_dict['output_directory'] = os.path.expanduser("~/temp") 10 | context = TestRunContext(context_dict) 11 | device = LocalhostDiscoverer(context, {}).discover_device({'device_id': 'localhost'}) 12 | return device 13 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/remote_mkstemp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os 4 | import sys 5 | import tempfile 6 | 7 | from wrap_scriptlet import wrap_scriptlet 8 | 9 | def run(): 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('--suffix') 12 | parser.add_argument('--prefix') 13 | parser.add_argument('--dir') 14 | args = parser.parse_args(sys.argv[1:]) 15 | 16 | filehandle, filename = tempfile.mkstemp(suffix=args.suffix, prefix=args.prefix, dir=args.dir) 17 | os.close(filehandle) 18 | return filename 19 | 20 | sys.exit(wrap_scriptlet(run)) 21 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/packet_capturer/ios/rvi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Opens a remote virtual interface for a single iPhone 3 | 4 | udid=$(instruments -s devices | grep -v "Simulator" | grep "iPhone" | awk -F '[][]' '{print $2}') 5 | 6 | if [ -z $udid ]; then 7 | echo "No UDID found" 1>&2 8 | exit 1 9 | else 10 | echo "Found UDID $udid" 11 | fi 12 | 13 | rvi="rvi0" 14 | if [[ $(ifconfig -l) =~ $rvi ]]; then 15 | echo "The $rvi interface already exists" 1>&2 16 | exit 2 17 | else 18 | rvictl -s $udid 19 | rvi_success=$? 20 | exit $rvi_success 21 | fi 22 | 23 | exit 1 24 | 25 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/open_wrt/open_wrt_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.component import ComponentNotSupported 3 | from xv_leak_tools.test_components.open_wrt.open_wrt import OpenWRT 4 | 5 | class OpenWRTBuilder(Builder): 6 | 7 | @staticmethod 8 | def name(): 9 | return 'open_wrt' 10 | 11 | def build(self, device, config): 12 | if device.os_name() != 'linux': 13 | raise ComponentNotSupported( 14 | "Can't create open_wrt tools for : {}".format(device.os_name())) 15 | 16 | return OpenWRT(device, config) 17 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_packet_capture_disrupt_force_public_dns_servers.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter import LocalPacketCaptureTestCaseWithDisrupter 2 | from desktop_local_tests.windows.windows_dns_force_public_dns_servers_disrupter import WindowsDNSForcePublicDNSServersDisrupter 3 | 4 | class TestWindowsPacketCaptureDisruptForcePublicDNSServers(LocalPacketCaptureTestCaseWithDisrupter): 5 | 6 | # TODO: Make the packet capture here DNS specific? 7 | 8 | def __init__(self, devices, parameters): 9 | super().__init__(WindowsDNSForcePublicDNSServersDisrupter, devices, parameters) 10 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/ip_tool/ip_tool_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.component import ComponentNotSupported 3 | from xv_leak_tools.test_components.ip_tool.ip_tool_curl import IPToolCurl 4 | 5 | class IPToolBuilder(Builder): 6 | 7 | @staticmethod 8 | def name(): 9 | return 'ip_tool' 10 | 11 | def build(self, device, config): 12 | if device.os_name() in ['macos', 'windows', 'linux']: 13 | return IPToolCurl(device, config) 14 | raise ComponentNotSupported("ip_tool is not currently supported on {}".format( 15 | device.os_name())) 16 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_packet_capture_disrupt_enable_new_adapter_generate_traffic.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter_and_generator import LocalPacketCaptureTestCaseWithDisrupterAndGenerator 2 | from desktop_local_tests.windows.windows_enable_new_adapter_disrupter import WindowsEnableNewAdapterDisrupter 3 | 4 | class TestWindowsPacketCaptureDisruptEnableNewAdapterAndGenerateTraffic( 5 | LocalPacketCaptureTestCaseWithDisrupterAndGenerator): 6 | 7 | # TODO: Docs 8 | 9 | def __init__(self, devices, parameters): 10 | super().__init__(WindowsEnableNewAdapterDisrupter, devices, parameters) 11 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | RUN_TESTS="./tools/run_tests.py" 4 | 5 | # Use the correct python version 6 | . activate 7 | 8 | for var in "$@" 9 | do 10 | # Don't force sudo if the user just wants help 11 | if [[ "$var" == "-h" || "$var" == "--help" ]]; then 12 | $RUN_TESTS $@ 13 | exit $? 14 | fi 15 | done 16 | 17 | unamestr=`uname` 18 | if [[ "$unamestr" == 'Linux' ]]; then 19 | echo "Leak tools require root permissions..." 20 | sudo env "PATH=$PATH" $RUN_TESTS $@ 21 | elif [[ "$unamestr" == 'Darwin' ]]; then 22 | echo "Leak tools require root permissions..." 23 | sudo $RUN_TESTS $@ 24 | else 25 | $RUN_TESTS $@ 26 | fi 27 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/dns_tool/macos/macos_dns_tool.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.dns_tool.dns_lookup_tool import DNSLookupTool 2 | 3 | class MacOSDNSTool(DNSLookupTool): 4 | 5 | # pylint: disable=no-self-use 6 | 7 | # N.B. This doesn't work remotely when why we are a LocalComponent 8 | def known_servers(self): 9 | # Import here to allow the file to be imported on any OS 10 | from xv_leak_tools.network.macos.locations_and_services import MacOSNetwork 11 | 12 | dns_servers = MacOSNetwork.dns_servers() 13 | # Quick sanity check 14 | self._check_current_dns_server_is_known(dns_servers) 15 | return dns_servers 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint 2 | lint: 3 | git lint 4 | 5 | .PHONY: lint-all 6 | lint-all: 7 | . activate && find . -iname "*.py" -a -not -path "./no_git/*" | xargs pylint 8 | 9 | .PHONY: lint-install 10 | lint-install: 11 | . activate && pip install pylint && pip install git-lint 12 | 13 | .PHONY: test 14 | test: 15 | . activate && python -m unittest discover -v 16 | 17 | # TODO: None of the setup really works well or is cross platform 18 | setup: setup_homebrew setup_python 19 | 20 | setup_homebrew: 21 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" && \ 22 | brew install python 23 | 24 | setup_python: ./setup_python.sh ~/xv_leak_testing_python 25 | -------------------------------------------------------------------------------- /desktop_local_tests/support/ice_lookup/ice_lookup_no_perms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xv_leak_tools local ICE IP detection 7 | 8 | 9 | 15 | 16 | 17 |
18 | 19 | 20 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/pgrep.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import sys 4 | 5 | import psutil 6 | 7 | from wrap_scriptlet import wrap_scriptlet, debug_output 8 | 9 | def run(): 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument('process_name') 12 | args = parser.parse_args(sys.argv[1:]) 13 | 14 | pids = [] 15 | for proc in psutil.process_iter(): 16 | try: 17 | debug_output(proc.exe()) 18 | if args.process_name in proc.exe(): 19 | pids.append(proc.pid) 20 | except (psutil.ZombieProcess, psutil.AccessDenied, FileNotFoundError) as _: 21 | continue 22 | return pids 23 | 24 | sys.exit(wrap_scriptlet(run)) 25 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/packet_capturer/packet_capturer_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.component import ComponentNotSupported 3 | from xv_leak_tools.test_components.packet_capturer.packet_capturer import PacketCapturer 4 | 5 | class PacketCaptureBuilder(Builder): 6 | 7 | @staticmethod 8 | def name(): 9 | return 'packet_capturer' 10 | 11 | def build(self, device, config): 12 | if device.os_name() not in ['macos', 'linux', 'windows']: 13 | raise ComponentNotSupported("packet_capturer is not currently supported on {}".format( 14 | device.os_name())) 15 | 16 | return PacketCapturer(device, config) 17 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/local_component.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.component import Component, ComponentNotSupported 2 | from xv_leak_tools.test_device.local_shell_connector import LocalShellConnector 3 | 4 | class LocalComponent(Component): 5 | 6 | def __init__(self, device, config): 7 | super().__init__(device, config) 8 | self._check_connector_is_local() 9 | 10 | def _check_connector_is_local(self): 11 | if isinstance(self._device.connector(), LocalShellConnector): 12 | return 13 | 14 | raise ComponentNotSupported( 15 | "Component {} can only be used directly on device. It will not work remotely".format( 16 | self.__class__.__name__)) 17 | -------------------------------------------------------------------------------- /desktop_local_tests/support/ice_lookup/ice_lookup_ask_perms.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | xv_leak_tools local ICE IP detection 7 | 8 | 9 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/remote_mkdir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os 4 | import sys 5 | 6 | from wrap_scriptlet import wrap_scriptlet 7 | 8 | class ActionStringToOctal(argparse.Action): # pylint: disable=too-few-public-methods 9 | def __call__(self, parser, namespace, values, option_string=None): 10 | setattr(namespace, self.dest, int(values, 8)) 11 | 12 | def run(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument('path') 15 | parser.add_argument('--mode', action=ActionStringToOctal) 16 | args = parser.parse_args(sys.argv[1:]) 17 | 18 | if args.mode: 19 | os.mkdir(args.path, args.mode) 20 | else: 21 | os.mkdir(args.path) 22 | return None 23 | 24 | sys.exit(wrap_scriptlet(run)) 25 | -------------------------------------------------------------------------------- /desktop_local_tests/disrupter_kill_vpn_process.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class DisrupterKillVPNProcess(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self._restrict_parameters(must_disrupt=True, must_restore=False) 9 | 10 | def disrupt(self): 11 | L.describe('Find the VPN processes and kill them (not the main application)') 12 | pids = self._device['vpn_application'].vpn_processes() 13 | self.assertNotEmpty(pids, 'Found no VPN processes. This should not happen') 14 | 15 | for pid in pids: 16 | self._device.kill_process(pid) 17 | L.info("Killed VPN process (PID {})".format(pid)) 18 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/windows/windows_cleanup.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.cleanup.cleanup_vpns import CleanupVPNs 2 | 3 | class WindowsCleanup(CleanupVPNs): 4 | 5 | # You can add more applications, processes etc. here or you can override this class 6 | # and the vpn_application component to avoid editing this one. 7 | 8 | VPN_PROCESS_NAMES = [ 9 | 'openvpn' 10 | ] 11 | 12 | VPN_APPLICATIONS = [ 13 | 'ExpressVPN.exe', 14 | ] 15 | 16 | UNKILLABLE_APPLICATIONS = [] 17 | 18 | def __init__(self, device, config): 19 | super().__init__( 20 | device, config, 21 | WindowsCleanup.VPN_PROCESS_NAMES, WindowsCleanup.VPN_APPLICATIONS, 22 | WindowsCleanup.UNKILLABLE_APPLICATIONS) 23 | -------------------------------------------------------------------------------- /desktop_local_tests/local_custom_dns_test_case_with_disrupter.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from desktop_local_tests.local_custom_dns_test_case import LocalCustomDNSTestCase 3 | 4 | class LocalCustomDNSTestCaseWithDisrupter(LocalCustomDNSTestCase): 5 | 6 | def __init__(self, disrupter_class, devices, parameters): 7 | super().__init__(devices, parameters) 8 | self.disrupter = disrupter_class(self.localhost, self.parameters) 9 | 10 | def setup(self): 11 | super().setup() 12 | self.disrupter.setup() 13 | 14 | def test_with_custom_dns(self): 15 | L.describe("Create disruption...") 16 | self.disrupter.create_disruption() 17 | 18 | def teardown(self): 19 | self.disrupter.teardown() 20 | super().teardown() 21 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/remote_makedirs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import os 4 | import sys 5 | 6 | from wrap_scriptlet import wrap_scriptlet 7 | 8 | class ActionStringToOctal(argparse.Action): # pylint: disable=too-few-public-methods 9 | def __call__(self, parser, namespace, values, option_string=None): 10 | setattr(namespace, self.dest, int(values, 8)) 11 | 12 | def run(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument('path') 15 | parser.add_argument('--mode', action=ActionStringToOctal) 16 | args = parser.parse_args(sys.argv[1:]) 17 | 18 | if args.mode: 19 | os.makedirs(args.path, args.mode) 20 | else: 21 | os.makedirs(args.path) 22 | return None 23 | 24 | sys.exit(wrap_scriptlet(run)) 25 | -------------------------------------------------------------------------------- /desktop_local_tests/local_ip_responder_test_case_with_disrupter.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from desktop_local_tests.local_ip_responder_test_case import LocalIPResponderTestCase 3 | 4 | class LocalIPResponderTestCaseWithDisrupter(LocalIPResponderTestCase): 5 | 6 | def __init__(self, disrupter_class, devices, parameters): 7 | super().__init__(devices, parameters) 8 | self.disrupter = disrupter_class(self.localhost, self.parameters) 9 | 10 | def setup(self): 11 | super().setup() 12 | self.disrupter.setup() 13 | 14 | def test_with_ip_responder(self): 15 | L.describe("Create disruption...") 16 | self.disrupter.create_disruption() 17 | 18 | def teardown(self): 19 | self.disrupter.teardown() 20 | super().teardown() 21 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_configuration/steps/ensure_local_dns.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.network_configuration.network_configuration_step import NetworkConfigurationStep 2 | 3 | class EnsureLocalDNS(NetworkConfigurationStep): 4 | 5 | def setup(self, device): 6 | dns_servers = device["dns_tool"].known_servers() 7 | for dns_server in dns_servers: 8 | if dns_server.is_private: 9 | return 10 | 11 | # TODO: Use a different exception type to distinguish unable to run from test failures. 12 | self.assertTrue( 13 | False, 14 | "This test requires a DNS server with a local IP. Available DNS servers are: {}".format( 15 | dns_servers)) 16 | 17 | # Expose the step 18 | Step = EnsureLocalDNS 19 | -------------------------------------------------------------------------------- /desktop_local_tests/local_packet_capture_test_case_with_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case import LocalPacketCaptureTestCase 2 | from xv_leak_tools.log import L 3 | 4 | class LocalPacketCaptureTestCaseWithDisrupter(LocalPacketCaptureTestCase): 5 | 6 | def __init__(self, disrupter_class, devices, parameters): 7 | super().__init__(devices, parameters) 8 | self.disrupter = disrupter_class(self.localhost, self.parameters) 9 | 10 | def setup(self): 11 | super().setup() 12 | self.disrupter.setup() 13 | 14 | def test_with_packet_capture(self): 15 | L.describe("Create disruption...") 16 | self.disrupter.create_disruption() 17 | 18 | def teardown(self): 19 | self.disrupter.teardown() 20 | super().teardown() 21 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/settings/settings_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.component import ComponentNotSupported 3 | from xv_leak_tools.test_components.settings.android_settings import AndroidSettings 4 | from xv_leak_tools.test_components.settings.ios_settings import IOSSettings 5 | 6 | class SettingsBuilder(Builder): 7 | 8 | @staticmethod 9 | def name(): 10 | return 'settings' 11 | 12 | def build(self, device, config): 13 | if device.os_name() == 'android': 14 | return AndroidSettings(device, config) 15 | elif device.os_name() == 'ios': 16 | return IOSSettings(device, config) 17 | raise ComponentNotSupported("settings is not currently supported on {}".format( 18 | device.os_name())) 19 | -------------------------------------------------------------------------------- /desktop_local_tests/vpn_connection_disrupter.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from desktop_local_tests.disrupter import Disrupter 3 | 4 | class VPNConnectionDisrupter(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self._restrict_parameters(must_disrupt=True, must_restore=False) 9 | self._vpn_server_ip = None 10 | 11 | def disrupt(self): 12 | L.describe('Block traffic to and from the VPN server with firewall rules') 13 | self._vpn_server_ip = self._device['vpn_application'].vpn_server_ip() 14 | self._device['firewall'].block_ip(self._vpn_server_ip) 15 | 16 | def teardown(self): 17 | if self._vpn_server_ip: 18 | self._device['firewall'].unblock_ip(self._vpn_server_ip) 19 | super().teardown() 20 | -------------------------------------------------------------------------------- /xv_leak_tools/test_execution/test_run_context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from xv_leak_tools.factory import Factory 4 | from xv_leak_tools.test_framework.test_factory import TestFactory 5 | 6 | class TestRunContext: 7 | 8 | # pylint: disable=too-few-public-methods 9 | 10 | def __init__(self, context_dict): 11 | self._context_dict = context_dict 12 | self._set_sys_path() 13 | 14 | def __getitem__(self, key): 15 | if key == 'component_factory': 16 | return Factory(self._context_dict['component_packages']) 17 | elif key == 'test_factory': 18 | return TestFactory(self._context_dict['test_packages']) 19 | return self._context_dict[key] 20 | 21 | def _set_sys_path(self): 22 | for path in self._context_dict['package_paths']: 23 | sys.path.append(path) 24 | -------------------------------------------------------------------------------- /ansible/roles/git/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Copy git ssh keys" 3 | copy: 4 | src: "{{ item }}" 5 | dest: "/Users/{{xv_leak_test_user}}/.ssh" 6 | mode: 0600 7 | with_items: 8 | - "id_rsa_github" 9 | - "id_rsa_github.pub" 10 | 11 | - name: "git clone and update xv_leak_testing" 12 | git: 13 | repo: 'git@github.com:xvpn/xv_leak_testing.git' 14 | dest: "{{ xv_leak_testing_directory }}" 15 | key_file: "/Users/{{xv_leak_test_user}}/.ssh/id_rsa_github" 16 | version: CS-1-jenkins-ci 17 | update: yes 18 | 19 | - name: "git clone and update xv_leak_testing_internal" 20 | git: 21 | repo: 'git@github.com:xvpn/xv_leak_testing_internal.git' 22 | dest: "{{ xv_leak_testing_internal_directory }}" 23 | key_file: "/Users/{{xv_leak_test_user}}/.ssh/id_rsa_github" 24 | version: master 25 | update: yes 26 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/macos/macos_cleanup.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_components.cleanup.cleanup_vpns import CleanupVPNs 2 | 3 | class MacOSCleanup(CleanupVPNs): 4 | 5 | # You can add more applications, processes etc. here or you can override this class 6 | # and the vpn_application component to avoid editing this one. 7 | 8 | VPN_PROCESS_NAMES = [ 9 | 'openvpn', 10 | 'racoon', 11 | 'pppd', 12 | ] 13 | 14 | VPN_APPLICATIONS = [ 15 | '/Applications/ExpressVPN.app/Contents/MacOS/ExpressVPN', 16 | ] 17 | 18 | UNKILLABLE_APPLICATIONS = [] 19 | 20 | def __init__(self, device, config): 21 | super().__init__( 22 | device, config, 23 | MacOSCleanup.VPN_PROCESS_NAMES, MacOSCleanup.VPN_APPLICATIONS, 24 | MacOSCleanup.UNKILLABLE_APPLICATIONS) 25 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_configuration/network_configuration.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | # from xv_leak_tools.log import L 3 | from xv_leak_tools.test_components.local_component import LocalComponent 4 | 5 | class NetworkConfiguration(LocalComponent): 6 | 7 | def __init__(self, device, config): 8 | super().__init__(device, config) 9 | self._steps = [] 10 | for step in self._config.get("steps", []): 11 | package = "xv_leak_tools.test_components.network_configuration.steps.{}".format(step) 12 | module = importlib.import_module(package) 13 | self._steps.append(module.Step()) 14 | 15 | def setup(self): 16 | for step in self._steps: 17 | step.setup(self._device) 18 | 19 | def teardown(self): 20 | for step in self._steps: 21 | step.teardown(self._device) 22 | -------------------------------------------------------------------------------- /tools/test_firewall.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | 6 | # TODO: I think we solve this problem by making a proper pip module 7 | # Add the root so we can import xv_leak_tools 8 | sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) 9 | 10 | import fake_device 11 | from xv_leak_tools.log import L 12 | from xv_leak_tools.test_components.firewall.linux.linux_firewall import LinuxFirewall 13 | from xv_leak_tools.test_components.firewall.macos.macos_firewall import MacOSFirewall 14 | 15 | L.configure({ 16 | 'trace': { 17 | 'level': L.DEBUG, 18 | }, 19 | 'describe': { 20 | 'file_format': None, 21 | }, 22 | 'report': { 23 | 'file_format': None, 24 | }, 25 | }) 26 | 27 | lf = MacOSFirewall(fake_device.get_fake_device(), {}) 28 | lf.block_ip("8.8.8.8") 29 | lf.unblock_ip("8.8.8.8") 30 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/webdriver/webdriver_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.webdriver.webdriver import XVDriverLinux, XVDriverMacOS, XVDriverWindows 3 | from xv_leak_tools.test_components.component import ComponentNotSupported 4 | 5 | class WebdriverBuilder(Builder): 6 | # TODO: Test this on MacOS, Windows 7 | 8 | @staticmethod 9 | def name(): 10 | return 'webdriver' 11 | 12 | def build(self, device, config): 13 | if device.os_name() == 'linux': 14 | return XVDriverLinux(device, config) 15 | elif device.os_name() == 'macos': 16 | return XVDriverMacOS(device, config) 17 | elif device.os_name() == 'windows': 18 | return XVDriverWindows(device, config) 19 | raise ComponentNotSupported("Can't build a webdriver on {}".format(device.os_name())) 20 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/windows_wifi_power_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class WindowsWifiPowerDisrupter(Disrupter): 6 | 7 | VIA = 'via the Start->Settings->Network & Internet->Wi-Fi' 8 | 9 | def disrupt(self): 10 | L.describe('Disable Wi-Fi') 11 | # TODO: I have no idea how to do this programmatically yet (or if it's even possible). 12 | # Probably it can be done via a registry setting 13 | message_and_await_enter("Disable Wi-Fi {}".format(WindowsWifiPowerDisrupter.VIA)) 14 | 15 | def restore(self): 16 | L.describe('Re-enable Wi-Fi') 17 | message_and_await_enter("Re-enable Wi-Fi {}".format(WindowsWifiPowerDisrupter.VIA)) 18 | 19 | def teardown(self): 20 | self.restore() 21 | super().teardown() 22 | -------------------------------------------------------------------------------- /xv_leak_tools/scriptlets/wrap_scriptlet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import codecs 3 | import pickle 4 | import sys 5 | 6 | def debug_scriptlet(): 7 | '''Set this function to return True if you need to debug any of the scriptlets. It will prevent 8 | output and exceptions from being encoded and also enable logging in the scriplets (if they 9 | implement any logging).''' 10 | return False 11 | 12 | def debug_output(msg): 13 | if not debug_scriptlet(): 14 | return 15 | print("SCRIPTLET DEBUG: {}".format(msg)) 16 | 17 | def wrap_scriptlet(func): 18 | if debug_scriptlet(): 19 | print(func()) 20 | else: 21 | try: 22 | sys.stdout.write(codecs.encode(pickle.dumps(func()), "base64").decode()) 23 | return 0 24 | except BaseException as ex: 25 | sys.stderr.write(codecs.encode(pickle.dumps(ex), "base64").decode()) 26 | return 1 27 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/open_wrt/open_wrt.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.test_components.component import Component 3 | from xv_leak_tools.test_device.shell_connector_helper import ShellConnectorHelper 4 | 5 | class OpenWRT(Component): 6 | 7 | def __init__(self, device, config): 8 | super().__init__(device, config) 9 | self._connector_helper = ShellConnectorHelper(self._device) 10 | 11 | def _execute(self, cmd): 12 | return self._connector_helper.check_command(cmd) 13 | 14 | def set_lan_ip(self, ip): 15 | L.info("Setting LAN IP for router to: {}".format(ip)) 16 | 17 | self._connector_helper.check_command( 18 | ['uci', 'set', "network.lan.ipaddr='{}'".format(ip.exploded)]) 19 | self._connector_helper.check_command(['uci', 'commit', 'network']) 20 | self._connector_helper.check_command(['/etc/init.d/network', 'restart', '&']) 21 | -------------------------------------------------------------------------------- /ansible/roles/homebrew/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Check if xcode developer tools are installed 3 | command: 'xcode-select -p' 4 | register: xcode_result 5 | ignore_errors: true 6 | 7 | - name: Check if xcode developer tools are installed 8 | fail: 9 | msg: "You must manually installed xcode command line tools before proceesing. Use xcode-select --install" 10 | when: xcode_result.rc != 0 11 | 12 | - name: Check if homebrew is installed 13 | stat: 14 | path: /usr/local/bin/brew 15 | register: brew_installed 16 | ignore_errors: true 17 | 18 | - name: "Install homebrew" 19 | shell: TRAVIS=1 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 20 | when: brew_installed.stat.exists != True 21 | 22 | - name: "Update homebrew" 23 | homebrew: 24 | update_homebrew: true 25 | 26 | - name: "Install python3" 27 | homebrew: 28 | name: python3 29 | state: latest 30 | -------------------------------------------------------------------------------- /xv_leak_tools/network/windows/network.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from xv_leak_tools.exception import XVEx 4 | from xv_leak_tools.network.common import RE_IPV4_ADDRESS 5 | from xv_leak_tools.network.windows.windows_network import WindowsNetwork 6 | 7 | def parse_ns_lookup_output(lines): 8 | server = None 9 | prog = re.compile(r"Address:\s*({})\s*".format(RE_IPV4_ADDRESS)) 10 | for line in lines: 11 | matches = prog.match(line) 12 | if not matches: 13 | continue 14 | server = matches.group(1) 15 | break 16 | 17 | if server is None: 18 | raise XVEx("Couldn't parse nslookup output: {}".format(lines)) 19 | 20 | # TODO: Implement parsing of actual DNS IPs 21 | return server, [] 22 | 23 | def known_dns_servers(): 24 | possible_dns_servers = [] 25 | for adapter in WindowsNetwork.adapters(): 26 | possible_dns_servers += adapter.dns_servers() 27 | return list(set(possible_dns_servers)) 28 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/route/route_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.route.macos.macos_route import MacOSRoute 3 | from xv_leak_tools.test_components.route.linux.linux_route import LinuxRoute 4 | from xv_leak_tools.test_components.component import ComponentNotSupported 5 | 6 | class RouteBuilder(Builder): 7 | 8 | @staticmethod 9 | def name(): 10 | return 'route' 11 | 12 | def build(self, device, config): 13 | if device.os_name() == 'linux': 14 | return LinuxRoute(device, config) 15 | elif device.os_name() == 'macos': 16 | return MacOSRoute(device, config) 17 | elif device.os_name() == 'windows': 18 | from xv_leak_tools.test_components.route.windows.windows_route import WindowsRoute 19 | return WindowsRoute(device, config) 20 | raise ComponentNotSupported("Can't build a route component on {}".format(device.os_name())) 21 | -------------------------------------------------------------------------------- /multimachine_tests/test_generic_pcap.py: -------------------------------------------------------------------------------- 1 | from multimachine_tests.multimachine_test_case import MultimachineTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestGenericPacketCapture(MultimachineTestCase): 6 | 7 | def test(self): 8 | L.describe('Open and connect the VPN application') 9 | self.target_device['vpn_application'].open_and_connect() 10 | 11 | L.describe('Capture traffic') 12 | self.capture_device['packet_capturer'].start() 13 | 14 | L.describe('Generate whatever traffic you want') 15 | message_and_await_enter('Are you done?') 16 | 17 | L.describe('Stop capturing traffic') 18 | packets = self.capture_device['packet_capturer'].stop() 19 | 20 | whitelist = self.capture_device.local_ips() 21 | L.debug('Excluding {} from analysis'.format(whitelist)) 22 | self.traffic_analyser.get_vpn_server_ip(packets, whitelist) 23 | -------------------------------------------------------------------------------- /desktop_local_tests/dns_helper.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_framework.assertions import Assertions 2 | 3 | class DNSHelper(Assertions): 4 | 5 | def __init__(self, dns_tool): 6 | self._dns_tool = dns_tool 7 | 8 | def dns_server_is_vpn_server(self, dns_servers_before_connect, vpn_dns_servers, **kwargs): 9 | server = self._dns_tool.lookup(**kwargs)[0] 10 | 11 | # Check DNS server used was not one which was previously known to the system 12 | # Note that this is overkill as the check below is sufficient, but it's helpful for 13 | # getting more info. 14 | self.assertIsNotIn( 15 | server, dns_servers_before_connect, 16 | "DNS server used was {} and was known to the system before connect".format(server)) 17 | 18 | # Check DNS server used was a VPN DNS server 19 | self.assertIsIn( 20 | server, vpn_dns_servers, 21 | "DNS server used was {} but wasn't a VPN DNS server".format(server)) 22 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/ip_tool/icanhazip.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from xv_leak_tools.exception import XVEx 4 | 5 | class ICanHazIP: 6 | 7 | def __init__(self, url_getter): 8 | self._url_getter = url_getter 9 | 10 | def _get_public_ip_addresses(self, url): 11 | stdout = self._url_getter(url)[0].strip() 12 | if stdout == '': 13 | # No ip addresses. This is okay. 14 | return [] 15 | try: 16 | return [ipaddress.ip_address(stdout)] 17 | except ValueError as _: 18 | raise XVEx("Couldn't convert output '{}' from {} into an IP".format(stdout, url)) 19 | 20 | def public_ipv4_addresses(self): 21 | return self._get_public_ip_addresses('http://ipv4.icanhazip.com') 22 | 23 | def public_ipv6_addresses(self): 24 | return self._get_public_ip_addresses('http://ipv6.icanhazip.com') 25 | 26 | def all_public_ip_addresses(self): 27 | return self.public_ipv4_addresses() + self.public_ipv6_addresses() 28 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/macos_wifi_power_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class MacOSWifiPowerDisrupter(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self.wifi_service = self._find_wifi_service() 9 | 10 | def _find_wifi_service(self): 11 | wifi_service = self._device['network_tool'].wifi_service() 12 | L.info("WiFi network service is {}".format(wifi_service.name())) 13 | return wifi_service 14 | 15 | def disrupt(self): 16 | L.describe('Disable power on the Wi-Fi network service') 17 | self.wifi_service.disable_wifi_power() 18 | 19 | def restore(self): 20 | L.describe('Re-enable power on the Wi-Fi network service') 21 | self.wifi_service.enable_wifi_power() 22 | 23 | def teardown(self): 24 | if self.wifi_service: 25 | self.wifi_service.enable_wifi_power() 26 | super().teardown() 27 | -------------------------------------------------------------------------------- /tools/test_docs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # TODO: Remove this once the sys.path.append is gone 4 | # pylint: disable=wrong-import-position 5 | 6 | import os 7 | import sys 8 | 9 | sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) 10 | 11 | from xv_leak_tools.test_documentation.test_docstring_reader import TestDocstringRead 12 | 13 | DOCS = TestDocstringRead([ 14 | 'xv_leak_tools.test_framework', 15 | 'desktop_local_tests', 16 | ]).docs() 17 | 18 | NO_DOCS = [] 19 | 20 | for test_class, doc in list(DOCS.items()): 21 | if doc['doc'] is None: 22 | NO_DOCS.append(test_class) 23 | else: 24 | print(test_class) 25 | print('-' * len(test_class)) 26 | print(doc['doc']) 27 | print() 28 | 29 | if len(NO_DOCS) != 0: 30 | WARN_STRING = 'Warning the following tests have no documentation' 31 | print("\n{}".format(WARN_STRING)) 32 | print('-' * len(WARN_STRING)) 33 | print() 34 | for test_class in NO_DOCS: 35 | print(test_class) 36 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/vpn_application_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.path import windows_safe_path 3 | from xv_leak_tools.test_components.vpn_application.generic_vpn_builder import GenericVPNBuilder 4 | 5 | class VPNApplicationBuilder(Builder): 6 | 7 | VPNs = { 8 | 'express_vpn': { 9 | 'macos': { 10 | 'app': '/Applications/ExpressVPN.app', 11 | }, 12 | 'windows': { 13 | 'app': windows_safe_path( 14 | "C:\\Program Files (x86)\\ExpressVPN\\xvpn-ui\\ExpressVpn.exe"), 15 | 'tap': "ExpressVPN Tap Adapter", 16 | }, 17 | 'linux': { 18 | 'app': '/usr/bin/expressvpn', 19 | }, 20 | }, 21 | } 22 | 23 | @staticmethod 24 | def name(): 25 | return 'vpn_application' 26 | 27 | def build(self, device, config): 28 | return GenericVPNBuilder(VPNApplicationBuilder.VPNs).build(device, config) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 ExpressVPN 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/windows_adapter_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class WindowsAdapterDisrupter(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self._restrict_parameters(must_disrupt=True) 9 | self._primary_adapter = self._find_primary_adapter() 10 | 11 | def _find_primary_adapter(self): 12 | primary_adapter = self._device['network_tool'].primary_adapter() 13 | L.info("Primary network adapter is {}".format(primary_adapter.name())) 14 | return primary_adapter 15 | 16 | def disrupt(self): 17 | L.describe('Disable the primary network adapter') 18 | self._primary_adapter.disable() 19 | 20 | def restore(self): 21 | L.describe('Re-enable the primary network adapter') 22 | self._primary_adapter.enable() 23 | 24 | def teardown(self): 25 | if self._primary_adapter: 26 | self._primary_adapter.enable() 27 | 28 | super().teardown() 29 | -------------------------------------------------------------------------------- /desktop_local_tests/test_ip_responder_vanilla.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case import LocalIPResponderTestCase 2 | 3 | class TestIPResponderVanilla(LocalIPResponderTestCase): 4 | 5 | '''Summary: 6 | 7 | Tests whether traffic leaving the user's device has the public IP hidden. 8 | 9 | Details: 10 | 11 | This test uses a simple UDP client which spams UDP packets to a public server. The server logs 12 | the source IP of every packet. The test checks with the server to make sure that the public IP 13 | is always the VPN server's IP and not the device's. 14 | 15 | Discussion: 16 | 17 | This test has no implementation as the base class handles everything. The only thing that needs 18 | to be done is specify the 'check_period' parameter in the test config. This determines how long 19 | the test will check the IP for. 20 | 21 | This is a vanilla test and makes no attempt to disrupt the VPN. 22 | 23 | Weaknesses: 24 | 25 | None 26 | 27 | Scenarios: 28 | 29 | No restrictions. 30 | 31 | ''' 32 | 33 | pass 34 | -------------------------------------------------------------------------------- /xv_leak_tools/network/macos/network.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from xv_leak_tools.exception import XVEx 4 | from xv_leak_tools.network.common import RE_IPV4_ADDRESS 5 | from xv_leak_tools.network.macos.locations_and_services import MacOSNetwork 6 | 7 | # TODO: MAKE THE dns component do this 8 | 9 | # TODO: Make private? How can I do this if I'm importing *? 10 | def parse_ns_lookup_output(lines): 11 | server = None 12 | prog = re.compile(r"Server:\s*({})\s*".format(RE_IPV4_ADDRESS)) 13 | for line in lines: 14 | matches = prog.match(line) 15 | if not matches: 16 | continue 17 | server = matches.group(1) 18 | break 19 | 20 | if server is None: 21 | raise XVEx("Couldn't parse nslookup output: {}".format(lines)) 22 | 23 | # TODO: Implement parsing of actual DNS IPs 24 | return server, [] 25 | 26 | def known_dns_servers(): 27 | possible_dns_servers = [] 28 | for service in MacOSNetwork.network_services_in_priority_order(): 29 | possible_dns_servers += service.dns_servers() 30 | return list(set(possible_dns_servers)) 31 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/firewall/windows/windows_firewall.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.exception import XVEx 3 | from xv_leak_tools.test_components.firewall.firewall import Firewall 4 | 5 | # TODO: Fix this class. It can only block one ip at once! Make changes to WindowsAdvFirewall if 6 | # necessary 7 | class WindowsFirewall(Firewall): 8 | 9 | def __init__(self, device, config): 10 | super().__init__(device, config) 11 | 12 | from xv_leak_tools.network.windows.adv_firewall import WindowsAdvFirewall 13 | self._adv_firewall = WindowsAdvFirewall 14 | self._rule_name = None 15 | 16 | def block_ip(self, ip): 17 | if self._rule_name is not None: 18 | raise XVEx("Already added block IP rule to firewall!") 19 | L.info("Adding outgoing IP block for {}".format(ip)) 20 | self._rule_name = self._adv_firewall.block_ip(ip) 21 | 22 | def unblock_ip(self, ip): 23 | if self._rule_name is not None: 24 | self._adv_firewall.delete_rule(self._rule_name) 25 | self._rule_name = None 26 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_tool/network_tool_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | from xv_leak_tools.test_components.component import ComponentNotSupported 3 | from xv_leak_tools.test_components.network_tool.macos.macos_network_tool import MacOSNetworkTool 4 | from xv_leak_tools.test_components.network_tool.linux.linux_network_tool import LinuxNetworkTool 5 | from xv_leak_tools.test_components.network_tool.windows.windows_network_tool import WindowsNetworkTool 6 | 7 | class NetworkToolBuilder(Builder): 8 | 9 | @staticmethod 10 | def name(): 11 | return 'network_tool' 12 | 13 | def build(self, device, config): 14 | if device.os_name() == 'macos': 15 | return MacOSNetworkTool(device, config) 16 | elif device.os_name() == 'windows': 17 | return WindowsNetworkTool(device, config) 18 | elif device.os_name() == 'linux': 19 | return LinuxNetworkTool(device, config) 20 | else: 21 | raise ComponentNotSupported("network_tool is not currently supported on {}".format( 22 | device.os_name())) 23 | -------------------------------------------------------------------------------- /multimachine_tests/multimachine_test_case.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.test_framework.test_case import TestCase 3 | from xv_leak_tools.traffic_filter import TrafficFilter 4 | from xv_leak_tools.traffic_analyser import TrafficAnalyser 5 | 6 | class MultimachineTestCase(TestCase): 7 | 8 | def __init__(self, devices, config): 9 | super().__init__(devices, config) 10 | self.target_device = self.devices['target_device'] 11 | self.capture_device = self.devices['packet_capture_device'] 12 | self.traffic_analyser = TrafficAnalyser() 13 | self.traffic_filter = TrafficFilter 14 | 15 | def setup(self): 16 | super().setup() 17 | 18 | L.describe('Ensure no VPN apps are connected or open') 19 | self.target_device['cleanup'].cleanup() 20 | 21 | L.describe('Configure VPN application') 22 | self.target_device['vpn_application'].configure() 23 | 24 | def teardown(self): 25 | self.target_device['vpn_application'].disconnect() 26 | self.target_device['vpn_application'].close() 27 | super().teardown() 28 | -------------------------------------------------------------------------------- /multimachine_tests/test_data_wifi.py: -------------------------------------------------------------------------------- 1 | from multimachine_tests.multimachine_test_case import MultimachineTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestDataWifi(MultimachineTestCase): 6 | 7 | def test(self): 8 | L.describe('Open and connect the VPN application') 9 | self.target_device['vpn_application'].open_and_connect() 10 | 11 | L.describe('Capture traffic') 12 | self.capture_device['packet_capturer'].start() 13 | 14 | L.describe('Generate whatever traffic you want') 15 | message_and_await_enter('Are you done?') 16 | 17 | self.target_device['settings'].enable_wifi() 18 | 19 | L.describe('Generate whatever traffic you want') 20 | message_and_await_enter('Are you done?') 21 | 22 | L.describe('Stop capturing traffic') 23 | packets = self.capture_device['packet_capturer'].stop() 24 | 25 | whitelist = self.capture_device.local_ips() 26 | L.debug('Excluding {} from analysis'.format(whitelist)) 27 | self.traffic_analyser.get_vpn_server_ip(packets, whitelist) 28 | -------------------------------------------------------------------------------- /multimachine_tests/test_eth_wifi.py: -------------------------------------------------------------------------------- 1 | from multimachine_tests.multimachine_test_case import MultimachineTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestEthWifi(MultimachineTestCase): 6 | 7 | def test(self): 8 | L.describe('Open and connect the VPN application') 9 | self.target_device['vpn_application'].open_and_connect() 10 | 11 | L.describe('Capture traffic') 12 | self.capture_device['packet_capturer'].start() 13 | 14 | L.describe('Generate whatever traffic you want') 15 | message_and_await_enter('Are you done?') 16 | 17 | self.target_device['settings'].disable_wired() 18 | 19 | L.describe('Generate whatever traffic you want') 20 | message_and_await_enter('Are you done?') 21 | 22 | L.describe('Stop capturing traffic') 23 | packets = self.capture_device['packet_capturer'].stop() 24 | 25 | whitelist = self.capture_device.local_ips() 26 | L.debug('Excluding {} from analysis'.format(whitelist)) 27 | self.traffic_analyser.get_vpn_server_ip(packets, whitelist) 28 | -------------------------------------------------------------------------------- /multimachine_tests/test_wifi_data.py: -------------------------------------------------------------------------------- 1 | from multimachine_tests.multimachine_test_case import MultimachineTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestWifiData(MultimachineTestCase): 6 | 7 | def test(self): 8 | L.describe('Open and connect the VPN application') 9 | self.target_device['vpn_application'].open_and_connect() 10 | 11 | L.describe('Capture traffic') 12 | self.capture_device['packet_capturer'].start() 13 | 14 | L.describe('Generate whatever traffic you want') 15 | message_and_await_enter('Are you done?') 16 | 17 | self.target_device['settings'].disable_wifi() 18 | 19 | L.describe('Generate whatever traffic you want') 20 | message_and_await_enter('Are you done?') 21 | 22 | L.describe('Stop capturing traffic') 23 | packets = self.capture_device['packet_capturer'].stop() 24 | 25 | whitelist = self.capture_device.local_ips() 26 | L.debug('Excluding {} from analysis'.format(whitelist)) 27 | self.traffic_analyser.get_vpn_server_ip(packets, whitelist) 28 | -------------------------------------------------------------------------------- /multimachine_tests/test_wifi_eth.py: -------------------------------------------------------------------------------- 1 | from multimachine_tests.multimachine_test_case import MultimachineTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestWifiEth(MultimachineTestCase): 6 | 7 | def test(self): 8 | L.describe('Open and connect the VPN application') 9 | self.target_device['vpn_application'].open_and_connect() 10 | 11 | L.describe('Capture traffic') 12 | self.capture_device['packet_capturer'].start() 13 | 14 | L.describe('Generate whatever traffic you want') 15 | message_and_await_enter('Are you done?') 16 | 17 | self.target_device['settings'].enable_wired() 18 | 19 | L.describe('Generate whatever traffic you want') 20 | message_and_await_enter('Are you done?') 21 | 22 | L.describe('Stop capturing traffic') 23 | packets = self.capture_device['packet_capturer'].stop() 24 | 25 | whitelist = self.capture_device.local_ips() 26 | L.debug('Excluding {} from analysis'.format(whitelist)) 27 | self.traffic_analyser.get_vpn_server_ip(packets, whitelist) 28 | -------------------------------------------------------------------------------- /desktop_local_tests/linux/linux_service_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class LinuxServiceDisrupter(Disrupter): 5 | def __init__(self, devices, parameters): 6 | super().__init__(devices, parameters) 7 | self._restrict_parameters(must_disrupt=True) 8 | self.primary_service = self._find_primary_service() 9 | 10 | def _find_primary_service(self): 11 | services = self._device['network_tool'].network_services_in_priority_order() 12 | primary_service = [service for service in services if service.active()][0] 13 | L.info("Primary network service is {}".format(primary_service.name())) 14 | return primary_service 15 | 16 | def disrupt(self): 17 | L.describe('Disable the primary network service') 18 | self.primary_service.disable() 19 | 20 | def restore(self): 21 | L.describe('Re-enable the primary network service') 22 | self.primary_service.enable() 23 | 24 | def teardown(self): 25 | if self.primary_service: 26 | self.primary_service.enable() 27 | 28 | super().teardown() 29 | -------------------------------------------------------------------------------- /configs/case_studies/vanilla_leaks.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_templating.templating import TemplateEvaluator, Replacee, Each 2 | 3 | # This list will contain all the individual test configurations 4 | TESTS = [] 5 | 6 | # Very simple template for the vanilla tests. There are no parameters or component specific 7 | # configurations here. 8 | TEMPLATE = { 9 | 'name': Replacee("$NAME"), 10 | 'devices': [ 11 | { 12 | "discovery_keys": { 13 | "device_id": "localhost" 14 | }, 15 | "device_name": "localhost", 16 | 'components': { 17 | 'vpn_application': { 18 | 'name': 'generic', 19 | } 20 | }, 21 | } 22 | ], 23 | } 24 | 25 | TEMPLATE_PARAMETERS_LIST = [ 26 | { 27 | 'TEMPLATE': TEMPLATE, 28 | '$NAME': Each([ 29 | 'TestDNSVanilla', 30 | 'TestDNSVanillaAggressive', 31 | 'TestPublicIPAddress', 32 | 'TestDNSVanillaAggressivePacketCapture', 33 | 'TestIPResponderVanilla']), 34 | }, 35 | ] 36 | 37 | TESTS += TemplateEvaluator.generate(TEMPLATE_PARAMETERS_LIST) 38 | -------------------------------------------------------------------------------- /tools/test_simple_ssh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # TODO: Remove this once the sys.path.append is gone 4 | # pylint: disable=wrong-import-position 5 | 6 | import os 7 | import sys 8 | 9 | # TODO: I think we solve this problem by making a proper pip module 10 | # Add the root so we can import xv_leak_tools 11 | sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) 12 | 13 | from xv_leak_tools.log import L 14 | from xv_leak_tools.test_device.simple_ssh_connector import SimpleSSHConnector 15 | # from xv_leak_tools.test_device.static_device_discoverer import StaticDeviceDiscoverer 16 | 17 | L.configure({ 18 | 'trace': { 19 | 'level': L.VERBOSE, 20 | }, 21 | 'describe': { 22 | 'file_format': None, 23 | }, 24 | 'report': { 25 | 'file_format': None, 26 | }, 27 | }) 28 | 29 | CONNECTOR = SimpleSSHConnector( 30 | ips=['10.163.0.1'], username='root', 31 | ssh_key=os.path.expanduser('~/.ssh/id_rsa'), 32 | ssh_password=None) 33 | 34 | RET, STDOUT, STDERR = CONNECTOR.execute(['ls']) 35 | 36 | print(RET) 37 | print("--------------") 38 | print(STDOUT) 39 | print("--------------") 40 | print(STDERR) 41 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/macos_service_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class MacOSServiceDisrupter(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self._restrict_parameters(must_disrupt=True) 9 | 10 | self.primary_service = self._find_primary_service() 11 | 12 | def _find_primary_service(self): 13 | services = self._device['network_tool'].network_services_in_priority_order() 14 | primary_service = [service for service in services if service.active()][0] 15 | L.info("Primary network service is {}".format(primary_service.name())) 16 | return primary_service 17 | 18 | def disrupt(self): 19 | L.describe('Disable the primary network service') 20 | self.primary_service.disable() 21 | 22 | def restore(self): 23 | L.describe('Re-enable the primary network service') 24 | self.primary_service.enable() 25 | 26 | def teardown(self): 27 | if self.primary_service: 28 | self.primary_service.enable() 29 | 30 | super().teardown() 31 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/ip_tool/dyndns.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | import re 3 | 4 | from xv_leak_tools.log import L 5 | 6 | class DynDNS: 7 | 8 | PROG_IPV4 = re.compile(r'.*Current IP Address: ([0-9\.]+).*') 9 | PROG_IPV6 = re.compile(r'.*Current IP Address: ([0-9a-f\:]+).*') 10 | 11 | def __init__(self, url_getter): 12 | self._url_getter = url_getter 13 | 14 | def _get_public_ip_addresses(self, url, prog): 15 | stdout = self._url_getter(url)[0] 16 | 17 | matches = prog.match(stdout) 18 | if not matches: 19 | L.warning("Couldn't determine public IP address using {}. Got response\n{}".format( 20 | url, stdout)) 21 | return [] 22 | 23 | return [ipaddress.ip_address(matches.group(1))] 24 | 25 | def public_ipv4_addresses(self): 26 | return self._get_public_ip_addresses('http://checkip.dyndns.org/', DynDNS.PROG_IPV4) 27 | 28 | def public_ipv6_addresses(self): 29 | return self._get_public_ip_addresses('http://checkipv6.dyndns.org/', DynDNS.PROG_IPV6) 30 | 31 | def all_public_ip_addresses(self): 32 | return self.public_ipv4_addresses() + self.public_ipv6_addresses() 33 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/create_device.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.exception import XVEx 2 | from xv_leak_tools.test_device.android_device import AndroidDevice 3 | from xv_leak_tools.test_device.ios_device import IOSDevice 4 | from xv_leak_tools.test_device.linux_device import LinuxDevice 5 | from xv_leak_tools.test_device.macos_device import MacOSDevice 6 | from xv_leak_tools.test_device.router_device import RouterDevice 7 | from xv_leak_tools.test_device.windows_device import WindowsDevice 8 | 9 | # TODO: This is very rudimentary but until we need something more it's fine 10 | def create_device(os_name, config, connector): 11 | if os_name == 'android': 12 | return AndroidDevice(config, connector) 13 | elif os_name == 'ios': 14 | return IOSDevice(config, connector) 15 | elif os_name == 'windows': 16 | return WindowsDevice(config, connector) 17 | elif os_name == 'linux': 18 | return LinuxDevice(config, connector) 19 | elif os_name == 'router': 20 | return RouterDevice(config, connector) 21 | elif os_name == 'macos': 22 | return MacOSDevice(config, connector) 23 | raise XVEx("Don't know how to create device for OS {}".format(os_name)) 24 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/firewall/firewall_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.factory import Builder 2 | 3 | from xv_leak_tools.log import L 4 | from xv_leak_tools.test_components.component import ComponentNotSupported 5 | from xv_leak_tools.test_components.firewall.linux.linux_firewall import LinuxFirewall 6 | from xv_leak_tools.test_components.firewall.macos.macos_firewall import MacOSFirewall 7 | from xv_leak_tools.test_components.firewall.windows.windows_firewall import WindowsFirewall 8 | from xv_leak_tools.test_device.desktop_device import DesktopDevice 9 | 10 | class FirewallBuilder(Builder): 11 | 12 | @staticmethod 13 | def name(): 14 | return 'firewall' 15 | 16 | def build(self, device, config): 17 | if not isinstance(device, DesktopDevice): 18 | raise ComponentNotSupported( 19 | "Can't create firewall tool for : {}".format(device.os_name())) 20 | 21 | if device.os_name() == 'macos': 22 | return MacOSFirewall(device, config) 23 | elif device.os_name() == 'windows': 24 | return WindowsFirewall(device, config) 25 | elif device.os_name() == 'linux': 26 | return LinuxFirewall(device, config) 27 | -------------------------------------------------------------------------------- /xv_leak_tools/test_documentation/docstring_helpers.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # This was lifted directly from PIP-257: https://www.python.org/dev/peps/pep-0257/ 4 | def trim_docstring(docstring): 5 | '''Remove leading whitespace from docstrings to left align the text nicely.''' 6 | 7 | if not docstring: 8 | return '' 9 | # Convert tabs to spaces (following the normal Python rules) 10 | # and split into a list of lines: 11 | lines = docstring.expandtabs().splitlines() 12 | # Determine minimum indentation (first line doesn't count): 13 | indent = sys.maxsize 14 | for line in lines[1:]: 15 | stripped = line.lstrip() 16 | if stripped: 17 | indent = min(indent, len(line) - len(stripped)) 18 | # Remove indentation (first line is special): 19 | trimmed = [lines[0].strip()] 20 | if indent < sys.maxsize: 21 | for line in lines[1:]: 22 | trimmed.append(line[indent:].rstrip()) 23 | # Strip off trailing and leading blank lines: 24 | while trimmed and not trimmed[-1]: 25 | trimmed.pop() 26 | while trimmed and not trimmed[0]: 27 | trimmed.pop(0) 28 | # Return a single string: 29 | return '\n'.join(trimmed) 30 | -------------------------------------------------------------------------------- /desktop_local_tests/linux/linux_enable_new_service_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class LinuxEnableNewServiceDisrupter(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self._restrict_parameters(must_disrupt=True, must_restore=False) 9 | self._primary_service = None 10 | 11 | def setup(self): 12 | L.describe('Disable the primary network service') 13 | services = self._device['network_tool'].network_services_in_priority_order() 14 | self._primary_service = [service for service in services if service.active()][0] 15 | self._primary_service.disable() 16 | L.info("Disabled service {}".format(self._primary_service.name())) 17 | 18 | def disrupt(self): 19 | # Slightly confusing, but the disrupt-ion step here is actually enabling the service, not 20 | # disabling it. 21 | L.describe('Re-enable primary network service') 22 | self._primary_service.enable() 23 | 24 | def teardown(self): 25 | if self._primary_service: 26 | self._primary_service.enable() 27 | super().teardown() 28 | -------------------------------------------------------------------------------- /xv_leak_tools/test_documentation/test_docstring_reader.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | from xv_leak_tools.import_helpers import import_all_from_package, itersubclasses 4 | from xv_leak_tools.test_framework.test_case import TestCase 5 | from xv_leak_tools.test_documentation.docstring_helpers import trim_docstring 6 | 7 | class TestDocstringRead: 8 | 9 | # pylint: disable=too-few-public-methods 10 | 11 | def __init__(self, test_packages): 12 | self._test_packages = test_packages 13 | self._test_docs = None 14 | 15 | def _read_docs(self): 16 | self._test_docs = {} 17 | for package in self._test_packages: 18 | import_all_from_package(package, restrict_platform=False) 19 | 20 | for subclass in itersubclasses(TestCase): 21 | if not subclass.__name__.startswith("Test"): 22 | continue 23 | self._test_docs[subclass.__name__] = { 24 | 'file': inspect.getfile(subclass.__class__), 25 | 'doc': None if subclass.__doc__ is None else trim_docstring(subclass.__doc__), 26 | } 27 | 28 | def docs(self): 29 | if self._test_docs is None: 30 | self._read_docs() 31 | return self._test_docs 32 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/dns_tool/dns_tool_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.exception import XVEx 2 | from xv_leak_tools.factory import Builder 3 | from xv_leak_tools.test_components.dns_tool.macos.macos_dns_tool import MacOSDNSTool 4 | from xv_leak_tools.test_components.dns_tool.windows.windows_dns_tool import WindowsDNSTool 5 | from xv_leak_tools.test_components.dns_tool.linux.linux_dns_tool import LinuxDNSTool 6 | from xv_leak_tools.test_components.dns_tool.android.android_dns_tool import AndroidDNSTool 7 | 8 | class DNSToolBuilder(Builder): 9 | 10 | @staticmethod 11 | def name(): 12 | return 'dns_tool' 13 | 14 | def build(self, device, config): 15 | if device.os_name() == 'macos': 16 | return MacOSDNSTool(device, config) 17 | elif device.os_name() == 'windows': 18 | if device.is_cygwin(): 19 | return WindowsDNSTool(device, config) 20 | elif device.os_name() == 'linux': 21 | return LinuxDNSTool(device, config) 22 | elif device.os_name() == 'android': 23 | return AndroidDNSTool(device, config) 24 | else: 25 | raise XVEx("Don't know how to build 'dns_tool' component for OS {}".format( 26 | device.os_name())) 27 | -------------------------------------------------------------------------------- /multimachine_tests/test_wifi1_to_wifi2.py: -------------------------------------------------------------------------------- 1 | from multimachine_tests.multimachine_test_case import MultimachineTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestWifi1Wifi2(MultimachineTestCase): 6 | 7 | def test(self): 8 | L.describe('Open and connect the VPN application') 9 | self.target_device['vpn_application'].open_and_connect() 10 | 11 | L.describe('Capture traffic') 12 | self.capture_device['packet_capturer'].start() 13 | 14 | L.describe('Generate whatever traffic you want') 15 | message_and_await_enter('Are you done?') 16 | 17 | L.describe('Connect to a different WiFi network') 18 | # TODO: Automate this. 19 | message_and_await_enter('Have you connected to a new network?') 20 | 21 | L.describe('Generate whatever traffic you want') 22 | message_and_await_enter('Are you done?') 23 | 24 | L.describe('Stop capturing traffic') 25 | packets = self.capture_device['packet_capturer'].stop() 26 | 27 | whitelist = self.capture_device.local_ips() 28 | L.debug('Excluding {} from analysis'.format(whitelist)) 29 | self.traffic_analyser.get_vpn_server_ip(packets, whitelist) 30 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/macos_interface_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class MacOSInterfaceDisrupter(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self._restrict_parameters(must_disrupt=True) 9 | 10 | self.primary_service = self._find_primary_service() 11 | 12 | def _find_primary_service(self): 13 | services = self._device['network_tool'].network_services_in_priority_order() 14 | primary_service = [service for service in services if service.active()][0] 15 | L.info("Primary network service {} has interface {}".format( 16 | primary_service.name(), primary_service.interface())) 17 | return primary_service 18 | 19 | def disrupt(self): 20 | L.describe("Disable the primary network service's interface") 21 | self.primary_service.disable_interface() 22 | 23 | def restore(self): 24 | L.describe("Enable the primary network service's interface") 25 | self.primary_service.enable_interface() 26 | 27 | def teardown(self): 28 | if self.primary_service: 29 | self.primary_service.enable_interface() 30 | 31 | super().teardown() 32 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/windows_enable_new_adapter_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class WindowsEnableNewAdapterDisrupter(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self._restrict_parameters(must_disrupt=True, must_restore=False) 9 | self._primary_adapter = self._find_primary_adapter() 10 | 11 | def _find_primary_adapter(self): 12 | primary_adapter = self._device['network_tool'].primary_adapter() 13 | L.info("Primary network adapter is {}".format(primary_adapter.name())) 14 | return primary_adapter 15 | 16 | def setup(self): 17 | L.describe('Disable the primary network adapter') 18 | self._primary_adapter.disable() 19 | L.info("Disabled adapter {}".format(self._primary_adapter.name())) 20 | 21 | def disrupt(self): 22 | # Slightly confusing, but the disrupt-ion step here is actually enabling the adapter, not 23 | # disabling it. 24 | L.describe('Re-enable primary network adapter') 25 | self._primary_adapter.enable() 26 | 27 | def teardown(self): 28 | if self._primary_adapter: 29 | self.disrupt() 30 | super().teardown() 31 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_public_ip_disrupt_wifi_power.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.windows.windows_wifi_power_disrupter import WindowsWifiPowerDisrupter 3 | 4 | class TestWindowsPublicIPDisruptWifiPower(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the Wi-Fi power is 9 | disabled. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then disable the Wi-Fi power. The test then queries a webpage to 14 | detect it's public IP. 15 | 16 | Discussion: 17 | 18 | This test is somewhat of a stress test. A more realistic scenario with Wi-Fi is losing 19 | connectivity, e.g. by physically moving out of range of the Wi-Fi network's APs. 20 | 21 | Weaknesses: 22 | 23 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 24 | preferred over these tests. 25 | 26 | Scenarios: 27 | 28 | * Run test when Wi-Fi is the primary adapter 29 | * Run test when Wi-Fi is NOT the primary adapter 30 | ''' 31 | 32 | def __init__(self, devices, parameters): 33 | super().__init__(WindowsWifiPowerDisrupter, devices, parameters) 34 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_public_ip_disrupt_wifi_power.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.macos.macos_wifi_power_disrupter import MacOSWifiPowerDisrupter 3 | 4 | class TestMacOSPublicIPDisruptWifiPower(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the Wi-Fi power is 9 | disabled. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then disable the Wi-Fi power. The test then queries a webpage to 14 | detect it's public IP. 15 | 16 | Discussion: 17 | 18 | This test is somewhat of a stress test. A more realistic scenario with Wi-Fi is losing 19 | connectivity, e.g. by physically moving out of range of the Wi-Fi network's APs. 20 | 21 | Weaknesses: 22 | 23 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 24 | preferred over these tests. 25 | 26 | Scenarios: 27 | 28 | * Run test when Wi-Fi is the primary network service 29 | * Run test when Wi-Fi is NOT the primary network service 30 | ''' 31 | 32 | def __init__(self, devices, parameters): 33 | super().__init__(MacOSWifiPowerDisrupter, devices, parameters) 34 | -------------------------------------------------------------------------------- /desktop_local_tests/linux/linux_interface_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class LinuxInterfaceDisrupter(Disrupter): 5 | 6 | def __init__(self, devices, parameters): 7 | super().__init__(devices, parameters) 8 | self._restrict_parameters(must_disrupt=True) 9 | self.primary_service = self._find_primary_service() 10 | 11 | def _find_primary_service(self): 12 | services = self._device['network_tool'].network_services_in_priority_order() 13 | primary_service = [service for service in services if service.active()][0] 14 | primary_interface = primary_service.interface() 15 | L.info("Primary network service is {}, with interface {}".format( 16 | primary_service.id(), primary_interface)) 17 | return primary_service 18 | 19 | def disrupt(self): 20 | L.describe('Disable the primary interface (with ifconfig)') 21 | self.primary_service.disable_interface() 22 | 23 | def restore(self): 24 | L.describe('Reenable the primary interface (with ifconfig)') 25 | self.primary_service.enable_interface() 26 | 27 | def teardown(self): 28 | if self.primary_service: 29 | self.primary_service.enable_interface() 30 | 31 | super().teardown() 32 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/windows_dns_force_public_dns_servers_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class WindowsDNSForcePublicDNSServersDisrupter(Disrupter): 6 | 7 | def __init__(self, device, parameters): 8 | super().__init__(device, parameters) 9 | self._restrict_parameters(must_disrupt=True, must_restore=False, must_wait=False) 10 | self._primary_adapter = self._find_primary_adapter() 11 | self._has_disrupted = False 12 | 13 | def _find_primary_adapter(self): 14 | primary_adapter = self._device['network_tool'].primary_adapter() 15 | L.info("Primary network adapter is {}".format(primary_adapter.name())) 16 | return primary_adapter 17 | 18 | def disrupt(self): 19 | self._has_disrupted = True 20 | message_and_await_enter("Set the DNS servers for adapter {} ({}) to 8.8.8.8".format( 21 | self._primary_adapter.name(), self._primary_adapter.net_connection_id())) 22 | 23 | def teardown(self): 24 | if self._has_disrupted: 25 | message_and_await_enter("Reset the DNS servers for adapter {} ({})".format( 26 | self._primary_adapter.name(), self._primary_adapter.net_connection_id())) 27 | super().teardown() 28 | -------------------------------------------------------------------------------- /docs/setting_up_test_machines.md: -------------------------------------------------------------------------------- 1 | These docs details how to fully setup a Mac, Linux or Windows machine so that it can run the leak 2 | test tools. 3 | 4 | Note that mobile devices currently don't need this setup as they can't run the Python test suite. 5 | 6 | Test device setup can be simplified by using the provided ansible scripts. See TODO for how to use 7 | ansible for setup. The result of following the steps below or using ansible is the same. 8 | 9 | > TODO: ansible not fully supported yet (manual steps only for now) 10 | 11 | # A Note on Users 12 | 13 | We find it convenient to have a consistent user across all our test devices. The main benefit is 14 | that we always know what the expected user name is for any machine. 15 | 16 | We always name the default user `xv_leak_test`. This is the user which the test framework ssh-es 17 | into. The user has passwordless `sudo` in order to simplify privilege escalation for automatic 18 | tests. 19 | 20 | > On Windows we assume that we're always running as an administrator. Due to the differences in 21 | permission models between Windows and POSIX systems, this is just the simplest approach. 22 | 23 | It's not essential that you create such a user but it's recommended that you follow a similar 24 | approach. 25 | 26 | * [Linux](setting_up_linux.md) 27 | * [MacOS](setting_up_macos.md) 28 | * [Windows](setting_up_windows.md) 29 | -------------------------------------------------------------------------------- /multimachine_tests/test_wifi_off_wifi.py: -------------------------------------------------------------------------------- 1 | from multimachine_tests.multimachine_test_case import MultimachineTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestWifiOffWifi(MultimachineTestCase): 6 | 7 | def test(self): 8 | L.describe('Open and connect the VPN application') 9 | self.target_device['vpn_application'].open_and_connect() 10 | 11 | L.describe('Capture traffic') 12 | self.capture_device['packet_capturer'].start() 13 | 14 | L.describe('Generate whatever traffic you want') 15 | message_and_await_enter('Are you done?') 16 | 17 | L.describe('Disconnect WiFi') 18 | self.target_device['settings'].disable_wifi() 19 | 20 | message_and_await_enter('Wait until the application has noticed.') 21 | 22 | L.describe('Connect WiFi') 23 | self.target_device['settings'].enable_wifi() 24 | 25 | L.describe('Generate whatever traffic you want') 26 | message_and_await_enter('Are you done?') 27 | 28 | L.describe('Stop capturing traffic') 29 | packets = self.capture_device['packet_capturer'].stop() 30 | 31 | whitelist = self.capture_device.local_ips() 32 | L.debug('Excluding {} from analysis'.format(whitelist)) 33 | self.traffic_analyser.get_vpn_server_ip(packets, whitelist) 34 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/dns_tool/windows/windows_dns_tool.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | 3 | from xml.etree.ElementTree import fromstring 4 | 5 | from xv_leak_tools.log import L 6 | from xv_leak_tools.test_components.dns_tool.dns_lookup_tool import DNSLookupTool 7 | from xv_leak_tools.test_device.connector_helper import ConnectorHelper 8 | 9 | # N.B. This component *can* run remotely! 10 | class WindowsDNSTool(DNSLookupTool): 11 | 12 | def __init__(self, device, config): 13 | super().__init__(device, config) 14 | self._connector_helper = ConnectorHelper(self._device) 15 | 16 | def known_servers(self): 17 | output = self._connector_helper.check_command([ 18 | 'wmic.exe', 'nicconfig', 'where', '"IPEnabled = True"', 'get', 'DNSServerSearchOrder', 19 | '/format:rawxml' 20 | ])[0] 21 | 22 | L.verbose("Got raw wmic output: {}".format(output)) 23 | dns_servers = [] 24 | for nic in fromstring(output).findall("./RESULTS/CIM/INSTANCE"): 25 | for prop in nic: 26 | if prop.tag != 'PROPERTY.ARRAY': 27 | continue 28 | for val in prop.findall("./VALUE.ARRAY/VALUE"): 29 | ip = ipaddress.ip_address(val.text) 30 | dns_servers.append(ip) 31 | 32 | self._check_current_dns_server_is_known(dns_servers) 33 | return dns_servers 34 | -------------------------------------------------------------------------------- /default_context.py: -------------------------------------------------------------------------------- 1 | # pylint: skip-file 2 | 3 | CONTEXT = { 4 | 'output_directory': None, 5 | 'run_directory': None, 6 | 'log_level': 'INFO', 7 | 'allow_manual': True, 8 | 'stop_on_fail': False, 9 | 'running_in_ci': False, 10 | 'package_paths': [], 11 | 'test_packages': [ 12 | 'desktop_local_tests', 13 | 'generic_tests', 14 | 'multimachine_tests', 15 | 'xv_leak_tools.test_framework', 16 | ], 17 | 'component_packages': [ 18 | 'xv_leak_tools.test_components.cleanup', 19 | 'xv_leak_tools.test_components.dns_tool', 20 | 'xv_leak_tools.test_components.firewall', 21 | 'xv_leak_tools.test_components.git', 22 | 'xv_leak_tools.test_components.ip_responder', 23 | 'xv_leak_tools.test_components.ip_tool', 24 | 'xv_leak_tools.test_components.network_configuration', 25 | 'xv_leak_tools.test_components.network_tool', 26 | 'xv_leak_tools.test_components.open_wrt', 27 | 'xv_leak_tools.test_components.packet_capturer', 28 | 'xv_leak_tools.test_components.route', 29 | 'xv_leak_tools.test_components.settings', 30 | 'xv_leak_tools.test_components.vpn_application', 31 | 'xv_leak_tools.test_components.webdriver', 32 | 'xv_leak_tools.test_components.webserver', 33 | ], 34 | 'default_device_components': 35 | { 36 | 'cleanup': {}, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_ip_responder_disrupt_wifi_power.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case_with_disrupter import LocalIPResponderTestCaseWithDisrupter 2 | from desktop_local_tests.macos.macos_wifi_power_disrupter import MacOSWifiPowerDisrupter 3 | 4 | class TestMacOSIPResponderDisruptWifiPower(LocalIPResponderTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the Wi-Fi power is 9 | disabled. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then disable the Wi-Fi power. 14 | 15 | This test uses a simple UDP client which spams UDP packets to a public server. The server logs 16 | the source IP of every packet. The test checks with the server to make sure that the public IP 17 | is always the VPN server's IP and not the device's. 18 | 19 | Discussion: 20 | 21 | This test is somewhat of a stress test. A more realistic scenario with Wi-Fi is losing 22 | connectivity, e.g. by physically moving out of range of the Wi-Fi network's APs. 23 | 24 | Weaknesses: 25 | 26 | None 27 | 28 | Scenarios: 29 | 30 | * Run test when Wi-Fi is the primary network service 31 | * Run test when Wi-Fi is NOT the primary network service 32 | ''' 33 | 34 | def __init__(self, devices, parameters): 35 | super().__init__(MacOSWifiPowerDisrupter, devices, parameters) 36 | -------------------------------------------------------------------------------- /desktop_local_tests/test_public_ip_disrupt_kill_vpn_process.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.disrupter_kill_vpn_process import DisrupterKillVPNProcess 3 | 4 | class TestPublicIPDisruptKillVPNProcess(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether the device's public IP is exposed when the VPN process crashes. 9 | 10 | Details: 11 | 12 | This test will kill the underlying process responsible for providing the VPN service. For 13 | example, it will kill the openvpn binary when using OpenVPN. It does not kill any other support 14 | processes for the provider, e.g. UI app, daemons etc.. Once the process has been killed, the 15 | repeatedly checks the device's public IPv4 and IPv6 addresses by visiting a webpage designed to 16 | report those IPs. 17 | 18 | Discussion: 19 | 20 | The test is a stress test. Crashes should be rare but in the real world they can happen. A VPN 21 | should be resilient to such crashes. 22 | 23 | Weaknesses: 24 | 25 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 26 | preferred over these tests. 27 | 28 | Scenarios: 29 | 30 | No restrictions. 31 | ''' 32 | 33 | def __init__(self, devices, parameters): 34 | super().__init__(DisrupterKillVPNProcess, devices, parameters) 35 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_ip_responder_disrupt_wifi_power.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case_with_disrupter import LocalIPResponderTestCaseWithDisrupter 2 | from desktop_local_tests.windows.windows_wifi_power_disrupter import WindowsWifiPowerDisrupter 3 | 4 | class TestWindowsIPResponderDisruptWifiPower(LocalIPResponderTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the Wi-Fi power is 9 | disabled. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then disable the Wi-Fi power. 14 | 15 | This test uses a simple UDP client which spams UDP packets to a public server. The server logs 16 | the source IP of every packet. The test checks with the server to make sure that the public IP 17 | is always the VPN server's IP and not the device's. 18 | 19 | Discussion: 20 | 21 | This test is somewhat of a stress test. A more realistic scenario with Wi-Fi is losing 22 | connectivity, e.g. by physically moving out of range of the Wi-Fi network's APs. 23 | 24 | Weaknesses: 25 | 26 | None 27 | 28 | Scenarios: 29 | 30 | * Run test when Wi-Fi is the primary adapter 31 | * Run test when Wi-Fi is NOT the primary adapter 32 | ''' 33 | 34 | def __init__(self, devices, parameters): 35 | super().__init__(WindowsWifiPowerDisrupter, devices, parameters) 36 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/windows_local_shell_connector.py: -------------------------------------------------------------------------------- 1 | # from xv_leak_tools.exception import XVEx 2 | # from xv_leak_tools.log import L 3 | from xv_leak_tools.process import execute_subprocess 4 | from xv_leak_tools.test_device.local_shell_connector import LocalShellConnector 5 | 6 | class WindowsLocalShellConnector(LocalShellConnector): 7 | 8 | def execute(self, cmd, root=False): 9 | # TODO: Don't have the ability to change to/from root on Windows. Any clean way of doing 10 | # this? 11 | return execute_subprocess(cmd) 12 | 13 | # def push(self, src, dst): 14 | # ret, stdout, stderr = execute_subprocess(['cp', '-rf', src, dst]) 15 | 16 | # if ret != 0: 17 | # raise XVEx("Couldn't copy {} -> {}: stdout: {}, stderr: {}".format( 18 | # src, dst, stdout, stderr)) 19 | 20 | # if os.geteuid() != 0: 21 | # return 22 | 23 | # # When executing locally it's entirely likely we'll be running as root. We don't want to 24 | # # have any files owned by root unless directly specified by the user - in which case they 25 | # # should chown via a command for now (which I think is an unlikely use case). 26 | 27 | # L.verbose("Removing root permissions from file {} ({})".format(dst, tools_user()[0])) 28 | # os.chown(dst, tools_user()[0], -1) 29 | 30 | # def pull(self, src, dst): 31 | # self.push(src, dst) 32 | -------------------------------------------------------------------------------- /desktop_local_tests/test_packet_capture_vanilla.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case import LocalPacketCaptureTestCase 2 | 3 | class TestPacketCaptureVanilla(LocalPacketCaptureTestCase): 4 | 5 | '''Summary: 6 | 7 | Test whether traffic leaks outside of the VPN tunnel during regular operation of the VPN. 8 | 9 | Details: 10 | 11 | This test connects to the VPN then starts packet capture to monitor all outgoing traffic from 12 | the device. It then checks to see if any traffic leaked outside of the VPN. The test is 13 | automatic and just runs for a fixed period of time specified by the 'check_period' parameter. 14 | 15 | Discussion: 16 | 17 | This test has no implementation as the base class handles everything. The only thing that needs 18 | to be done is specify the 'check_period' parameter in the test config. This determines how long 19 | the test will check the IP for. 20 | 21 | This is a vanilla test and makes no attempt to disrupt the VPN. 22 | 23 | Weaknesses: 24 | 25 | Packet capture tests can be noisy. Traffic can be detected as a leak but in actual fact may not 26 | be. For example, traffic might go to a server owned by the VPN provider to re-establish 27 | connections. In general this test is best used for manual exploring leaks rather than for 28 | automation. 29 | 30 | Scenarios: 31 | 32 | No restrictions. 33 | 34 | ''' 35 | 36 | pass 37 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/ios_device.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.test_device.mobile_device import MobileDevice 3 | from xv_leak_tools.test_device.shell_connector_helper import ShellConnectorHelper 4 | 5 | class IOSDevice(MobileDevice): 6 | 7 | def __init__(self, config, connector): 8 | super().__init__(config, connector) 9 | self._connector_helper = ShellConnectorHelper(self) 10 | 11 | @staticmethod 12 | def os_name(): 13 | return 'ios' 14 | 15 | def os_version(self): 16 | L.warning("iOS version detection not implemented") 17 | return 'TODO: iOS version' 18 | 19 | def open_app(self, bundle, activity, root=False): 20 | pass 21 | 22 | def close_app(self, bundle, root=False): 23 | pass 24 | 25 | def run_cmd(self, cmd, root=False): 26 | return self._connector_helper.check_command(cmd, root) 27 | 28 | def wakeup(self): 29 | L.debug("Waking device up") 30 | 31 | def sleep(self): 32 | L.debug("Go back to sleep") 33 | 34 | def close_tray(self): 35 | pass 36 | 37 | def kill_process(self, pid): 38 | L.debug("Killing process {}".format(pid)) 39 | L.warning("Not implemented!") 40 | 41 | def pgrep(self, process_name): 42 | L.debug("pgrep-ing for {}".format(process_name)) 43 | L.warning("Not implemented!") 44 | 45 | def focused_activity(self): 46 | pass 47 | -------------------------------------------------------------------------------- /desktop_local_tests/test_dns_disrupt_kill_vpn_process.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.dns_during_disruption import DNSDuringDisruptionTestCase 2 | from desktop_local_tests.disrupter_kill_vpn_process import DisrupterKillVPNProcess 3 | 4 | class TestDNSDisruptKillVPNProcess(DNSDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether DNS leaks when the VPN process crashes. 9 | 10 | Details: 11 | 12 | This test will kill the underlying process responsible for providing the VPN service. For 13 | example, it will kill the openvpn binary when using OpenVPN. It does not kill any other support 14 | processes for the provider, e.g. UI app, daemons etc.. 15 | 16 | Discussion: 17 | 18 | The test is a stress test. Crashes should be rare but in the real world they can happen. A VPN 19 | should be resilient to such crashes. 20 | 21 | Weaknesses: 22 | 23 | Currently uses dig to decide if DNS leaks. This isn't reliable for some VPN providers. Some 24 | providers intercept DNS upstream and change the destination DNS server to their own server. 25 | However dig will still report the server which it originally sent the request to. 26 | 27 | Scenarios: 28 | 29 | No restrictions. 30 | 31 | TODO: 32 | 33 | Consider tests which kill other/all processes related to a VPN. 34 | ''' 35 | 36 | def __init__(self, devices, parameters): 37 | super().__init__(DisrupterKillVPNProcess, devices, parameters) 38 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_dns_disrupt_wifi_power.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.dns_during_disruption import DNSDuringDisruptionTestCase 2 | from desktop_local_tests.macos.macos_wifi_power_disrupter import MacOSWifiPowerDisrupter 3 | 4 | class TestMacOSDNSDisruptWifiPower(DNSDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether DNS leaks when the Wi-Fi power is disabled. 9 | 10 | Details: 11 | 12 | This test will connect to VPN then disable the Wi-Fi power. Once the power is disabled the 13 | test repeatedly makes DNS requests and checks whether the DNS request went to a non VPN DNS 14 | server 15 | 16 | Discussion: 17 | 18 | This test is somewhat of a stress test. A more realistic scenario with Wi-Fi is losing 19 | connectivity, e.g. by physically moving out of range of the Wi-Fi network's APs. 20 | 21 | Weaknesses: 22 | 23 | Currently uses dig to decide if DNS leaks. This isn't reliable for some VPN providers. Some 24 | providers intercept DNS upstream and change the destination DNS server to their own server. 25 | However dig will still report the server which it originally sent the request to. 26 | 27 | Scenarios: 28 | 29 | * Run test when Wi-Fi is the primary network service 30 | * Run test when Wi-Fi is NOT the primary network service 31 | ''' 32 | 33 | def __init__(self, devices, parameters): 34 | super().__init__(MacOSWifiPowerDisrupter, devices, parameters) 35 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_dns_disrupt_wifi_power.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.dns_during_disruption import DNSDuringDisruptionTestCase 2 | from desktop_local_tests.windows.windows_wifi_power_disrupter import WindowsWifiPowerDisrupter 3 | 4 | class TestWindowsDNSDisruptWifiPower(DNSDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether DNS leaks when the Wi-Fi power is disabled. 9 | 10 | Details: 11 | 12 | This test will connect to VPN then disable the Wi-Fi power. Once the power is disabled the 13 | test repeatedly makes DNS requests and checks whether the DNS request went to a non VPN DNS 14 | server 15 | 16 | Discussion: 17 | 18 | This test is somewhat of a stress test. A more realistic scenario with Wi-Fi is losing 19 | connectivity, e.g. by physically moving out of range of the Wi-Fi network's APs. 20 | 21 | Weaknesses: 22 | 23 | Currently uses dig to decide if DNS leaks. This isn't reliable for some VPN providers. Some 24 | providers intercept DNS upstream and change the destination DNS server to their own server. 25 | However dig will still report the server which it originally sent the request to. 26 | 27 | Scenarios: 28 | 29 | * Run test when Wi-Fi is the primary adapter 30 | * Run test when Wi-Fi is NOT the primary adapter 31 | ''' 32 | 33 | def __init__(self, devices, parameters): 34 | super().__init__(WindowsWifiPowerDisrupter, devices, parameters) 35 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_public_ip_disrupt_reorder_services.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.macos.macos_reorder_services_disrupter import MacOSDNSReorderServicesDisrupter 3 | 4 | class TestMacOSPublicIPDisruptReorderServices(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the network 9 | service order is changed. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then swap the priority of the primary and secondary network 14 | services. The test then queries a webpage to detect it's public IP. 15 | 16 | Discussion: 17 | 18 | It's not 100% clear if, in the real world, services can change their order without user 19 | involvement. It is still however a good stress test of the application. 20 | 21 | Weaknesses: 22 | 23 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 24 | preferred over these tests. 25 | 26 | Scenarios: 27 | 28 | Requires two active network services. 29 | 30 | TODO: 31 | 32 | Consider a variant which changes the network "Location". This is much more likely to be 33 | something a user might do. 34 | ''' 35 | 36 | def __init__(self, devices, parameters): 37 | super().__init__(MacOSDNSReorderServicesDisrupter, devices, parameters) 38 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/device_discovery.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_device.device_discoverers.localhost_discoverer import LocalhostDiscoverer 2 | from xv_leak_tools.test_device.device_discoverers.static_discoverer import StaticDeviceDiscoverer 3 | from xv_leak_tools.test_device.device_discoverers.vmware_discoverer import VMWareDeviceDiscoverer 4 | from xv_leak_tools.exception import XVEx 5 | 6 | class DeviceDiscovery: 7 | 8 | def __init__(self, context, inventory): 9 | # No need for anything clever here yet. We only have 3 ways of discovering devices 10 | # currently. Let's just add the discoverers manually. 11 | self._discoverers = [] 12 | self._discoverers.append(LocalhostDiscoverer(context, inventory)) 13 | self._discoverers.append(StaticDeviceDiscoverer(context, inventory)) 14 | self._discoverers.append(VMWareDeviceDiscoverer(context, inventory)) 15 | 16 | def discover_device(self, discovery_keys): 17 | for discoverer in self._discoverers: 18 | device = discoverer.discover_device(discovery_keys) 19 | if device is not None: 20 | return device 21 | raise XVEx("Couldn't discover device using keys: {}".format(discovery_keys)) 22 | 23 | def release_devices(self): 24 | for discoverer in self._discoverers: 25 | discoverer.release_devices() 26 | 27 | def cleanup(self): 28 | for discoverer in self._discoverers: 29 | discoverer.cleanup() 30 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/settings/settings.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.exception import XVEx 2 | from xv_leak_tools.manual_input import message_and_await_enter 3 | from xv_leak_tools.test_components.component import Component 4 | 5 | class Settings(Component): 6 | 7 | # pylint: disable=no-self-use 8 | 9 | def enable_wifi(self): 10 | message_and_await_enter('Enable WiFi') 11 | 12 | def disable_wifi(self): 13 | message_and_await_enter('Disable WiFi') 14 | 15 | def toggle_wifi(self): 16 | raise XVEx('Toggling settings is not implemented') 17 | 18 | def enable_mobile_data(self): 19 | message_and_await_enter('Enable data') 20 | 21 | def disable_mobile_data(self): 22 | message_and_await_enter('Disable data') 23 | 24 | def toggle_mobile_data(self): 25 | raise XVEx('Toggling settings is not implemented') 26 | 27 | def enable_airplane_mode(self): 28 | message_and_await_enter('Enable airplane mode') 29 | 30 | def disable_airplane_mode(self): 31 | message_and_await_enter('Disable airplane mode') 32 | 33 | def toggle_airplane_mode(self): 34 | raise XVEx('Toggling settings is not implemented') 35 | 36 | def enable_wired(self): 37 | message_and_await_enter('Plug in wired internet') 38 | 39 | def disable_wired(self): 40 | message_and_await_enter('Plug out wired internet') 41 | 42 | def toggle_wired(self): 43 | raise XVEx('Toggling settings is not implemented') 44 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/cleanup/cleanup_builder.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.exception import XVEx 2 | from xv_leak_tools.factory import Builder 3 | from xv_leak_tools.test_components.cleanup.macos.macos_cleanup import MacOSCleanup 4 | from xv_leak_tools.test_components.cleanup.android.android_cleanup import AndroidCleanup 5 | from xv_leak_tools.test_components.cleanup.windows.windows_cleanup import WindowsCleanup 6 | from xv_leak_tools.test_components.cleanup.ios.ios_cleanup import IOSCleanup 7 | from xv_leak_tools.test_components.cleanup.linux.linux_cleanup import LinuxCleanup 8 | 9 | class CleanupBuilder(Builder): 10 | 11 | @staticmethod 12 | def name(): 13 | return 'cleanup' 14 | 15 | def build(self, device, config): 16 | if device.os_name() == 'macos': 17 | return MacOSCleanup(device, config) 18 | elif device.os_name() == 'windows': 19 | return WindowsCleanup(device, config) 20 | elif device.os_name() == 'linux': 21 | return LinuxCleanup(device, config) 22 | elif device.os_name() == 'android': 23 | return AndroidCleanup(device, config) 24 | elif device.os_name() == 'ios': 25 | return IOSCleanup(device, config) 26 | else: 27 | # Don't raise ComponentNotSupported as, if we get here, this implies something was 28 | # overlooked. 29 | raise XVEx("Don't know how to build 'cleanup' component for OS {}".format( 30 | device.os_name())) 31 | -------------------------------------------------------------------------------- /multimachine_tests/test_wifi_off_upstream_wifi.py: -------------------------------------------------------------------------------- 1 | from multimachine_tests.multimachine_test_case import MultimachineTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestWifiOffUpstreamWifi(MultimachineTestCase): 6 | 7 | def test(self): 8 | L.describe('Open and connect the VPN application') 9 | self.target_device['vpn_application'].open_and_connect() 10 | 11 | L.describe('Capture traffic') 12 | self.capture_device['packet_capturer'].start() 13 | 14 | L.describe('Generate whatever traffic you want') 15 | message_and_await_enter('Are you done?') 16 | 17 | L.describe('Disconnect WiFi upstream') 18 | # TODO: self.upstream.disable()? 19 | message_and_await_enter('Unplug the cable from the router') 20 | 21 | message_and_await_enter('Wait until the application has noticed (or however long you want)') 22 | 23 | L.describe('Connect WiFi upstream') 24 | message_and_await_enter('Plug the cable back in') 25 | 26 | L.describe('Generate whatever traffic you want') 27 | message_and_await_enter('Are you done?') 28 | 29 | L.describe('Stop capturing traffic') 30 | packets = self.capture_device['packet_capturer'].stop() 31 | 32 | whitelist = self.capture_device.local_ips() 33 | L.debug('Excluding {} from analysis'.format(whitelist)) 34 | self.traffic_analyser.get_vpn_server_ip(packets, whitelist) 35 | -------------------------------------------------------------------------------- /tools/evaluate_test_template.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # TODO: Remove this once the sys.path.append is gone 4 | # pylint: disable=wrong-import-position 5 | 6 | import argparse 7 | import json 8 | import os 9 | import sys 10 | 11 | sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..')) 12 | 13 | from xv_leak_tools.helpers import exception_to_string 14 | from xv_leak_tools.object_parser import object_from_command_line 15 | 16 | def main(argv=None): 17 | if argv is None: 18 | argv = sys.argv[1:] 19 | 20 | # Some of these command line args are also specified in the context. This is just for 21 | # convenience as certain of the arguments are changed often enough to warrant specifying them 22 | # on the command line. 23 | parser = argparse.ArgumentParser( 24 | description='') 25 | parser.add_argument( 26 | 'template', help='') 27 | args = parser.parse_args(argv) 28 | 29 | test_configs = object_from_command_line(args.template, 'TESTS') 30 | print(len(test_configs)) 31 | output_file = 'tests.json' 32 | with open(output_file, 'w') as _file: 33 | json.dump(test_configs, _file, indent=4) 34 | print("Wrote json file: {}".format(output_file)) 35 | 36 | if __name__ == "__main__": 37 | # pylint: disable=broad-except 38 | try: 39 | sys.exit(main()) 40 | except Exception as ex: 41 | sys.stderr.write("Unrecoverable error: {}\n".format(exception_to_string(ex))) 42 | sys.exit(-1) 43 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/macos_reorder_services_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.exception import XVEx 3 | from xv_leak_tools.log import L 4 | 5 | class MacOSDNSReorderServicesDisrupter(Disrupter): 6 | 7 | def __init__(self, device, parameters): 8 | super().__init__(device, parameters) 9 | self._restrict_parameters(must_disrupt=True, must_restore=False) 10 | 11 | def _swap_highest_priority_services(self): 12 | def active_service_indices(services): 13 | for service in services: 14 | if service.active(): 15 | yield services.index(service) 16 | 17 | services = self._device['network_tool'].network_services_in_priority_order() 18 | try: 19 | active_service_index = active_service_indices(services) 20 | i = next(active_service_index) 21 | j = next(active_service_index) 22 | L.debug('Swapping {}, {}'.format(services[i], services[j])) 23 | services[i], services[j] = services[j], services[i] 24 | except StopIteration: 25 | raise XVEx('There must be at least two active services') 26 | 27 | self._device['network_tool'].set_network_service_order(services) 28 | 29 | def disrupt(self): 30 | L.describe('Swap the two highest-priority active network services') 31 | self._swap_highest_priority_services() 32 | 33 | def teardown(self): 34 | self._swap_highest_priority_services() 35 | super().teardown() 36 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_packet_capture_disrupt_wifi_power.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter import LocalPacketCaptureTestCaseWithDisrupter 2 | from desktop_local_tests.windows.windows_wifi_power_disrupter import WindowsWifiPowerDisrupter 3 | 4 | class TestWindowsPacketCaptureDisruptWifiPower(LocalPacketCaptureTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device leaks outside of the VPN tunnel when the Wi-Fi 9 | power is disabled. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then disable the Wi-Fi power. The test looks for leaking traffic 14 | once the interface has been disabled. 15 | 16 | Discussion: 17 | 18 | This test is somewhat of a stress test. A more realistic scenario with Wi-Fi is losing 19 | connectivity, e.g. by physically moving out of range of the Wi-Fi network's APs. 20 | 21 | Weaknesses: 22 | 23 | Packet capture tests can be noisy. Traffic can be detected as a leak but in actual fact may not 24 | be. For example, traffic might go to a server owned by the VPN provider to re-establish 25 | connections. In general this test is best used for manual exploring leaks rather than for 26 | automation. 27 | 28 | Scenarios: 29 | 30 | * Run test when Wi-Fi is the primary adapter 31 | * Run test when Wi-Fi is NOT the primary adapter 32 | ''' 33 | 34 | def __init__(self, devices, parameters): 35 | super().__init__(WindowsWifiPowerDisrupter, devices, parameters) 36 | -------------------------------------------------------------------------------- /desktop_local_tests/test_packet_capture_disrupt_kill_vpn_process.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter import LocalPacketCaptureTestCaseWithDisrupter 2 | from desktop_local_tests.disrupter_kill_vpn_process import DisrupterKillVPNProcess 3 | 4 | class TestPacketCaptureDisruptKillVPNProcess(LocalPacketCaptureTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Test whether traffic leaks when the VPN process crashes. 9 | 10 | Details: 11 | 12 | This test will kill the underlying process responsible for providing the VPN service. For 13 | example, it will kill the openvpn binary when using OpenVPN. It does not kill any other support 14 | processes for the provider, e.g. UI app, daemons etc.. 15 | 16 | Discussion: 17 | 18 | The test is a stress test. Crashes should be rare but in the real world they can happen. A VPN 19 | should be resilient to such crashes. 20 | 21 | Weaknesses: 22 | 23 | Packet capture tests can be noisy. Traffic can be detected as a leak but in actual fact may not 24 | be. For example, traffic might go to a server owned by the VPN provider to re-establish 25 | connections. In general this test is best used for manual exploring leaks rather than for 26 | automation. 27 | 28 | Scenarios: 29 | 30 | No restrictions. 31 | 32 | TODO: 33 | 34 | Consider tests which kill other/all processes related to a VPN. 35 | ''' 36 | 37 | def __init__(self, devices, parameters): 38 | super().__init__(DisrupterKillVPNProcess, devices, parameters) 39 | -------------------------------------------------------------------------------- /desktop_local_tests/test_ip_responder_disrupt_kill_vpn_process.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case_with_disrupter import LocalIPResponderTestCaseWithDisrupter 2 | from desktop_local_tests.disrupter_kill_vpn_process import DisrupterKillVPNProcess 3 | 4 | class TestIPResponderDisruptKillVPNProcess(LocalIPResponderTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the VPN process 9 | crashes. 10 | 11 | Details: 12 | 13 | This test will kill the underlying process responsible for providing the VPN service. For 14 | example, it will kill the openvpn binary when using OpenVPN. It does not kill any other support 15 | processes for the provider, e.g. UI app, daemons etc.. 16 | 17 | This test uses a simple UDP client which spams UDP packets to a public server. The server logs 18 | the source IP of every packet. The test checks with the server to make sure that the public IP 19 | is always the VPN server's IP and not the device's. 20 | 21 | Discussion: 22 | 23 | The test is a stress test. Crashes should be rare but in the real world they can happen. A VPN 24 | should be resilient to such crashes. 25 | 26 | Weaknesses: 27 | 28 | None 29 | 30 | Scenarios: 31 | 32 | No restrictions. 33 | 34 | TODO: 35 | 36 | Consider tests which kill other/all processes related to a VPN. 37 | 38 | ''' 39 | 40 | def __init__(self, devices, parameters): 41 | super().__init__(DisrupterKillVPNProcess, devices, parameters) 42 | -------------------------------------------------------------------------------- /xv_leak_tools/object_parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from xv_leak_tools.exception import XVEx 4 | from xv_leak_tools.helpers import exception_to_string 5 | from xv_leak_tools.import_helpers import import_by_filename 6 | from xv_leak_tools.log import L 7 | 8 | def object_from_json_string(json_string, _): 9 | return json.loads(json_string) 10 | 11 | def object_from_json_file(filename, _): 12 | with open(filename) as file_: 13 | return json.loads(file_.read()) 14 | 15 | def object_from_python_module(filename, attribute_name): 16 | module = import_by_filename(filename) 17 | if not hasattr(module, attribute_name): 18 | raise XVEx("'{}' should have an attribute called '{}'".format( 19 | filename, attribute_name)) 20 | return getattr(module, attribute_name) 21 | 22 | def object_from_command_line(source, attribute_name): 23 | L.debug("Trying to convert '{}' to an object (attribute_name={})".format( 24 | source, attribute_name)) 25 | 26 | # Order here is the "most likely" order we'll get objects 27 | funcs = [ 28 | object_from_python_module, 29 | object_from_json_file, 30 | object_from_json_string, 31 | ] 32 | 33 | all_errors = "" 34 | for func in funcs: 35 | try: 36 | return func(source, attribute_name) 37 | except Exception as ex: # pylint: disable=broad-except 38 | all_errors += "{} failed due to: {}\n".format(func.__name__, exception_to_string(ex)) 39 | 40 | raise XVEx("Couldn't convert '{}' to a valid object:\n{}".format(source, all_errors)) 41 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/local_shell_connector.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from xv_leak_tools import tools_user 4 | from xv_leak_tools.exception import XVEx 5 | from xv_leak_tools.log import L 6 | from xv_leak_tools.process import execute_subprocess 7 | from xv_leak_tools.test_device.connector import Connector 8 | 9 | class LocalShellConnector(Connector): 10 | 11 | def execute(self, cmd, root=False): 12 | if not root and os.geteuid() == 0: 13 | return execute_subprocess(['sudo', '-u', tools_user()[1]] + cmd) 14 | 15 | # Make it clear we're running as root. We could check os.geteuid() != 0 though. 16 | if root: 17 | cmd = ['sudo', '-n'] + cmd 18 | 19 | return execute_subprocess(cmd) 20 | 21 | def push(self, src, dst): 22 | ret, stdout, stderr = execute_subprocess(['cp', '-rf', src, dst]) 23 | 24 | if ret != 0: 25 | raise XVEx("Couldn't copy {} -> {}: stdout: {}, stderr: {}".format( 26 | src, dst, stdout, stderr)) 27 | 28 | if os.geteuid() != 0: 29 | return 30 | 31 | # When executing locally it's entirely likely we'll be running as root. We don't want to 32 | # have any files owned by root unless directly specified by the user - in which case they 33 | # should chown via a command for now (which I think is an unlikely use case). 34 | 35 | L.verbose("Removing root permissions from file {} ({})".format(dst, tools_user()[0])) 36 | os.chown(dst, tools_user()[0], -1) 37 | 38 | def pull(self, src, dst): 39 | self.push(src, dst) 40 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_ip_responder_disrupt_reorder_services.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case_with_disrupter import LocalIPResponderTestCaseWithDisrupter 2 | from desktop_local_tests.macos.macos_reorder_services_disrupter import MacOSDNSReorderServicesDisrupter 3 | 4 | class TestMacOSIPResponderDisruptReorderServices(LocalIPResponderTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the network 9 | service order is changed. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then swap the priority of the primary and secondary network 14 | services. 15 | 16 | This test uses a simple UDP client which spams UDP packets to a public server. The server logs 17 | the source IP of every packet. The test checks with the server to make sure that the public IP 18 | is always the VPN server's IP and not the device's. 19 | 20 | Discussion: 21 | 22 | It's not 100% clear if, in the real world, services can change their order without user 23 | involvement. It is still however a good stress test of the application. 24 | 25 | Weaknesses: 26 | 27 | None 28 | 29 | Scenarios: 30 | 31 | Requires two active network services. 32 | 33 | TODO: 34 | 35 | Consider a variant which changes the network "Location". This is much more likely to be 36 | something a user might do. 37 | ''' 38 | 39 | def __init__(self, devices, parameters): 40 | super().__init__(MacOSDNSReorderServicesDisrupter, devices, parameters) 41 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_dns_disrupt_reorder_services.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.dns_during_disruption import DNSDuringDisruptionTestCase 2 | from desktop_local_tests.macos.macos_reorder_services_disrupter import MacOSDNSReorderServicesDisrupter 3 | 4 | class TestMacOSDNSDisruptReorderServices(DNSDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether DNS leaks when the network service order is changed. 9 | 10 | Details: 11 | 12 | This test will connect to VPN then swap the priority of the primary and secondary network 13 | services. Once the order is changed the test repeatedly makes DNS requests and checks whether 14 | the DNS request went to a non VPN DNS server 15 | 16 | Discussion: 17 | 18 | It's not 100% clear if, in the real world, services can change their order without user 19 | involvement. It is still however a good stress test of the application. 20 | 21 | Weaknesses: 22 | 23 | Currently uses dig to decide if DNS leaks. This isn't reliable for some VPN providers. Some 24 | providers intercept DNS upstream and change the destination DNS server to their own server. 25 | However dig will still report the server which it originally sent the request to. 26 | 27 | Scenarios: 28 | 29 | Requires two active network services. 30 | 31 | TODO: 32 | 33 | Consider a variant which changes the network "Location". This is much more likely to be 34 | something a user might do. 35 | ''' 36 | 37 | def __init__(self, devices, parameters): 38 | super().__init__(MacOSDNSReorderServicesDisrupter, devices, parameters) 39 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/vpn_application/macos/macos_vpn_application.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.log import L 2 | from xv_leak_tools.test_components.vpn_application.desktop_vpn_application import DesktopVPNApplication 3 | 4 | class MacOSVPNApplication(DesktopVPNApplication): 5 | 6 | def __init__(self, app_path, device, config): 7 | super().__init__(app_path, device, config) 8 | self._dns_servers_before_connect = device['dns_tool'].known_servers() 9 | 10 | def dns_server_ips(self): 11 | info = self._vpn_info() 12 | if info is not None and info.dns_server_ips: 13 | return info.dns_server_ips 14 | 15 | if not self._config.get('strict', False): 16 | dns_servers_after_connect = set(self._device['dns_tool'].known_servers()) 17 | L.debug( 18 | "Inferring VPN DNS servers. DNS before connect: {}, DNS after connect: {}".format( 19 | self._dns_servers_before_connect, dns_servers_after_connect)) 20 | 21 | for server in self._dns_servers_before_connect: 22 | dns_servers_after_connect.discard(server) 23 | 24 | if dns_servers_after_connect: 25 | L.warning("Inferring VPN DNS server IPs from System Configuration. " 26 | "This is likely correct, but can be prevented by specifying " 27 | "the 'strict' keyword in the VPN configuration.") 28 | return list(dns_servers_after_connect) 29 | L.warning("Couldn't find DNS servers by inspecting system.") 30 | 31 | return super().dns_server_ips() 32 | -------------------------------------------------------------------------------- /xv_leak_tools/network/common.py: -------------------------------------------------------------------------------- 1 | import ipaddress 2 | import re 3 | 4 | from xv_leak_tools.exception import XVEx 5 | 6 | RE_IPV4_ADDRESS = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}' 7 | PROG_MAC_ADDRESS_COLON = re.compile( 8 | r"[0-9a-f]{1,2}:[0-9a-f]{1,2}:[0-9a-f]{1,2}:[0-9a-f]{1,2}:[0-9a-f]{1,2}:[0-9a-f]{1,2}") 9 | PROG_MAC_ADDRESS_DOT = re.compile( 10 | r"[0-9a-f]{1,2}.[0-9a-f]{1,2}.[0-9a-f]{1,2}.[0-9a-f]{1,2}.[0-9a-f]{1,2}.[0-9a-f]{1,2}") 11 | 12 | def ips_to_ip_addresses(ips): 13 | if not isinstance(ips, list): 14 | return ipaddress.ip_address(ips) 15 | 16 | ret = [] 17 | for ip in ips: 18 | if isinstance(ip, str): 19 | ret.append(ipaddress.ip_address(ip)) 20 | elif isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)): 21 | ret.append(ip) 22 | else: 23 | raise XVEx("Can't convert {} to an ip address object".format(ip)) 24 | return ret 25 | 26 | def ip_addresses_to_strings(ips): 27 | if not isinstance(ips, list): 28 | if isinstance(ips, str): 29 | return ips 30 | return ips.exploded 31 | 32 | ret = [] 33 | for ip in ips: 34 | if isinstance(ip, str): 35 | ret.append(ip) 36 | elif isinstance(ip, (ipaddress.IPv4Address, ipaddress.IPv6Address)): 37 | ret.append(ip.exploded) 38 | else: 39 | raise XVEx("Can't convert {} to an ip address string".format(ip)) 40 | return ret 41 | 42 | def is_mac_address(mac): 43 | if PROG_MAC_ADDRESS_COLON.match(mac) or PROG_MAC_ADDRESS_DOT.match(mac): 44 | return True 45 | return False 46 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/device_discoverers/device_discoverer.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from abc import ABCMeta, abstractmethod 3 | 4 | from xv_leak_tools.exception import XVEx 5 | 6 | class DeviceDiscoverer(metaclass=ABCMeta): 7 | 8 | def __init__(self, context, device_inventory): 9 | self._context = context 10 | self._device_inventory = [] 11 | for device in device_inventory: 12 | if device['discovery_type'] != self.__class__.discovery_type(): 13 | continue 14 | self._device_inventory.append(device) 15 | 16 | @staticmethod 17 | def _matches_keys(device, discovery_keys): 18 | for key, value in list(discovery_keys.items()): 19 | if key not in device or device[key] != value: 20 | return False 21 | return True 22 | 23 | def _inventory_item_for_discovery_keys(self, discovery_keys): 24 | match = None 25 | for device in self._device_inventory: 26 | if not DeviceDiscoverer._matches_keys(device, discovery_keys): 27 | continue 28 | if match is not None: 29 | raise XVEx( 30 | "Found two devices in the inventory matching the keys:\n{}\n{}".format( 31 | match, device)) 32 | match = device 33 | 34 | if match is None: 35 | return None 36 | 37 | return copy.deepcopy(match) 38 | 39 | @abstractmethod 40 | def discover_device(self, discovery_keys): 41 | pass 42 | 43 | def release_devices(self): 44 | pass 45 | 46 | def cleanup(self): 47 | pass 48 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/macos_enable_new_service_disrupter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | 4 | class MacOSEnableNewServiceDisrupter(Disrupter): 5 | 6 | def __init__(self, device, parameters): 7 | super().__init__(device, parameters) 8 | self._restrict_parameters(must_disrupt=True, must_restore=False) 9 | self._primary_service = None 10 | 11 | def setup(self): 12 | # TODO: This should really be in the network config steps. 13 | L.describe('Ensure there are two active network services') 14 | services = self._device['network_tool'].network_services_in_priority_order() 15 | active_services = [service for service in services if service.active()] 16 | self.assertGreaterEqual( 17 | len(active_services), 2, 18 | "Need two active network services to run this test. Only the following are " 19 | "active: {}".format(active_services)) 20 | 21 | L.describe('Disable the primary network service') 22 | self._primary_service = active_services[0] 23 | self._primary_service.disable() 24 | L.info("Disabled service {}".format(self._primary_service.name())) 25 | 26 | def disrupt(self): 27 | # Slightly confusing, but the disrupt-ion step here is actually enabling the service, not 28 | # disabling it. 29 | L.describe('Re-enable primary network service') 30 | self._primary_service.enable() 31 | 32 | def teardown(self): 33 | if self._primary_service: 34 | self._primary_service.enable() 35 | super().teardown() 36 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/network_tool/linux/linux_network_tool.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from xv_leak_tools.exception import XVEx 4 | from xv_leak_tools.test_components.local_component import LocalComponent 5 | 6 | class LinuxNetworkTool(LocalComponent): 7 | 8 | PROG_PING = re.compile(r"([\d]+) packets transmitted, ([\d]+) received.*") 9 | 10 | def __init__(self, device, config): 11 | super().__init__(device, config) 12 | from xv_leak_tools.network.linux.network_services import LinuxNetwork 13 | self._linux_network = LinuxNetwork 14 | 15 | def network_services_in_priority_order(self): 16 | return self._linux_network.network_services_in_priority_order() 17 | 18 | # TODO: Untested on linux. Will likely fail 19 | def ping(self, ip="8.8.8.8", count=1, timeout=1, interface=None): 20 | '''Returns the number of packets lost when pinging''' 21 | cmd = [ 22 | "ping", 23 | "-c{}".format(count), 24 | "-W{}".format(timeout) 25 | ] 26 | if interface is not None: 27 | cmd += ["-b", interface] 28 | 29 | cmd.append(ip) 30 | ret, stdout, stderr = self._device.connector().execute(cmd) 31 | if not ret: 32 | return 0 33 | 34 | # Find number of packets lost 35 | for line in stdout.splitlines(): 36 | match = LinuxNetworkTool.PROG_PING.match(line) 37 | if not match: 38 | continue 39 | return int(match.group(1)) - int(match.group(2)) 40 | 41 | raise XVEx("Couldn't parse ping output\nstderr: {}\n stdout: {}".format(stderr, stdout)) 42 | -------------------------------------------------------------------------------- /desktop_local_tests/test_packet_capture_disrupt_kill_vpn_process_generate_traffic.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter_and_generator import LocalPacketCaptureTestCaseWithDisrupterAndGenerator 2 | from desktop_local_tests.disrupter_kill_vpn_process import DisrupterKillVPNProcess 3 | 4 | class TestPacketCaptureDisruptKillVPNProcessAndGenerateTraffic( 5 | LocalPacketCaptureTestCaseWithDisrupterAndGenerator): 6 | 7 | '''Summary: 8 | 9 | Test whether traffic leaks when the VPN process crashes. 10 | 11 | Details: 12 | 13 | This test will kill the underlying process responsible for providing the VPN service. For 14 | example, it will kill the openvpn binary when using OpenVPN. It does not kill any other support 15 | processes for the provider, e.g. UI app, daemons etc.. 16 | 17 | Discussion: 18 | 19 | The test is a stress test. Crashes should be rare but in the real world they can happen. A VPN 20 | should be resilient to such crashes. 21 | 22 | Weaknesses: 23 | 24 | Packet capture tests can be noisy. Traffic can be detected as a leak but in actual fact may not 25 | be. For example, traffic might go to a server owned by the VPN provider to re-establish 26 | connections. In general this test is best used for manual exploring leaks rather than for 27 | automation. 28 | 29 | Scenarios: 30 | 31 | No restrictions. 32 | 33 | TODO: 34 | 35 | Consider tests which kill other/all processes related to a VPN. 36 | ''' 37 | 38 | def __init__(self, devices, parameters): 39 | super().__init__(DisrupterKillVPNProcess, devices, parameters) 40 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/route/macos/macos_route.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from xv_leak_tools.test_components.route.route import Route, RouteEntry 4 | from xv_leak_tools.test_device.connector_helper import ConnectorHelper 5 | 6 | class MacOSRoute(Route): 7 | 8 | PROG_ROW = re.compile( 9 | r"^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*([^\s]*)$") 10 | 11 | def __init__(self, device, config): 12 | super().__init__(device, config) 13 | self._connector_helper = ConnectorHelper(self._device) 14 | 15 | # Internet: 16 | # Destination Gateway Flags Refs Use Netif Expire 17 | # default 192.168.56.1 UGSc 100 90 en0 18 | # default 192.168.104.1 UGScI 1 0 en4 19 | # default 192.168.216.1 UGScI 0 0 vlan0 20 | 21 | def get_v4_routes(self): 22 | routes = [] 23 | lines = self._connector_helper.check_command(['netstat', '-rn'])[0].splitlines() 24 | for line in lines: 25 | if "Destination" in line: 26 | continue 27 | match = MacOSRoute.PROG_ROW.match(line) 28 | if not match: 29 | continue 30 | entry = RouteEntry( 31 | dest=match.group(1), 32 | gway=match.group(2), 33 | flags=match.group(3), 34 | refs=match.group(4), 35 | use=match.group(5), 36 | iface=match.group(6), 37 | expire=match.group(7) 38 | ) 39 | routes.append(entry) 40 | return routes 41 | -------------------------------------------------------------------------------- /configs/case_studies/bittorrent_leaks.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_templating.templating import TemplateEvaluator, Replacee, Each 2 | 3 | # This list will contain all the individual test configurations 4 | TESTS = [] 5 | 6 | TEMPLATE = { 7 | 'name': "TestTorrentTrackerIPLeakNet", 8 | 'devices': [ 9 | { 10 | "discovery_keys": { 11 | "device_id": "localhost" 12 | }, 13 | "device_name": "localhost", 14 | 'components': { 15 | 'vpn_application': { 16 | 'name': 'generic', 17 | } 18 | }, 19 | } 20 | ], 21 | 'parameters': { 22 | # Change this if you don't use chrome as your default browser. It doesn't really matter 23 | # which one you use here. 24 | 'browser': 'chrome', 25 | # It's known that some torrent clients cache your IP addresses. For the purpose of this test 26 | # let's exclude that because it would pollute results. Caching is a separate issue. 27 | # If torrent_client_preopened is True then ALL VPN providers are going to leak here. 28 | 'torrent_client_preopened': False, 29 | 'torrent_client': Replacee("$TORRENT_CLIENT"), 30 | } 31 | } 32 | 33 | # Currently we only test transmission and utorrent. You can extend the tests to cover any 34 | # client by creating a torrent client class and adding it to the list below. 35 | TEMPLATE_PARAMETERS_LIST = [ 36 | { 37 | 'TEMPLATE': TEMPLATE, 38 | '$TORRENT_CLIENT': Each(['transmission', 'utorrent']), 39 | }, 40 | ] 41 | 42 | TESTS += TemplateEvaluator.generate(TEMPLATE_PARAMETERS_LIST) 43 | -------------------------------------------------------------------------------- /configs/case_studies/vpn_process_crashes_leaks.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_templating.templating import TemplateEvaluator, Replacee, Each 2 | 3 | # Note that packet capture tests are inherently noisy at the moment. Very few (if any) VPN providers 4 | # guarantee no non-tunnel traffic so false positives are almost guaranteed. The tests are still 5 | # useful for investigatory purposes. 6 | 7 | # This list will contain all the individual test configurations 8 | TESTS = [] 9 | 10 | TEMPLATE = { 11 | 'name': Replacee("$TEST_NAME"), 12 | 'devices': [ 13 | { 14 | "discovery_keys": { 15 | "device_id": "localhost" 16 | }, 17 | "device_name": "localhost", 18 | 'components': { 19 | 'vpn_application': { 20 | 'name': 'generic', 21 | } 22 | }, 23 | } 24 | ], 25 | 'parameters': { 26 | # Check period here is quite long. The reason being that many VPN providers use an openvpn 27 | # settings called ping-timeout to decide if they lost contact with the VPN server. Often 28 | # that is set to 1 minute. We need to wait at least that long to see if there's a leak. 29 | 'check_period': 90, 30 | } 31 | } 32 | 33 | TEMPLATE_PARAMETERS_LIST = [ 34 | { 35 | 'TEMPLATE': TEMPLATE, 36 | '$TEST_NAME': Each( 37 | [ 38 | "TestDNSDisruptKillVPNProcess", 39 | "TestIPResponderDisruptKillVPNProcess", 40 | "TestPacketCaptureDisruptKillVPNProcess" 41 | ]), 42 | }, 43 | ] 44 | 45 | TESTS = TemplateEvaluator.generate(TEMPLATE_PARAMETERS_LIST) 46 | -------------------------------------------------------------------------------- /configs/case_studies/vpn_server_rechability_leaks.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.test_templating.templating import TemplateEvaluator, Replacee, Each 2 | 3 | # Note that packet capture tests are inherently noisy at the moment. Very few (if any) VPN providers 4 | # guarantee no non-tunnel traffic so false positives are almost guaranteed. The tests are still 5 | # useful for investigatory purposes. 6 | 7 | # This list will contain all the individual test configurations 8 | TESTS = [] 9 | 10 | TEMPLATE = { 11 | 'name': Replacee("$TEST_NAME"), 12 | 'devices': [ 13 | { 14 | "discovery_keys": { 15 | "device_id": "localhost" 16 | }, 17 | "device_name": "localhost", 18 | 'components': { 19 | 'vpn_application': { 20 | 'name': 'generic', 21 | } 22 | }, 23 | } 24 | ], 25 | 'parameters': { 26 | # Check period here is quite long. The reason being that many VPN providers use an openvpn 27 | # settings called ping-timeout to decide if they lost contact with the VPN server. Often 28 | # that is set to 1 minute. We need to wait at least that long to see if there's a leak. 29 | 'check_period': 90, 30 | } 31 | } 32 | 33 | TEMPLATE_PARAMETERS_LIST = [ 34 | { 35 | 'TEMPLATE': TEMPLATE, 36 | '$TEST_NAME': Each( 37 | [ 38 | "TestDNSDisruptVPNConnection", 39 | "TestIPResponderDisruptVPNConnection", 40 | "TestPacketCaptureDisruptVPNConnection" 41 | ]), 42 | }, 43 | ] 44 | 45 | TESTS = TemplateEvaluator.generate(TEMPLATE_PARAMETERS_LIST) 46 | -------------------------------------------------------------------------------- /xv_leak_tools/test_framework/test_factory.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools.exception import XVEx 2 | from xv_leak_tools.import_helpers import import_all_from_package, class_by_name, itersubclasses 3 | from xv_leak_tools.test_framework.test_case import TestCase 4 | 5 | # TODO: I'm even tempted to remove this and just make users specify the full path to the test 6 | # in the config and have an attribute in the test file called TestClass (or similar) 7 | class TestFactory: 8 | 9 | # pylint: disable=no-self-use 10 | # pylint: disable=too-few-public-methods 11 | 12 | def __init__(self, packages): 13 | self._packages = packages 14 | self._import_tests() 15 | 16 | @staticmethod 17 | def _check_test_classes_are_unique(): 18 | '''Helper to check that no classes have duplicate names. We want tests to have unique names 19 | to avoid any confusion.''' 20 | all_test_classes = [] 21 | for subclass in itersubclasses(TestCase): 22 | if not subclass.__name__.startswith("Test"): 23 | continue 24 | if subclass.__name__ in all_test_classes: 25 | raise XVEx( 26 | "A test with name {} is defined twice. Please ensure test class names are " 27 | "unique".format(subclass.__name__)) 28 | all_test_classes.append(subclass.__name__) 29 | 30 | def _import_tests(self): 31 | for package in self._packages: 32 | import_all_from_package(package, restrict_platform=False) 33 | TestFactory._check_test_classes_are_unique() 34 | 35 | def create(self, name, devices, parameters): 36 | return class_by_name(name, TestCase)(devices, parameters) 37 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_packet_capture_disrupt_reorder_services.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter import LocalPacketCaptureTestCaseWithDisrupter 2 | from desktop_local_tests.macos.macos_reorder_services_disrupter import MacOSDNSReorderServicesDisrupter 3 | 4 | class TestMacOSPacketCaptureDisruptReorderServices(LocalPacketCaptureTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device leaks outside of the VPN tunnel when the network 9 | service order is changed. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then swap the priority of the primary and secondary network 14 | services. The test looks for leaking traffic once the service order is changed. 15 | 16 | Discussion: 17 | 18 | It's not 100% clear if, in the real world, services can change their order without user 19 | involvement. It is still however a good stress test of the application. 20 | 21 | Weaknesses: 22 | 23 | Packet capture tests can be noisy. Traffic can be detected as a leak but in actual fact may not 24 | be. For example, traffic might go to a server owned by the VPN provider to re-establish 25 | connections. In general this test is best used for manual exploring leaks rather than for 26 | automation. 27 | 28 | Scenarios: 29 | 30 | Requires two active network services. 31 | 32 | TODO: 33 | 34 | Consider a variant which changes the network "Location". This is much more likely to be 35 | something a user might do. 36 | ''' 37 | 38 | def __init__(self, devices, parameters): 39 | super().__init__(MacOSDNSReorderServicesDisrupter, devices, parameters) 40 | -------------------------------------------------------------------------------- /desktop_local_tests/local_packet_capture_test_case_with_disrupter_and_generator.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter import LocalPacketCaptureTestCaseWithDisrupter 2 | from xv_leak_tools.log import L 3 | 4 | class LocalPacketCaptureTestCaseWithDisrupterAndGenerator(LocalPacketCaptureTestCaseWithDisrupter): 5 | 6 | def __init__(self, disrupter_class, devices, parameters): 7 | super().__init__(disrupter_class, devices, parameters) 8 | self.webdriver = None 9 | 10 | def filter_packets(self, packets): 11 | # Don't use the parent filter. We want to be stricter here. Local traffic is okay. For 12 | # example, suppose I'm testing a VM guest and the host uses local DNS. Then the traffic 13 | # will look like local->local:53. We don't want to filter that out. That would be a leak. 14 | gateway = self.localhost['vpn_application'].tunnel_gateway() 15 | unmatched = self.traffic_filter.filter_traffic(packets, multicast=True)[1] 16 | unmatched = self.traffic_filter.filter_traffic(packets, dst_ip=gateway)[1] 17 | just_port_53_packets = self.traffic_filter.filter_traffic(unmatched, dst_port=53)[0] 18 | return just_port_53_packets 19 | 20 | def test_with_packet_capture(self): 21 | L.describe("Create disruption...") 22 | self.disrupter.create_disruption() 23 | L.describe("Generate traffic") 24 | self.webdriver = self.localhost['webdriver'].driver(self.parameters['browser']) 25 | self.webdriver.get("https://www.expressvpn.com/dns-leak-test") 26 | 27 | def teardown(self): 28 | if self.webdriver: 29 | self.webdriver.quit() 30 | super().teardown() 31 | -------------------------------------------------------------------------------- /desktop_local_tests/test_public_ip_disrupt_cable.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.disrupter_cable import DisrupterCable 3 | 4 | class TestPublicIPDisruptCable(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether the device's public IP is exposed when the Ethernet cable is either removed or 9 | plugged in after connection. 10 | 11 | Details: 12 | 13 | The test is a manual test which prompts the user to unplug or plug in an Ethernet (depending 14 | on how the test is configured). Once the cable has been unplugged/plugged the test repeatedly 15 | checks the device's public IPv4 and IPv6 addresses by visiting a webpage designed to report 16 | those IPs. 17 | 18 | Discussion: 19 | 20 | Since the test is manual, one could actually replace "Plug/Unplug Ethernet Cable" with any 21 | action they desire. We could have just created a test called TestDNSManualDisruption (or 22 | similar). The reason for specifically naming the test is just to catalog the type of test case 23 | we're interested in. 24 | 25 | If you're working with a VM then you can simulate pulling a cable by just disabling the network 26 | adapter on the host machine, e.g. with VMWare: 27 | 28 | VM Settings->Network Adapter N->Connect Network Adapter. 29 | 30 | Weaknesses: 31 | 32 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 33 | preferred over these tests. 34 | 35 | Scenarios: 36 | 37 | No restrictions. 38 | ''' 39 | 40 | def __init__(self, devices, parameters): 41 | super().__init__(DisrupterCable, devices, parameters) 42 | -------------------------------------------------------------------------------- /desktop_local_tests/disrupter_cable.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.disrupter import Disrupter 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class DisrupterCable(Disrupter): 6 | 7 | PULL_MESSAGE = '''\ 8 | Pull the ethernet cable. 9 | If you're using a VM then you can simulate this by disconnecting the network adapter from the VM \ 10 | settings. 11 | NOTE: You should press enter BEFORE pulling the cable to maximize the chance of detecting a leak. 12 | ''' 13 | 14 | PLUG_MESSAGE = 'Replace the ethernet cable. You should press enter ' \ 15 | 'BEFORE replacing the cable to maximize the chance of detecting a leak.' 16 | 17 | def __init__(self, device, parameters): 18 | super().__init__(device, parameters) 19 | self._restrict_parameters(must_disrupt=True) 20 | self._pull = self._parameters.get("pull", True) 21 | 22 | def setup(self): 23 | # TODO: This should be done with a network configuration step 24 | msg = "Ensure you have an Ethernet (wired) connection and at least one " \ 25 | "other network service, e.g. Wi-Fi\n" 26 | if not self._pull: 27 | msg += "Ensure that the cable is UNPLUGGED." 28 | message_and_await_enter(msg) 29 | 30 | def disrupt(self): 31 | msg = DisrupterCable.PULL_MESSAGE if self._pull else DisrupterCable.PLUG_MESSAGE 32 | L.describe(msg) 33 | message_and_await_enter(msg) 34 | 35 | def restore(self): 36 | msg = "Ensure the ethernet cable is plugged back in" 37 | L.describe(msg) 38 | message_and_await_enter(msg) 39 | 40 | def teardown(self): 41 | self.restore() 42 | super().teardown() 43 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_public_ip_disrupt_reorder_adapters.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.windows.windows_reorder_adapters_disrupter import WindowsReorderAdaptersDisrupter 3 | 4 | class TestWindowsPublicIPDisruptReorderAdapters(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the adapter order 9 | is changed. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then swap the priority of the primary and secondary network 14 | adapters. The test then queries a webpage to detect it's public IP. 15 | 16 | Discussion: 17 | 18 | It's not 100% clear if, in the real world, adapters can change their order without user 19 | involvement. It is still however a good stress test of the application. 20 | 21 | On Windows adapter order is determined by the interface metric. It can be manually set but 22 | otherwise it is determined by the system by deciding how "good" an adapter is, e.g. what is the 23 | throughput. In theory that means metrics can change dynamically. 24 | 25 | Weaknesses: 26 | 27 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 28 | preferred over these tests. 29 | 30 | Scenarios: 31 | 32 | Requires two active adapters. 33 | 34 | TODO: 35 | 36 | Consider a variant which changes the network "Location". This is much more likely to be 37 | something a user might do. 38 | ''' 39 | 40 | def __init__(self, devices, parameters): 41 | super().__init__(WindowsReorderAdaptersDisrupter, devices, parameters) 42 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_packet_capture_disrupt_wifi_power.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter import LocalPacketCaptureTestCaseWithDisrupter 2 | from desktop_local_tests.macos.macos_wifi_power_disrupter import MacOSWifiPowerDisrupter 3 | 4 | class TestMacOSPacketCaptureDisruptWifiPower(LocalPacketCaptureTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device leaks outside of the VPN tunnel when the Wi-Fi 9 | power is disabled. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then disable the Wi-Fi power. 14 | 15 | This test uses a simple UDP client which spams UDP packets to a public server. The server logs 16 | the source IP of every packet. The test checks with the server to make sure that the public IP 17 | is always the VPN server's IP and not the device's. 18 | 19 | Discussion: 20 | 21 | This test is somewhat of a stress test. A more realistic scenario with Wi-Fi is losing 22 | connectivity, e.g. by physically moving out of range of the Wi-Fi network's APs. 23 | 24 | Weaknesses: 25 | 26 | Packet capture tests can be noisy. Traffic can be detected as a leak but in actual fact may not 27 | be. For example, traffic might go to a server owned by the VPN provider to re-establish 28 | connections. In general this test is best used for manual exploring leaks rather than for 29 | automation. 30 | 31 | Scenarios: 32 | 33 | * Run test when Wi-Fi is the primary network service 34 | * Run test when Wi-Fi is NOT the primary network service 35 | ''' 36 | 37 | 38 | def __init__(self, devices, parameters): 39 | super().__init__(MacOSWifiPowerDisrupter, devices, parameters) 40 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/git/git.py: -------------------------------------------------------------------------------- 1 | from xv_leak_tools import tools_root 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.process import check_subprocess 4 | from xv_leak_tools.test_components.local_component import LocalComponent 5 | from xv_leak_tools.test_device.connector_helper import ConnectorHelper 6 | 7 | class Git(LocalComponent): 8 | 9 | @staticmethod 10 | def _git_branch(): 11 | '''Return the git branch we're currently on. This is designed to run on the test 12 | orchestration device, i.e. localhost. We use it to ensure that all devices are checked out 13 | to the same revision''' 14 | # TODO: Consider making this configurable as well, with the default being this. 15 | return check_subprocess(['git', 'rev-parse', '--abbrev-ref', 'HEAD'])[0].strip() 16 | 17 | def setup(self): 18 | if not self._config.get("checkout", False): 19 | return 20 | 21 | # Update the device's git checkout to be the same as our branch and to be at latest revision 22 | branch = Git._git_branch() 23 | L.info( 24 | "Updating git repo to branch {} on device {}".format(branch, self._device.device_id())) 25 | connector_helper = ConnectorHelper(self._device) 26 | 27 | git_root = self._device.config().get('git_root', tools_root()) 28 | 29 | # TODO: Potentially should clean as well? 30 | connector_helper.execute_command( 31 | [ 32 | # This can possibly be done in fewer lines 33 | 'cd', git_root, '&&', 34 | 'git', 'checkout', branch, '&&', 35 | 'git', 'pull', '&&', 36 | 'git', 'submodule', 'update', '--init', '--recursive' 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/settings/android_settings.py: -------------------------------------------------------------------------------- 1 | import time 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.test_components.settings.settings import Settings 4 | 5 | class AndroidSettings(Settings): 6 | 7 | def enable_wifi(self): 8 | L.info('Enabling WiFi') 9 | cmd = ["svc", "wifi", "enable"] 10 | self._device.connector().execute(cmd, root=True) 11 | L.debug('Sleeping for 5 seconds') 12 | time.sleep(5) 13 | 14 | def disable_wifi(self): 15 | L.info('Disabling WiFi') 16 | cmd = ["svc", "wifi", "disable"] 17 | self._device.connector().execute(cmd, root=True) 18 | 19 | def enable_mobile_data(self): 20 | L.info('Enabling mobile data') 21 | cmd = ["svc", "data", "enable"] 22 | return self._device.connector().execute(cmd, root=True) 23 | 24 | def disable_mobile_data(self): 25 | L.info('Disabling mobile data') 26 | cmd = ["svc", "data", "disable"] 27 | self._device.connector().execute(cmd, root=True) 28 | 29 | def enable_airplane_mode(self): 30 | L.info('Enabling airplane mode') 31 | cmd = ['settings', 'put', 'global', 'airplane_mode_on', '1'] 32 | self._device.connector().execute(cmd, root=False) 33 | cmd = ['am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE'] 34 | self._device.connector().execute(cmd, root=True) 35 | 36 | def disable_airplane_mode(self): 37 | L.info('Disabling airplane mode') 38 | cmd = ['settings', 'put', 'global', 'airplane_mode_on', '0'] 39 | self._device.connector().execute(cmd, root=False) 40 | cmd = ['am', 'broadcast', '-a', 'android.intent.action.AIRPLANE_MODE'] 41 | self._device.connector().execute(cmd, root=True) 42 | -------------------------------------------------------------------------------- /desktop_local_tests/test_public_ip_disrupt_vpn_connection.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.vpn_connection_disrupter import VPNConnectionDisrupter 3 | 4 | class TestPublicIPDisruptVPNConnection(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the VPN server 9 | becomes unreachable. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then put up firewall rules which block connectivity to the VPN 14 | server. The test then queries a webpage to detect it's public IP. 15 | 16 | Discussion: 17 | 18 | Connectivity drops to the VPN server are very real world threats. This could happen for a 19 | variety of reasons: 20 | 21 | * Server goes down 22 | * Server is deliberately taken out of rotation for maintenance etc.. 23 | * Blocking 24 | * Bad routes 25 | 26 | In all cases a firewall adequately represents these connectivity drops. 27 | 28 | Weaknesses: 29 | 30 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 31 | preferred over these tests. 32 | 33 | With some systems/VPN applications, a firewall on the test device might not adequately block the 34 | VPN server. For such setups, a secondary device is needed e.g. 35 | 36 | * Firewall on a router 37 | * Firewall on host if the test device is a VM. 38 | 39 | Scenarios: 40 | 41 | No restrictions. 42 | 43 | TODO: 44 | 45 | Implement multi-device test with firewall off device 46 | 47 | ''' 48 | 49 | def __init__(self, devices, parameters): 50 | super().__init__(VPNConnectionDisrupter, devices, parameters) 51 | -------------------------------------------------------------------------------- /desktop_local_tests/local_test_case.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from xv_leak_tools.exception import XVEx 4 | from xv_leak_tools.helpers import TimeUp 5 | from xv_leak_tools.log import L 6 | from xv_leak_tools.test_framework.test_case import TestCase 7 | 8 | class LocalTestCase(TestCase): 9 | 10 | def __init__(self, devices, parameters): 11 | super().__init__(devices, parameters) 12 | self.localhost = self.devices['localhost'] 13 | 14 | def _check_network(self, time_limit=5): 15 | L.info("Checking if there's a network connection") 16 | timeup = TimeUp(time_limit) 17 | while not timeup: 18 | lost = self.localhost['network_tool'].ping('8.8.8.8', count=3, timeout=2) 19 | if lost == 3: 20 | L.warning("No network detected. Will try for another {} seconds" 21 | .format(int(timeup.time_left()))) 22 | time.sleep(0.5) 23 | elif lost == 0: 24 | L.info("Network okay") 25 | return 26 | else: 27 | L.warning("Network detected but there's some packet loss") 28 | return 29 | raise XVEx("No network connection detected.") 30 | 31 | def setup(self): 32 | super().setup() 33 | 34 | # TODO: Not sure all this stuff belongs here. Probably belongs in a derived class 35 | L.describe("Ensure no VPN apps are connected or open") 36 | self.localhost['cleanup'].cleanup() 37 | 38 | self._check_network() 39 | 40 | L.describe("Configure VPN application") 41 | self.localhost['vpn_application'].configure() 42 | 43 | def teardown(self): 44 | self.localhost['vpn_application'].disconnect() 45 | self.localhost['vpn_application'].close() 46 | 47 | super().teardown() 48 | -------------------------------------------------------------------------------- /desktop_local_tests/linux/linux_dns_force_public_resolv_conf.py: -------------------------------------------------------------------------------- 1 | import os 2 | from desktop_local_tests.disrupter import Disrupter 3 | from xv_leak_tools.exception import XVEx 4 | from xv_leak_tools.log import L 5 | 6 | class LinuxDNSForcePublicResolvConfDisrupter(Disrupter): 7 | 8 | def __init__(self, device, parameters): 9 | super().__init__(device, parameters) 10 | self._restrict_parameters(must_disrupt=True, must_restore=False, must_wait=False) 11 | self._resolv_conf_path = "/etc/resolv.conf" 12 | self._resolv_conf_target_path = None 13 | self._temp_resolv_conf_path = os.path.join(device.temp_directory(), "resolv.conf") 14 | 15 | def disrupt(self): 16 | L.describe('Set the DNS servers in resolv.conf to public DNS servers') 17 | 18 | temp_resolv_conf = open(self._temp_resolv_conf_path, "w") 19 | # TODO: make configurable? 20 | dns_servers = ['37.235.1.174', '37.235.1.177'] 21 | for nameserver in dns_servers: 22 | temp_resolv_conf.write("nameserver {}\n".format(nameserver)) 23 | temp_resolv_conf.close() 24 | 25 | if os.path.islink(self._resolv_conf_path) and os.path.exists(self._resolv_conf_path): 26 | self._resolv_conf_target_path = os.readlink(self._resolv_conf_path) 27 | os.remove(self._resolv_conf_path) 28 | os.symlink(self._temp_resolv_conf_path, self._resolv_conf_path) 29 | os.sync() 30 | else: 31 | raise XVEx("Can't replace resolv.conf; not a symlink") 32 | 33 | def _restore_dns_servers(self): 34 | os.remove(self._resolv_conf_path) 35 | os.symlink(self._resolv_conf_target_path, self._resolv_conf_path) 36 | os.sync() 37 | 38 | def teardown(self): 39 | self._restore_dns_servers() 40 | super().teardown() 41 | -------------------------------------------------------------------------------- /xv_leak_tools/test_components/route/linux/linux_route.py: -------------------------------------------------------------------------------- 1 | import netaddr 2 | import re 3 | 4 | from xv_leak_tools.exception import XVEx 5 | from xv_leak_tools.test_components.route.route import Route, RouteEntry 6 | from xv_leak_tools.test_device.connector_helper import ConnectorHelper 7 | 8 | class LinuxRoute(Route): 9 | 10 | PROG_ROW = re.compile( 11 | r"^([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s*$") 12 | 13 | def __init__(self, device, config): 14 | super().__init__(device, config) 15 | self._connector_helper = ConnectorHelper(self._device) 16 | 17 | # Kernel IP routing table 18 | # Destination Gateway Genmask Flags MSS Window irtt Iface 19 | # 0.0.0.0 172.16.49.2 0.0.0.0 UG 0 0 0 ens33 20 | # 169.254.0.0 0.0.0.0 255.255.0.0 U 0 0 0 ens33 21 | # 172.16.49.0 0.0.0.0 255.255.255.0 U 0 0 0 ens33 22 | 23 | def get_v4_routes(self): 24 | routes = [] 25 | lines = self._connector_helper.check_command(['netstat', '-rn'])[0].splitlines() 26 | for line in lines: 27 | if "Destination" in line: 28 | continue 29 | match = LinuxRoute.PROG_ROW.match(line) 30 | print(line) 31 | if not match: 32 | continue 33 | # Windows routes use IPs for the netmask rather than CIDR blocks. 34 | dest = "{}/{}".format(match.group(1), netaddr.IPAddress(match.group(3)).netmask_bits()) 35 | entry = RouteEntry( 36 | dest=dest, 37 | gway=match.group(2), 38 | flags=match.group(4), 39 | iface=match.group(8), 40 | ) 41 | routes.append(entry) 42 | return routes 43 | -------------------------------------------------------------------------------- /docs/setting_up_linux.md: -------------------------------------------------------------------------------- 1 | # Linux 2 | 3 | ## Install packages 4 | 5 | ### Ubuntu 6 | 7 | ``` 8 | sudo apt-get install python3-dev libdbus-glib-1-dev 9 | ``` 10 | 11 | ### Fedora 12 | 13 | ``` 14 | sudo dnf install redhat-lsb-core python3-devel gcc dbus-devel dbus-glib-devel 15 | ``` 16 | 17 | ## Checkout xv\_leak\_tools\_internal 18 | 19 | Simply `git clone` this repo to a location of your choice. We'll refer to the `python` subfolder in 20 | that location as `$LEAK_TOOLS_ROOT`. 21 | 22 | ## Setup Python 23 | 24 | Run `./setup_python.sh $VIRTUALENV_LOCATION` where `$VIRTUALENV_LOCATION` is the directory 25 | where you want the virtualenv to be created, e.g. 26 | 27 | ``` 28 | ./setup_python.sh ~/xv_leak_testing_python 29 | ``` 30 | 31 | You can now source the `activate` script as a shortcut to activating `virtualenv` with this version 32 | of python. 33 | 34 | ## Install VPN Applications 35 | 36 | Install ExpressVPN and any other applications you want to test. No specific steps related to leak 37 | testing are required. 38 | 39 | ## Install Browsers and Webdrivers 40 | 41 | First ensure that Chrome, Firefox and Opera are installed (where supported). 42 | 43 | > Note that some drives can be installed with package managers, e.g. apt. 44 | 45 | In order for selenium to control various browsers some install steps are required. 46 | 47 | Install Edge driver: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/#downloads 48 | 49 | Install Chrome driver: https://chromedriver.storage.googleapis.com/index.html?path=2.30/ 50 | 51 | Firefox gecko driver: https://github.com/mozilla/geckodriver/releases 52 | 53 | Opera driver: https://github.com/operasoftware/operachromiumdriver/releases 54 | 55 | ## Install Torrent Clients 56 | 57 | You can choose which torrent clients you want to test. We have tested 58 | 59 | * Transmission 60 | * uTorrent 61 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_dns_disrupt_reorder_adapters.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.dns_during_disruption import DNSDuringDisruptionTestCase 2 | from desktop_local_tests.windows.windows_reorder_adapters_disrupter import WindowsReorderAdaptersDisrupter 3 | 4 | class TestWindowsDNSDisruptReorderAdapters(DNSDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether DNS leaks when the adapter order is changed. 9 | 10 | Details: 11 | 12 | This test will connect to VPN then swap the priority of the primary and secondary network 13 | adapters. Once the order is changed the test repeatedly makes DNS requests and checks whether 14 | the DNS request went to a non VPN DNS server 15 | 16 | Discussion: 17 | 18 | It's not 100% clear if, in the real world, adapters can change their order without user 19 | involvement. It is still however a good stress test of the application. 20 | 21 | On Windows adapter order is determined by the interface metric. It can be manually set but 22 | otherwise it is determined by the system by deciding how "good" an adapter is, e.g. what is the 23 | throughput. In theory that means metrics can change dynamically. 24 | 25 | Weaknesses: 26 | 27 | Currently uses dig to decide if DNS leaks. This isn't reliable for some VPN providers. Some 28 | providers intercept DNS upstream and change the destination DNS server to their own server. 29 | However dig will still report the server which it originally sent the request to. 30 | 31 | Scenarios: 32 | 33 | Requires two active adapters. 34 | 35 | TODO: 36 | 37 | Consider a variant which changes the network "Location". This is much more likely to be 38 | something a user might do. 39 | ''' 40 | 41 | def __init__(self, devices, parameters): 42 | super().__init__(WindowsReorderAdaptersDisrupter, devices, parameters) 43 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_public_ip_disrupt_enable_new_adapter.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.windows.windows_enable_new_adapter_disrupter import WindowsEnableNewAdapterDisrupter 3 | 4 | class TestWindowsPublicIPDisruptEnableNewAdapter(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when a higher priority 9 | network adapter becomes active after connecting. 10 | 11 | Details: 12 | 13 | The test first identifies the highest priority adapter and disables it. The test then queries a 14 | webpage to detect it's public IP. 15 | 16 | Discussion: 17 | 18 | There are several ways in which a adapter could become active after connect: 19 | 20 | * The adapter is "enabled" via Network Connections (in Control Panel) 21 | * The adapter is enabled but there's no connectivity, e.g. the Ethernet cable is unplugged or 22 | Wi-Fi isn't connected to a Wi-Fi network. We refer to this situation as the adapter having 23 | "no network". 24 | * The adapter never existed in the first place and is created after connect. 25 | 26 | This test uses the first method to disable/re-enable the adapter to test for leaks. The other 27 | two scenarios are valid test cases and should also be implemented. 28 | 29 | Weaknesses: 30 | 31 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 32 | preferred over these tests. 33 | 34 | Scenarios: 35 | 36 | Requires two active adapters. 37 | 38 | TODO: 39 | 40 | Add tests for inactive and newly created adapters. 41 | ''' 42 | 43 | def __init__(self, devices, parameters): 44 | super().__init__(WindowsEnableNewAdapterDisrupter, devices, parameters) 45 | -------------------------------------------------------------------------------- /desktop_local_tests/test_ip_responder_disrupt_cable.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_ip_responder_test_case_with_disrupter import LocalIPResponderTestCaseWithDisrupter 2 | from desktop_local_tests.disrupter_cable import DisrupterCable 3 | 4 | class TestIPResponderDisruptCable(LocalIPResponderTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when the Ethernet cable 9 | is either removed or plugged in after connection. 10 | 11 | Details: 12 | 13 | The test is a manual test which prompts the user to unplug or plug in an Ethernet (depending on 14 | how the test is configured). 15 | 16 | This test uses a simple UDP client which spams UDP packets to a public server. The server logs 17 | the source IP of every packet. The test checks with the server to make sure that the public IP 18 | is always the VPN server's IP and not the device's. 19 | 20 | Discussion: 21 | 22 | Since the test is manual, one could actually replace "Plug/Unplug Ethernet Cable" with any 23 | action they desire. We could have just created a test called TestDNSManualDisruption (or 24 | similar). The reason for specifically naming the test is just to catalog the type of test case 25 | we're interested in. 26 | 27 | If you're working with a VM then you can simulate pulling a cable by just disabling the network 28 | adapter on the host machine, e.g. with VMWare: 29 | 30 | VM Settings->Network Adapter N->Connect Network Adapter. 31 | 32 | Weaknesses: 33 | 34 | None 35 | 36 | Scenarios: 37 | 38 | No restrictions. 39 | 40 | TODO: 41 | 42 | Automate this test when using a VM. We should be able to disconnect/reconnect the guest 43 | programmatically. 44 | 45 | ''' 46 | 47 | def __init__(self, devices, parameters): 48 | super().__init__(DisrupterCable, devices, parameters) 49 | -------------------------------------------------------------------------------- /desktop_local_tests/macos/test_macos_public_ip_disrupt_enable_new_service.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.public_ip_during_disruption import PublicIPDuringDisruptionTestCase 2 | from desktop_local_tests.macos.macos_enable_new_service_disrupter import MacOSEnableNewServiceDisrupter 3 | 4 | class TestMacOSPublicIPDisruptEnableNewService(PublicIPDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device has the public IP hidden when a higher priority 9 | network service becomes active after connecting. 10 | 11 | Details: 12 | 13 | The test first identifies the highest priority network service and disables it. It then 14 | connects to the VPN and re-enables that service. The test then queries a webpage to detect it's 15 | public IP. 16 | 17 | Discussion: 18 | 19 | There are several ways in which a service could become active after connect: 20 | 21 | * The service is "enabled" via System Preferences 22 | * Service is enabled but there's no connectivity, e.g. the Ethernet cable is unplugged or Wi-Fi 23 | isn't connected to a Wi-Fi network. We refer to this situation as the service being 24 | "inactive". 25 | * The service never existed in the first place and is created after connect. 26 | 27 | This test uses the first method to disable/reenable the service to test for leaks. The other two 28 | scenarios are valid test cases and should also be implemented. 29 | 30 | Weaknesses: 31 | 32 | The time taken to perform each IP request is relatively long. Tests using IPResponder should be 33 | preferred over these tests. 34 | 35 | Scenarios: 36 | 37 | Requires two active network services. 38 | 39 | TODO: 40 | 41 | Add tests for inactive and newly created services. 42 | ''' 43 | 44 | def __init__(self, devices, parameters): 45 | super().__init__(MacOSEnableNewServiceDisrupter, devices, parameters) 46 | -------------------------------------------------------------------------------- /desktop_local_tests/test_dns_disrupt_vpn_connection.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.dns_during_disruption import DNSDuringDisruptionTestCase 2 | from desktop_local_tests.vpn_connection_disrupter import VPNConnectionDisrupter 3 | 4 | class TestDNSDisruptVPNConnection(DNSDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether DNS leaks when the VPN server becomes unreachable. 9 | 10 | Details: 11 | 12 | This test will connect to VPN then put up firewall rules which block connectivity to the VPN 13 | server. Once the rules are up the test repeatedly makes DNS requests and checks whether the DNS 14 | request went to a non VPN DNS server a DNS leak. 15 | 16 | Discussion: 17 | 18 | Connectivity drops to the VPN server are very real world threats. This could happen for a 19 | variety of reasons: 20 | 21 | * Server goes down 22 | * Server is deliberately taken out of rotation for maintenance etc.. 23 | * Blocking 24 | * Bad routes 25 | 26 | In all cases a firewall adequately represents these connectivity drops. 27 | 28 | Weaknesses: 29 | 30 | Currently uses dig to decide if DNS leaks. This isn't reliable for some VPN providers. Some 31 | providers intercept DNS upstream and change the destination DNS server to their own server. 32 | However dig will still report the server which it originally sent the request to. 33 | 34 | With some systems/VPN applications, a firewall on the test device might not adequately block the 35 | VPN server. For such setups, a secondary device is needed e.g. 36 | 37 | * Firewall on a router 38 | * Firewall on host if the test device is a VM. 39 | 40 | Scenarios: 41 | 42 | No restrictions. 43 | 44 | TODO: 45 | 46 | Implement multi-device test with firewall off device 47 | 48 | ''' 49 | 50 | def __init__(self, devices, parameters): 51 | super().__init__(VPNConnectionDisrupter, devices, parameters) 52 | -------------------------------------------------------------------------------- /xv_leak_tools/test_device/device_discoverers/static_discoverer.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from xv_leak_tools.exception import XVEx 4 | from xv_leak_tools.log import L 5 | from xv_leak_tools.test_device.create_device import create_device 6 | from xv_leak_tools.test_device.device_discoverers.device_discoverer import DeviceDiscoverer 7 | from xv_leak_tools.test_device.simple_ssh_connector import SimpleSSHConnector 8 | from xv_leak_tools.test_device.adb_connector import ADBConnector 9 | from xv_leak_tools.test_device.dummy_connector import DummyConnector 10 | 11 | class StaticDeviceDiscoverer(DeviceDiscoverer): 12 | 13 | @staticmethod 14 | def discovery_type(): 15 | return 'static' 16 | 17 | def discover_device(self, discovery_keys): 18 | L.debug('Looking for device with keys {}'.format(discovery_keys)) 19 | 20 | device = self._inventory_item_for_discovery_keys(discovery_keys) 21 | if device is None: 22 | return None 23 | 24 | if 'output_root' not in device: 25 | raise XVEx("Device config didn't specify 'output_root': {}".format(device)) 26 | 27 | device['output_directory'] = os.path.join( 28 | device['output_root'], self._context['run_directory']) 29 | 30 | # TODO: Look into refactoring this. 31 | if 'dummy' in device and device['dummy']: 32 | connector = DummyConnector() 33 | elif 'adb_id' in device and device['adb_id']: 34 | connector = ADBConnector(device['adb_id']) 35 | else: 36 | connector = SimpleSSHConnector( 37 | ips=device['ips'], 38 | username=device['username'], 39 | account_password=device.get('account_password', None), 40 | ssh_key=device.get('ssh_key', None), 41 | ssh_password=device.get('ssh_password', None) 42 | ) 43 | 44 | return create_device(device['os_name'], device, connector) 45 | -------------------------------------------------------------------------------- /desktop_local_tests/test_dns_disrupt_cable.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.dns_during_disruption import DNSDuringDisruptionTestCase 2 | from desktop_local_tests.disrupter_cable import DisrupterCable 3 | 4 | class TestDNSDisruptCable(DNSDuringDisruptionTestCase): 5 | 6 | '''Summary: 7 | 8 | Test whether DNS leaks when the Ethernet cable is either removed or plugged in after connection. 9 | 10 | Details: 11 | 12 | The test is a manual test which prompts the user to unplug or plug in an Ethernet (depending 13 | on how the test is configured). Once the cable has been unplugged/plugged the test repeatedly 14 | makes DNS requests and checks whether the DNS request went to a non VPN DNS server. 15 | 16 | Discussion: 17 | 18 | Since the test is manual, one could actually replace "Plug/Unplug Ethernet Cable" with any 19 | action they desire. We could have just created a test called TestDNSManualDisruption (or 20 | similar). The reason for specifically naming the test is just to catalog the type of test case 21 | we're interested in. 22 | 23 | If you're working with a VM then you can simulate pulling a cable by just disabling the network 24 | adapter on the host machine, e.g. with VMWare: 25 | 26 | VM Settings->Network Adapter N->Connect Network Adapter. 27 | 28 | Weaknesses: 29 | 30 | Currently uses dig to decide if DNS leaks. This isn't reliable for some VPN providers. Some 31 | providers intercept DNS upstream and change the destination DNS server to their own server. 32 | However dig will still report the server which it originally sent the request to. 33 | 34 | Scenarios: 35 | 36 | No restrictions. 37 | 38 | TODO: 39 | 40 | Automate this test when using a VM. We should be able to disconnect/reconnect the guest 41 | programmatically. 42 | ''' 43 | 44 | def __init__(self, devices, parameters): 45 | super().__init__(DisrupterCable, devices, parameters) 46 | -------------------------------------------------------------------------------- /desktop_local_tests/test_packet_capture_manual.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case import LocalPacketCaptureTestCase 2 | from xv_leak_tools.log import L 3 | from xv_leak_tools.manual_input import message_and_await_enter 4 | 5 | class TestPacketCaptureManual(LocalPacketCaptureTestCase): 6 | 7 | '''Summary: 8 | 9 | Test whether traffic leaks outside of the VPN tunnel during regular operation of the VPN. 10 | 11 | Details: 12 | 13 | This test connects to the VPN then starts packet capture to monitor all outgoing traffic from 14 | the device. It then checks to see if any traffic leaked outside of the VPN. The test allows 15 | the user an arbitrary window in which to generate traffic and thus is inherently a manual test. 16 | 17 | The test is almost identical to TestPacketCaptureVanilla but allows for indefinite manual 18 | control of the machine rather than a timed wait. 19 | 20 | Discussion: 21 | 22 | It is useful for investigating scenarios which might leak. If a leak is found a more specific 23 | test should be made. 24 | 25 | The test is a basic template for all packet capture based tests. The manual step can in 26 | principal be replaced by any form of traffic generation. 27 | 28 | Weaknesses: 29 | 30 | Packet capture tests can be noisy. Traffic can be detected as a leak but in actual fact may not 31 | be. For example, traffic might go to a server owned by the VPN provider to re-establish 32 | connections. In general this test is best used for manual exploring leaks rather than for 33 | automation. 34 | 35 | Scenarios: 36 | 37 | No restrictions. 38 | ''' 39 | 40 | def test_with_packet_capture(self): 41 | L.describe('Generate traffic') 42 | message_and_await_enter( 43 | "Generate traffic however you want. Packet capture will continue in the background. " 44 | "When you're done press enter to check if anything leaked.") 45 | -------------------------------------------------------------------------------- /docs/setting_up_macos.md: -------------------------------------------------------------------------------- 1 | # MacOS 2 | 3 | ## Homebrew 4 | 5 | * Install homebrew: https://brew.sh/ 6 | * `brew install python3 geckodriver chromedriver` 7 | 8 | ## Checkout xv\_leak\_tools\_internal 9 | 10 | Simply `git clone` this repo to a location of your choice. We'll refer to the `python` subfolder in 11 | that location as `$LEAK_TOOLS_ROOT`. 12 | 13 | ## Setup python 14 | 15 | Run `./setup_python.sh $VIRTUALENV_LOCATION` where `$VIRTUALENV_LOCATION` is the directory where you 16 | want the virtualenv to be created, e.g. 17 | 18 | ``` 19 | ./setup_python.sh ~/xv_leak_testing_python 20 | ``` 21 | 22 | You can now source the `activate` script as a shortcut to activating `virtualenv` with this version 23 | of python. 24 | 25 | > TODO: Consider adding pyobjc directly into the codebase. It looks like it's not updated often. 26 | 27 | ## Install VPN Applications 28 | 29 | Install ExpressVPN and any other applications you want to test. No specific steps related to leak 30 | testing are required. 31 | 32 | ## Install Browsers and Webdrivers 33 | 34 | Ensure that Chrome, Firefox, Opera and Safari are installed. 35 | 36 | In order for selenium to control various browsers some extra steps are required. 37 | 38 | > Note that Chrome and Firefox are covered by the `brew` steps above. 39 | 40 | ### Opera 41 | 42 | You need to copy the opera driver into a folder in your `PATH` variable. It can be downloaded from 43 | https://github.com/operasoftware/operachromiumdriver/releases. The easiest thing to do is just copy 44 | it into `/usr/local/sbin`. 45 | 46 | ### Safari 47 | 48 | Safari doesn't need an extra driver, but permissions are required to drive the browser. Go to 49 | Safari->Preferences->Advanced and check "Show Develop in Menu Bar". Then go to the Develop dropdown 50 | in the menu bar and check "Allow Remote Automation". 51 | 52 | ## Install Torrent Clients 53 | 54 | You can choose which torrent clients you want to test. We have tested 55 | 56 | * Transmission 57 | * uTorrent 58 | -------------------------------------------------------------------------------- /desktop_local_tests/windows/test_windows_packet_capture_disrupt_reorder_adapters.py: -------------------------------------------------------------------------------- 1 | from desktop_local_tests.local_packet_capture_test_case_with_disrupter import LocalPacketCaptureTestCaseWithDisrupter 2 | from desktop_local_tests.windows.windows_reorder_adapters_disrupter import WindowsReorderAdaptersDisrupter 3 | 4 | class TestWindowsPacketCaptureDisruptReorderAdapters(LocalPacketCaptureTestCaseWithDisrupter): 5 | 6 | '''Summary: 7 | 8 | Tests whether traffic leaving the user's device leaks outside of the VPN tunnel when the adapter 9 | order is changed. 10 | 11 | Details: 12 | 13 | This test will connect to VPN then swap the priority of the primary and secondary network 14 | adapters. The test looks for leaking traffic once the interface has been disabled. 15 | 16 | Discussion: 17 | 18 | It's not 100% clear if, in the real world, adapters can change their order without user 19 | involvement. It is still however a good stress test of the application. 20 | 21 | On Windows adapter order is determined by the interface metric. It can be manually set but 22 | otherwise it is determined by the system by deciding how "good" an adapter is, e.g. what is the 23 | throughput. In theory that means metrics can change dynamically. 24 | 25 | Weaknesses: 26 | 27 | Packet capture tests can be noisy. Traffic can be detected as a leak but in actual fact may not 28 | be. For example, traffic might go to a server owned by the VPN provider to re-establish 29 | connections. In general this test is best used for manual exploring leaks rather than for 30 | automation. 31 | 32 | Scenarios: 33 | 34 | Requires two active adapters. 35 | 36 | TODO: 37 | 38 | Consider a variant which changes the network "Location". This is much more likely to be 39 | something a user might do. 40 | ''' 41 | 42 | def __init__(self, devices, parameters): 43 | super().__init__(WindowsReorderAdaptersDisrupter, devices, parameters) 44 | --------------------------------------------------------------------------------