Skip to content

Commit

Permalink
Remove netifaces dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
pimveldhuisen committed Apr 25, 2016
1 parent 37636db commit 58c483c
Show file tree
Hide file tree
Showing 2 changed files with 45 additions and 87 deletions.
117 changes: 30 additions & 87 deletions dispersy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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)",
Expand Down
15 changes: 15 additions & 0 deletions tests/test_nat_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)

0 comments on commit 58c483c

Please sign in to comment.