def test(self): L.describe('Get the public IP addresses before VPN connect') public_ips_before_connect = self.localhost[ 'ip_tool'].all_public_ip_addresses() # Sanity check: We should always have at least one IP address! self.assertNotEmpty(public_ips_before_connect, "Couldn't get public IP addresses") L.info("Public IP addresses before VPN connect are {}".format( public_ips_before_connect)) L.describe('Open and connect the VPN application') self.localhost['vpn_application'].open_and_connect() L.describe('Get the public IP addresses after VPN connect') public_ips_after_connect = self.localhost[ 'ip_tool'].all_public_ip_addresses() L.info("Public IP addresses after VPN connect are {}".format( public_ips_after_connect)) L.describe( 'Expect public IP addresses after VPN connect to have all changed') unchanged_ips = set(public_ips_before_connect).intersection( public_ips_after_connect) self.assertEmpty( unchanged_ips, "These IPs did not get changed after connection to VPN {}".format( unchanged_ips))
def setup(self): if not self._config.get("checkout", False): return # Update the device's git checkout to be the same as our branch and to be at latest revision branch = Git._git_branch() L.info("Updating git repo to branch {} on device {}".format( branch, self._device.device_id())) connector_helper = ConnectorHelper(self._device) git_root = self._device.config().get('git_root', tools_root()) # TODO: Potentially should clean as well? connector_helper.execute_command([ # This can possibly be done in fewer lines 'cd', git_root, '&&', 'git', 'checkout', branch, '&&', 'git', 'pull', '&&', 'git', 'submodule', 'update', '--init', '--recursive' ])
def detect(self): L.debug("Trying to determine if we're using the l2tp protocol") vpn_info = VPNInfo() # Assume that no-process definitively means no openvpn if not self._l2tp_processes(): L.debug('Not using l2tp') return None L.info("Detected l2tp as the current VPN protocol") vpn_info.vpn_processes = self._l2tp_processes() vpn_info.protocol = 'l2tp' # TODO: Do tunnel_interface vpn_info.vpn_server_ip = self._get_vpn_server_ip() vpn_info.dns_server_ips = [] vpn_info.tunnel_interface = None if vpn_info.vpn_server_ip == ipaddress.ip_address('127.0.0.1'): L.warning( "Got vpn server IP 127.0.0.1. The app is likely using a proxy. Removing this IP" ) vpn_info.vpn_server_ip = None # Prevent anyone who's holding on to this class from using stale data. self._reset() L.debug( "Detected the following info about openvpn: {}".format(vpn_info)) return vpn_info
def _find_primary_adapter(self): adapters = self._device['network_tool'].adapters_in_priority_order() primary_adapter = [ adapter for adapter in adapters if adapter.pingable() ][0] L.info("Primary network adapter is {}".format(primary_adapter.name())) return primary_adapter
def delete_rule(name): L.info("Deleting firewall rule {}".format(name)) cmd = [ 'netsh', 'advfirewall', 'firewall', 'delete', 'rule', "name={}".format(name) ] check_subprocess(cmd)
def open(self): if self._app_path: L.info("Opening VPN application {} (it might take a moment to appear)".format( self._config["name"])) self._device.open_app(self._app_path) else: super().open()
def default_interfaces(self): L.info( "No interfaces specified for packet capture. Checking config to determine which to use" ) if 'capture_interface' not in self._config: raise XVEx( "Specify the 'capture_interface' in the 'packet_capturer' component config" ) if self._config['capture_interface'] == 'span': interface = self._device.config()['span_interface'] L.info("Using span interface for capture: {}".format(interface)) return [interface] elif self._config['capture_interface'] == 'primary': interface = self._get_primary_interface() L.info("Using primary interface for capture: {}".format(interface)) return [interface] elif self._config['capture_interface'] == 'non-tunnel': interfaces = self._get_non_tunnel_interfaces() L.info("Using non-tunnel interfaces for capture: {}".format( interfaces)) return interfaces elif self._config['capture_interface'] == 'all': interfaces = self._get_all_interfaces() L.info("Using all interfaces for capture: {}".format(interfaces)) return interfaces else: raise XVEx( "Don't know how to handle capture_interface '{}'".format( self._config['capture_interface']))
def block_application(full_path, direction="out"): full_path = windows_real_path(full_path) L.info( "Creating firewall rule to block application {}".format(full_path)) return WindowsAdvFirewall.create_rule("block", program=full_path, direction=direction)
def test(self): L.describe('Open and connect the VPN application') self.localhost['vpn_application'].open_and_connect() self.start_checking_dns() # This is a bit of a hack. If a VPN fails immediately, i.e. just allows custom DNS servers # then we'll know very quickly. Let's check for that before doing anything specific for the # test: time.sleep(2) self.assertIsNone( self._failure, "Managed to send DNS request our own DNS server. Got response {}.\n" "NOTE: This happened BEFORE we even did any custom testing. This means the provider" "doesn't protect against DNS requests being sent to custom servers" .format(self._failure)) # Derived classes can override this with whatever they want. self.test_with_custom_dns() L.describe("Wait {} seconds before stopping DNS tests".format( self._check_period)) timeup = TimeUp(self._check_period) while not timeup: L.info("Running DNS tests for another {} seconds".format( math.ceil(timeup.time_left()))) self.assertIsNone( self._failure, "Managed to send DNS request our own DNS server. Got response {}" .format(self._failure)) time.sleep(1) self.stop_checking_dns()
def set_lan_ip(self, ip): L.info("Setting LAN IP for router to: {}".format(ip)) self._connector_helper.check_command( ['uci', 'set', "network.lan.ipaddr='{}'".format(ip.exploded)]) self._connector_helper.check_command(['uci', 'commit', 'network']) self._connector_helper.check_command(['/etc/init.d/network', 'restart', '&'])
def start_checking_dns(self): self._stop_dns = False L.info("Starting {} background DNS threads".format( self._background_threads)) for _ in range(0, self._background_threads): self._thread_results.append( self._thread_pool.apply_async(self.dns_check_main))
def disrupt(self): L.describe('Find the VPN processes and kill them (not the main application)') pids = self._device['vpn_application'].vpn_processes() self.assertNotEmpty(pids, 'Found no VPN processes. This should not happen') for pid in pids: self._device.kill_process(pid) L.info("Killed VPN process (PID {})".format(pid))
def _find_primary_service(self): services = self._device[ 'network_tool'].network_services_in_priority_order() primary_service = [ service for service in services if service.active() ][0] L.info("Primary network service is {}".format(primary_service.name())) return primary_service
def setup(self): L.describe('Disable the primary network service') services = self._device[ 'network_tool'].network_services_in_priority_order() self._primary_service = [ service for service in services if service.active() ][0] self._primary_service.disable() L.info("Disabled service {}".format(self._primary_service.name()))
def stop_traffic_generation(self): L.info("Stopping background DNS lookup threads") for _ in range(0, TestDNSVanillaAggressivePacketCapture.NUM_PROCESSES): self.semaphore.release() # There is no result returned from check_dns, but .get() will propagate any exception # thrown by check_dns, which is what we want. for result in self.results: result.get()
def query_ip_responder(self): try: ips_from_responder = self.localhost['ip_responder'].query() L.info("IP addresses used were {}".format(ips_from_responder)) return ips_from_responder except XVEx as ex: L.info("No response from IP responder. No problem, will continue testing: {}".format( ex)) return set()
def _find_primary_service(self): services = self._device[ 'network_tool'].network_services_in_priority_order() primary_service = [ service for service in services if service.active() ][0] primary_interface = primary_service.interface() L.info("Primary network service is {}, with interface {}".format( primary_service.id(), primary_interface)) return primary_service
def start(self, interfaces=None): if self._capturers: raise XVEx("Packet capturer already running") interfaces = interfaces or self.default_interfaces() for interface in interfaces: L.info('Starting packet capture on interface {}'.format(interface)) self._capturers.append( self._create_go_packet_capture(self._device, interface)) self._capturers[-1].start()
def _check_server(self): # Query the server first and make sure it's up try: self.query(timeout=1) L.info("IP Responder is up and running at {}:{}".format( *self._server_address)) except XVEx as _: raise XVEx( "IP responder server isn't running at {}:{}. Please make sure you start " "it!".format(*self._server_address))
def test(self): services = self.localhost[ 'network_tool'].network_services_in_priority_order() interfaces = [ service.interface() for service in services if service.active() ] self.original_active_services = [ service for service in services if service.active() ] primary_service = self.original_active_services[0] L.describe('Disable all active services') for service in self.original_active_services: service.disable() L.describe('Open and connect the VPN application') self.localhost['vpn_application'].open_and_connect() L.describe('Capture traffic') self.localhost['packet_capturer'].start(interfaces) L.describe('Wait for the VPN application to notice the interruption') self.localhost[ 'vpn_application'].wait_for_connection_interrupt_detection() L.describe('Enable primary network service') primary_service.enable() L.info("Enabled {}".format(primary_service)) L.describe('Reconnect the VPN') # TODO: Let's consider if there's a more general way to do this. Potentially we can make # a manual step here which asks the user to ensure the app reconnects. For automation of # XV we can consider an override which does the appropriate thing. self.localhost['vpn_application'].connect() L.describe('Determine the VPN server IP') vpn_server_ip = self.localhost['vpn_application'].vpn_server_ip() L.info("VPN server IP is {}".format(vpn_server_ip)) L.describe('Stop capturing traffic') self.localhost['packet_capturer'].stop() L.describe('Analyse packets to ensure we saw no traffic leaking') packets = self.localhost['packet_capturer'].get_capture(interfaces) # TODO: Consider only filtering local traffic if LAN block is not on. unmatched = self.traffic_filter.filter_traffic(packets, local=True)[1] unmatched = self.traffic_filter.filter_traffic(unmatched, link_local=True)[1] unmatched = self.traffic_filter.filter_traffic(unmatched, multicast=True)[1] unmatched = self.traffic_filter.filter_traffic(unmatched, dst_ip=True)[1] self.assertEmpty(unmatched, json.dumps(unmatched, indent=2))
def filter_tests(tests, regex): filtered_tests = [] L.info("Filtering tests with regex {}".format(regex)) prog = re.compile(regex) for test in tests: if prog.match(test['name']): filtered_tests.append(test) L.info("Regex matched {}/{} tests".format(len(filtered_tests), len(tests))) return filtered_tests
def block_ip(self, ip): from xv_leak_tools.network.macos.pf_firewall import PFCtl # Delay initialize the PFCtl object to prevent VPN application connect from removing our # reference to the pf firewall. Some VPN apps take full ownership of the firewall which can # mean that the firewall will be disabled unless we initialize here. self._pfctl = PFCtl() L.info("Adding outgoing IP block for {}".format(ip)) self._pfctl.set_leak_test_rules([ "block in quick from {} no state".format(ip), "block out quick to {} no state".format(ip) ])
def unblock_ip(self, ip): if self._pfctl is None: return L.info("Removing outgoing IP block for {}".format(ip)) rules_to_remove = self._block_ip_rules(ip) for rule_to_remove in rules_to_remove: self._current_rules = [ rule for rule in self._current_rules if rule != rule_to_remove ] self._pfctl.set_rules(self._current_rules)
def block_ip(self, ip): from xv_leak_tools.network.macos.pf_firewall import PFCtl L.info("Adding outgoing IP block for {}".format(ip)) # Delay initialize the PFCtl object to prevent VPN application connect from removing our # reference to the pf firewall. Some VPN apps take full ownership of the firewall which can # mean that the firewall will be disabled unless we initialize here. if self._pfctl is None: self._pfctl = PFCtl() self._current_rules += MacOSFirewall._block_ip_rules(ip) self._pfctl.set_rules(self._current_rules)
def setup(self): # TODO: This should really be in the network config steps. L.describe('Ensure there are two active network services') services = self._device['network_tool'].network_services_in_priority_order() active_services = [service for service in services if service.active()] self.assertGreaterEqual( len(active_services), 2, "Need two active network services to run this test. Only the following are " "active: {}".format(active_services)) L.describe('Disable the primary network service') self._primary_service = active_services[0] self._primary_service.disable() L.info("Disabled service {}".format(self._primary_service.name()))
def test(self): L.describe('Find all known DNS servers before connecting to VPN') dns_servers_before_connect = self.localhost['dns_tool'].known_servers() L.info( "All known DNS servers are: {}".format(dns_servers_before_connect)) L.describe('Open and connect the VPN application') self.localhost['vpn_application'].open_and_connect() vpn_dns_servers = self.localhost['vpn_application'].dns_server_ips() L.info("VPN DNS servers are: {}".format(vpn_dns_servers)) L.describe('Check DNS server used was a VPN DNS server') DNSHelper(self.localhost['dns_tool']).dns_server_is_vpn_server( dns_servers_before_connect, vpn_dns_servers)
def detect(self): if current_os() != "macos": return None L.debug("Trying to determine if we're using a macOS network extension") vpn_info = VPNInfo() if not self._ne_processes(): L.debug('Not using a network extension') return None L.info("Detected a VPN network extension (unknown protocol)") vpn_info.vpn_processes = self._ne_processes() return vpn_info
def test(self): L.describe('Get the public IP addresses before VPN connect') public_ips_before_connect = self.localhost[ 'ip_tool'].all_public_ip_addresses() # Sanity check: We should always have at least one IP address! self.assertNotEmpty(public_ips_before_connect, "Couldn't get public IP addresses") L.info("Public IP addresses before VPN connect are {}".format( public_ips_before_connect)) L.describe('Open and connect the VPN application') self.localhost['vpn_application'].open_and_connect() self._check_network(20) L.describe('Get the public IP addresses after VPN connect') public_ips_after_connect = self.localhost[ 'ip_tool'].all_public_ip_addresses() timeout = TimeUp(20) while not timeout: public_ips_after_connect = self.localhost[ 'ip_tool'].all_public_ip_addresses() if public_ips_after_connect: break L.debug("Waiting {} s for a public IP address".format( int(timeout.time_left()))) time.sleep(4) self.assertNotEmpty( public_ips_after_connect, "Couldn't get public IP addresses after connecting to the VPN server" ) L.info("Public IP addresses after VPN connect are {}".format( public_ips_after_connect)) L.describe( 'Expect public IP addresses after VPN connect to have all changed') unchanged_ips = set(public_ips_before_connect).intersection( public_ips_after_connect) self.assertEmpty( unchanged_ips, "These IPs did not get changed after connection to VPN {}".format( unchanged_ips))
def _summarise_run(self, test_runs): num_ran = sum(1 for run in test_runs if run.ran()) passes = [run for run in test_runs if run.passed()] failures = [run for run in test_runs if run.failed()] errors = [run for run in test_runs if run.errored()] L.info("{} Test Run Summary {}".format(STARS, STARS)) for pass_ in passes: L.info("PASSED: {}".format(pass_.name())) for fail in failures: L.info("FAILED: {}".format(fail.name())) for error in errors: L.info("ERROR : {}".format(error.name())) L.info("Ran {} / {} tests: {} passes, {} fails, {} errors".format( num_ran, len(test_runs), len(passes), len(failures), len(errors))) return len(failures) + len(errors)
def test(self): L.describe('Get the public IP addresses before VPN connect') public_ips_before_connect = self.localhost[ 'ip_tool'].all_public_ip_addresses() # Sanity check: We should always have at least one IP address! self.assertNotEmpty(public_ips_before_connect, "Couldn't get public IP addresses") L.info("Public IP addresses before VPN connect are {}".format( public_ips_before_connect)) L.describe('Open and connect the VPN application') self.localhost['vpn_application'].open_and_connect() L.describe('Forcibly enable IPv6 on all active network services') self.enable_ipv6_on_all_active_services() L.describe('Find IPs reported by WebRTC') webdriver = self.localhost['webdriver'].driver( self.parameters['browser']) webrtc_ice_checker = WebRTCICEHelper(self.localhost, webdriver, self.parameters['ask_perms'], self.parameters['host_remotely']) webrtc_ips = webrtc_ice_checker.webrtc_ips() L.info("Found the following WebRTC IPs: {}".format(webrtc_ips)) L.describe( 'Check IPv4 addresses reported by WebRTC are all unknown and IPv6 ' 'addresses are in a different subnet') ipv6_subnets = [ ipaddress.ip_interface((ip, 64)).network for ip in public_ips_before_connect if ip.version == 6 ] for ip in webrtc_ips: self.assertIsNotIn(ip, public_ips_before_connect, "Found public IP {} in ICE IPs".format(ip)) if ip.version == 6: for subnet in ipv6_subnets: self.assertFalse( ip in subnet, "New IPv6 address {} is in {}".format(ip, subnet))