├── 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 |
--------------------------------------------------------------------------------