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 test(self): public_ips_before_connect = self.localhost['ip_tool'].all_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.localhost['ip_responder'].start() # Derived classes can override this with whatever they want. self.test_with_ip_responder() L.describe("Wait {} seconds before stopping ip responder".format(self._check_period)) timeup = TimeUp(self._check_period) while not timeup: L.info("Running IP responder tests for another {} seconds".format( math.ceil(timeup.time_left()))) ips_from_responder = self.query_ip_responder() ip_intersection = ips_from_responder.intersection(public_ips_before_connect) self.assertEmpty( ip_intersection, "The following public IPs were detected {}".format(ip_intersection)) # Don't over query the responder. The background thread will still be spamming the # server so we won't miss anything. time.sleep(5) self.localhost['ip_responder'].stop() # Get rid of the VPN else it can affect our ability to talk to the server self.localhost['vpn_application'].disconnect() self.localhost['vpn_application'].close() # Some VPN apps take a while to close which means, if they implement a firewall, the # responder can't be reached. Allow some time for the network to come back in this situation timeup = TimeUp(20) while not timeup: ips_from_responder = self.query_ip_responder() if ips_from_responder: break if timeup: raise XVEx("Couldn't do final query of IP responder to get IPs. Got no response.") if not ips_from_responder: L.info("No IP addresses detected") else: L.info("IP addresses used were {}".format(ips_from_responder)) ip_intersection = ips_from_responder.intersection(public_ips_before_connect) self.assertEmpty( ip_intersection, "The following public IPs were detected: {}".format(ip_intersection))
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 test(self): L.info("Using disrupter: {}".format(self.disrupter)) 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)) self._check_network(time_limit=20) L.describe('Check DNS server used was a VPN DNS server') self.dns_helper.dns_server_is_vpn_server(dns_servers_before_connect, vpn_dns_servers) L.describe("Create disruption...") self.disrupter.create_disruption() L.describe( "Do some DNS lookups for the next {} seconds and check the DNS server used is a VPN " "DNS Server".format(self._check_period)) timeup = TimeUp(self._check_period) message_time = self._check_period while not timeup: if timeup.time_left() < message_time: L.info("Doing DNS lookup tests for another {} seconds".format( math.ceil(timeup.time_left()))) message_time = message_time - 5 try: # Max timeout for each DNS lookup is 2 seconds. timeout = min(timeup.time_left(), 2) self.dns_helper.dns_server_is_vpn_server( dns_servers_before_connect, vpn_dns_servers, timeout=timeout) except XVEx as ex: L.warning("DNS lookup failed, assuming no leak: {}".format(ex))
def _check_network(self, time_limit=5): L.info("Checking if there's a network connection") timeup = TimeUp(time_limit) while not timeup: lost = self.localhost['network_tool'].ping('8.8.8.8', count=3, timeout=2) if lost == 3: L.warning( "No network detected. Will try for another {} seconds". format(int(timeup.time_left()))) time.sleep(0.5) elif lost == 0: L.info("Network okay") return else: L.warning("Network detected but there's some packet loss") return raise XVEx("No network connection detected.")
def _check_network(self): L.info("Checking if there's a network connection") timeup = TimeUp(5) while not timeup: lost = self.localhost['network_tool'].ping('8.8.8.8', count=3, timeout=2) if lost == 3: L.warning( "No network detected. Will try for {} seconds.".format( timeup.time_left())) elif lost == 0: L.info("Network okay") return else: L.warning("Network detected but there's some packet loss") return raise XVEx( "No network connection detected. Tests must be started with an active network. " "Do you have any VPN applications open?")
def _get_packets(self): dst = os.path.join(self._device.temp_directory(), self._capture_file) timeup = TimeUp(5) while not timeup: if os.path.exists(dst): break L.warning("Waiting for capture file to be written: {}".format(dst)) time.sleep(1) if not os.path.exists(dst): raise XVEx("Couldn't get capture file from capture device") return object_from_json_file(dst, 'attribute')['data']
def get_magnet_link(self, timeout=10): self._activate_torrent_leak_detection() # Add a loop to wait for the page to load the piece we want # TODO: This might be a good helper function. A wrapper to "wait for element" magnet_link = None timeup = TimeUp(timeout) while not timeup: try: magnet_link = ( self._webdriver.find_element_by_id('torrent_detection'). find_element_by_tag_name('div').find_element_by_tag_name( 'a').get_attribute('href')) break except NoSuchElementException as _: time.sleep(0.5) self.assertIsNotNone(magnet_link, "Torrent detection div didn't load") return magnet_link
def _find_windows_pid(self): # It can take a moment for the PID to register with cygwin. So retry for up to 5 seconds # if it fails. timeup = TimeUp(5) while not timeup: pids = self._device.pgrep("xv_packet_capture.exe") L.debug( "Found the following PIDs matching xv_packet_capture.exe: {}". format(pids)) for pid in pids: cmd_line = self._device.command_line_for_pid(pid) L.debug("Command line for PID {} was: {}".format( pid, cmd_line)) for arg in cmd_line: if self._capture_file in arg: return pid raise XVEx( "Couldn't find PID for xv_packet_capture.exe with capture file {}". format(self._capture_file))
def _wait_for_vm_power(vmx_path): # Hacky trick to make sure VM has power timeup = TimeUp(60) while not timeup: try: if VMWareDeviceDiscoverer._vm_state(vmx_path) != 'running': time.sleep(0.2) continue VMWareDeviceDiscoverer._get_vm_ip(vmx_path) # If we manage to get the IP then we're powered on. This will be unlikely to happen break except XVProcessException as ex: if 'The virtual machine is not powered on' in ex.stdout: time.sleep(0.2) continue # Any other exception means we're powered on break if timeup: raise XVEx("VM never got power: {}".format(vmx_path)) L.verbose('VM now has power')
def query(self, timeout=10): L.debug("IPResponder getting IPs for {}".format(self._token.decode())) timeup = TimeUp(timeout) first_time_around = True while not timeup: try: if not first_time_around: L.verbose("Trying to querying IP responder server again") else: L.verbose("Querying IP responder server") first_time_around = False self._query_sock.sendto(self._token + b'?', self._server_address) return self._receive() except (OSError, XVEx) as ex: # OSError happens if sendto fails (which probably means no network) # XVEx happens if there's no response to receive. L.warning("IP responder query failed: {}".format(ex)) time.sleep(1) raise XVEx("Couldn't query IP Responder server")
def _get_packets(self): # TODO: This highlights a weakness in the framework. I can't pull to my local device # because I don't know where to pull to! Just pulling to a temp file for now src = os.path.join(self._device.temp_directory(), self._capture_file) file_, dst = tempfile.mkstemp(prefix="xv_leak_test_", suffix="_{}".format(self._capture_file)) os.close(file_) os.remove(dst) timeup = TimeUp(5) while not timeup: try: self._device.connector().pull(src, dst) break except XVEx: L.warning( "Waiting for capture file to be written: {}".format(src)) time.sleep(1) if not os.path.exists(dst): raise XVEx("Couldn't get capture file from capture device") packets = object_from_json_file(dst, 'attribute') return packets['data']
def test(self): L.info("Using disrupter: {}".format(self.disrupter)) L.describe('Get public IP before connect') ipv4_addresses_pre_connect = self.localhost[ 'ip_tool'].public_ipv4_addresses() ipv6_addresses_pre_connect = self.localhost[ 'ip_tool'].public_ipv6_addresses() ip_addresses_pre_connect = ipv4_addresses_pre_connect + ipv6_addresses_pre_connect L.info("Public IP addresses before connection were: {}".format( ip_addresses_pre_connect)) L.describe('Open and connect the VPN application') self.localhost['vpn_application'].open_and_connect() L.describe("Create disruption...") self.disrupter.create_disruption() L.describe( "Repeatedly check public IP address for the next {} seconds". format(self._check_period)) timeup = TimeUp(self._check_period) message_time = self._check_period while not timeup: try: if timeup.time_left() < message_time: L.info("Running IP tests for another {} seconds".format( math.ceil(timeup.time_left()))) message_time = message_time - 5 # Max timeout for each IP lookup is 1 second. It needs to be quick in order to # find disruption windows. This will eventually be replaces by something like iperf # but which tracks what IPs packets came from. current_ipv4_addresses = self.localhost[ 'ip_tool'].public_ipv4_addresses(timeout=1) if len(current_ipv4_addresses) != 0: L.debug("Got IPv4 address {}".format( current_ipv4_addresses[0])) # Let's not check against the VPN server IP as it means prompting for user input # for the VPN IP in many cases self.assertIsNotIn( current_ipv4_addresses[0], ip_addresses_pre_connect, "IPv4 address {} used but this is a public IP of this device" .format(current_ipv4_addresses[0])) # Do ipv4 and ipv6 separately to reduce lookup time. Only do IPv6 if the device has # an ipv6 address else it's a waste of time. if len(ipv6_addresses_pre_connect) == 0: continue current_ipv6_addresses = self.localhost[ 'ip_tool'].public_ipv6_addresses(timeout=1) if len(current_ipv6_addresses) != 0: L.debug("Got IPv6 address {}".format( current_ipv6_addresses[0])) # Let's not check against the VPN server IP as it means prompting for user input # for the VPN IP in many cases self.assertIsNotIn( current_ipv6_addresses[0], ipv6_addresses_pre_connect, "IPv6 address {} used but this is a public IP of this device" .format(current_ipv6_addresses[0])) except XVEx as ex: L.warning("IP lookup failed, assuming timeout: {}".format(ex))
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)) self.localhost['torrent_client'].set_client(self.torrent_client) # We're assuming the client is already closed. I think that's ok. if self.torrent_client_preopened: self.localhost['torrent_client'].open() L.describe('Open and connect the VPN application') self.localhost['vpn_application'].open_and_connect() self._webdriver = self.localhost['webdriver'].driver( self.parameters['browser']) ip_leak_helper = IPLeakNetHelper(self._webdriver) L.describe('Open ipleak.net page') ip_leak_helper.load_page() L.describe('Get the torrent magnet link from ipleak.net') magnet_link = ip_leak_helper.get_magnet_link() L.info("Got magnet link {}".format(magnet_link)) L.describe('Add the torrent magnet to the torrent client') self.localhost['torrent_client'].add_torrent(magnet_link) L.describe("Check the reported torrent IPs aren't public") ipv6_subnets = [ ipaddress.ip_interface((ip, 64)).network for ip in public_ips_before_connect if ip.version == 6 ] timeup = TimeUp(20) reported_ips = set() while not timeup: L.info('Checking webpage contents for leaked torrent IPs') reported_ips.update(ip_leak_helper.get_reported_torrent_ips()) for ip in reported_ips: self.assertIsNotIn( ip, public_ips_before_connect, "Torrent tracker found a public IP: {}".format(ip)) if ip.version == 6: for subnet in ipv6_subnets: self.assertFalse( ip in subnet, "IPv6 address {} is in {}".format(ip, subnet)) if reported_ips: L.info( "Found IPs {} but none were public".format(reported_ips)) else: L.info('Found no IPs') # Pause a little each run to give IPs a chance to report. time.sleep(1) if not reported_ips: raise XVEx( "The torrent client didn't report any IP addresses at all")
def discover_device(self, discovery_keys): L.debug('Looking for device with keys {}'.format(discovery_keys)) device = self._inventory_item_for_discovery_keys(discovery_keys) if device is None: return None device['output_directory'] = os.path.join( device['output_root'], self._context['run_directory']) # TODO: Make this more generic. It would be helpful to know what keys are required. # TODO: Consider making snapshots smarter. e.g. revert to LATEST snapshot with a specific # prefix. if 'vmx_path' not in device or 'vm_snapshot' not in device: raise XVEx("VMWare device inventory items need to specify: '{}' and '{}'".format( 'vmx_path', 'vm_snapshot')) vmx_path = device['vmx_path'] vm_snapshot = device['vm_snapshot'] # Check that either the VM is stopped or we're already using it. I'm thinking ahead here to # potentially having tests running in parallel. state = VMWareDeviceDiscoverer._vm_state(vmx_path) if state == 'running' and vmx_path not in self._vms_in_use: raise XVEx( "The VM {} is currently in use by someone else (current state is '{}'')".format( vmx_path, state)) # Always add. It might already be there but we don't care self._vms_in_use.add(vmx_path) if state == 'running': VMWareDeviceDiscoverer._unpause_vm(vmx_path) else: L.debug("Reverting VM to snapshot: {}".format(vm_snapshot)) VMWareDeviceDiscoverer._revert_vm_to_snapshot(vmx_path, vm_snapshot) VMWareDeviceDiscoverer._chown_vm_files(vmx_path) L.debug("Starting VM") VMWareDeviceDiscoverer._start_vm(vmx_path) VMWareDeviceDiscoverer._wait_for_vm_power(vmx_path) # It can take quite a while for IPs to become available. # TODO: Try to improve this. It seems like a VMWare issue. I can ping the machine well # before vmware reports its IPs timeup = TimeUp(60) ip = None while not timeup: try: L.debug('Waiting for VM IP to become available') ip = VMWareDeviceDiscoverer._get_vm_ip(vmx_path) break except XVProcessException as ex: if 'The VMware Tools are not running in the virtual machine' not in ex.stdout and \ 'Unable to get the IP address' not in ex.stdout: raise L.warning("Failed getting IP for VM: {}".format(ex)) time.sleep(5) if ip is None: raise XVEx("Couldn't get IP for VM {}".format(vmx_path)) connector = SimpleSSHConnector( ips=[ip], username=device['username'], ssh_key=device['ssh_key'], ssh_password=device['ssh_password']) return create_device(device['os_name'], device, connector)