diff --git a/dispersy.py b/dispersy.py index a026cfa5..5d228874 100644 --- a/dispersy.py +++ b/dispersy.py @@ -42,11 +42,10 @@ from hashlib import sha1 from itertools import groupby, count from pprint import pformat -from socket import inet_aton, error as socket_error +from socket import inet_aton, socket, AF_INET, SOCK_DGRAM from struct import unpack_from from time import time -import netifaces from twisted.internet import reactor from twisted.internet.defer import maybeDeferred, gatherResults from twisted.internet.task import LoopingCall @@ -133,9 +132,7 @@ def __init__(self, endpoint, working_directory, database_filename=u"dispersy.db" self._connection_type = u"unknown" # our LAN and WAN addresses - self._local_interfaces = list(self._get_interface_addresses()) - interface = self._guess_lan_address(self._local_interfaces) - self._lan_address = ((interface.address if interface else "0.0.0.0"), 0) + self._lan_address = (self._get_lan_address(), 0) self._wan_address = ("0.0.0.0", 0) self._wan_address_votes = defaultdict(set) self._logger.debug("my LAN address is %s:%d", self._lan_address[0], self._lan_address[1]) @@ -158,85 +155,16 @@ def __init__(self, endpoint, working_directory, database_filename=u"dispersy.db" # statistics... self._statistics = DispersyStatistics(self) - - @staticmethod - def _get_interface_addresses(): - """ - Yields Interface instances for each available AF_INET interface found. - - An Interface instance has the following properties: - - name (i.e. "eth0") - - address (i.e. "10.148.3.254") - - netmask (i.e. "255.255.255.0") - - broadcast (i.e. "10.148.3.255") - """ - class Interface(object): - - def __init__(self, name, address, netmask, broadcast): - self.name = name - self.address = address - self.netmask = netmask - self.broadcast = broadcast - self._l_address, = unpack_from(">L", inet_aton(address)) - self._l_netmask, = unpack_from(">L", inet_aton(netmask)) - - def __contains__(self, address): - assert isinstance(address, str), type(address) - l_address, = unpack_from(">L", inet_aton(address)) - return (l_address & self._l_netmask) == (self._l_address & self._l_netmask) - - def __str__(self): - return "<{self.__class__.__name__} \"{self.name}\" addr:{self.address} mask:{self.netmask}>".format(self=self) - - def __repr__(self): - return "<{self.__class__.__name__} \"{self.name}\" addr:{self.address} mask:{self.netmask}>".format(self=self) - - try: - for interface in netifaces.interfaces(): - try: - addresses = netifaces.ifaddresses(interface) - - except ValueError: - # some interfaces are given that are invalid, we encountered one called ppp0 - pass - - else: - for option in addresses.get(netifaces.AF_INET, []): - try: - yield Interface(interface, option.get("addr"), option.get("netmask"), option.get("broadcast")) - - except TypeError: - # some interfaces have no netmask configured, causing a TypeError when - # trying to unpack _l_netmask - pass - except OSError, e: - logger = logging.getLogger("dispersy") - logger.exception("failed to check network interfaces, error was: %r", e) - - def _guess_lan_address(self, interfaces, default=None): + def _get_lan_address(self): """ - Chooses the most likely Interface instance out of INTERFACES to use as our LAN address. - - INTERFACES can be obtained from _get_interface_addresses() - DEFAULT is used when no appropriate Interface can be found + # Get the local ip address by connecting to a (random) internet ip + :return: the local ip address """ - assert isinstance(interfaces, list), type(interfaces) - blacklist = ["127.0.0.1", "0.0.0.0", "255.255.255.255"] - - # prefer interfaces where we have a broadcast address - for interface in interfaces: - if interface.broadcast and interface.address and not interface.address in blacklist: - self._logger.debug("%s", interface) - return interface - - # Exception for virtual machines/containers - for interface in interfaces: - if interface.address and not interface.address in blacklist: - self._logger.debug("%s", interface) - return interface - - self._logger.error("Unable to find our public interface!") - return default + s = socket(AF_INET, SOCK_DGRAM) + s.connect(("192.0.2.0", 80)) # TEST-NET-1, guaranteed to not be connected => no callbacks + local_ip = s.getsockname()[0] + s.close() + return local_ip @property def working_directory(self): @@ -716,6 +644,23 @@ def wan_address_unvote(self, voter): del self._wan_address_votes[vote] return vote + def address_is_lan(self, address): + if address == self._get_lan_address(): + return True + else: + lan_subnets = (("192.168.0.0", 16), + ("172.16.0.0", 12), + ("10.0.0.0", 8)) + return any(self.address_in_subnet(address, subnet) for subnet in lan_subnets) + + def address_in_subnet(self, address, subnet): + address = unpack_from(">L", inet_aton(address))[0] + (subnet_main, netmask) = subnet + subnet_main = unpack_from(">L", inet_aton(subnet_main))[0] + address >>= 32-netmask + subnet_main >>= 32-netmask + return address == subnet_main + def wan_address_vote(self, address, voter): """ Add one vote and possibly re-determine our wan address. @@ -778,7 +723,7 @@ def set_connection_type(connection_type): # ignore votes from voters that we know are within any of our LAN interfaces. these voters # can not know our WAN address - if any(voter.sock_addr[0] in interface for interface in self._local_interfaces): + if self.address_is_lan(voter.sock_addr[0]): self._logger.debug("ignore vote for %s from %s (voter is within our LAN)", address, voter.sock_addr) return @@ -795,9 +740,7 @@ def set_connection_type(connection_type): if len(self._wan_address_votes[address]) > len(self._wan_address_votes.get(self._wan_address, ())): if set_wan_address(address): # refresh our LAN address(es), perhaps we are running on a roaming device - self._local_interfaces = list(self._get_interface_addresses()) - interface = self._guess_lan_address(self._local_interfaces) - lan_address = ((interface.address if interface else "0.0.0.0"), self._lan_address[1]) + lan_address = self._get_lan_address() if not is_valid_address(lan_address): lan_address = (self._wan_address[0], self._lan_address[1]) set_lan_address(lan_address) @@ -1622,7 +1565,7 @@ def estimate_lan_and_wan_addresses(self, sock_addr, lan_address, wan_address): """ assert is_valid_address(sock_addr), sock_addr - if any(sock_addr[0] in interface for interface in self._local_interfaces): + if self.address_is_lan(sock_addr[0]): # is SOCK_ADDR is on our local LAN, hence LAN_ADDRESS should be SOCK_ADDR if sock_addr != lan_address: self._logger.debug("estimate someones LAN address is %s (LAN was %s, WAN stays %s)", diff --git a/tests/test_nat_detection.py b/tests/test_nat_detection.py index 56844a1e..ac2469d7 100644 --- a/tests/test_nat_detection.py +++ b/tests/test_nat_detection.py @@ -111,6 +111,20 @@ def test_symmetric_vote(self): class TestAddressEstimation(DispersyTestFunc): + def test_address_in_lan_function(self): + # Positive cases: + assert self._dispersy.address_is_lan("192.168.1.5") + assert self._dispersy.address_is_lan("10.42.42.42") + assert self._dispersy.address_is_lan("192.168.0.7") + assert self._dispersy.address_is_lan("172.31.255.255") + #Negative cases: + self.assertFalse(self._dispersy.address_is_lan("192.169.1.5")) + self.assertFalse(self._dispersy.address_is_lan("11.42.42.42")) + self.assertFalse(self._dispersy.address_is_lan("192.0.0.7")) + self.assertFalse(self._dispersy.address_is_lan("172.32.0.0")) + self.assertFalse(self._dispersy.address_is_lan("123.123.123.123")) + self.assertFalse(self._dispersy.address_is_lan("42.42.42.42")) + def test_estimate_addresses_within_LAN(self): """ Tests the estimate_lan_and_wan_addresses method while NODE and OTHER are within the same LAN. @@ -155,3 +169,4 @@ def test_estimate_addresses_within_LAN(self): self.assertEqual(candidate.sock_addr, node.lan_address) self.assertEqual(candidate.lan_address, node.lan_address) self.assertEqual(candidate.wan_address, incorrect_WAN) +