Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Remove the netifaces dependency #471

Open
wants to merge 1 commit into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 29 additions & 109 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
from socket import inet_aton, socket, AF_INET, SOCK_DGRAM, error as socket_error
from struct import unpack_from
from time import time

import netifaces
from twisted.internet import reactor
from twisted.internet.defer import maybeDeferred, gatherResults, inlineCallbacks, returnValue
from twisted.internet.task import LoopingCall
Expand Down Expand Up @@ -134,8 +133,7 @@ def __init__(self, endpoint, working_directory, database_filename=u"dispersy.db"
self._connection_type = u"unknown"

# our LAN and WAN addresses
self._netifaces_failed = False
self._lan_address = self._get_lan_address(True)
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,110 +156,16 @@ def __init__(self, endpoint, working_directory, database_filename=u"dispersy.db"
# statistics...
self._statistics = DispersyStatistics(self)

@staticmethod
def _get_interface_addresses():
def _get_lan_address(self):
"""
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")
# Get the local ip address by connecting to a (random) internet ip
:return: the local ip address
"""
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:
# On Windows netifaces currently returns IP addresses as unicode,
# and on *nix it returns str. So, we convert any unicode objects to str.
unicode_to_str = lambda s: s.encode('utf-8') if isinstance(s, unicode) else s
yield Interface(interface,
unicode_to_str(option.get("addr")),
unicode_to_str(option.get("netmask")),
unicode_to_str(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.warning("failed to check network interfaces, error was: %r", e)

def _address_is_lan(self, address):
if self._netifaces_failed:
return address_is_lan_without_netifaces(address)
else:
return any(address in interface for interface in self._local_interfaces)

def _get_lan_address(self, bootstrap=False):
"""
Attempt to get the newest lan ip of this machine, preferably with netifaces, but use the fallback if it fails
:return: lan address
"""
if self._netifaces_failed:
return (get_lan_address_without_netifaces(), self._lan_address[1])
else:
self._local_interfaces = list(self._get_interface_addresses())
interface = self._guess_lan_address(self._local_interfaces)
return (interface.address if interface else get_lan_address_without_netifaces()), \
(0 if bootstrap else self._lan_address[1])

def _guess_lan_address(self, interfaces, default=None):
"""
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
"""
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.warning("Unable to find our public interface!")
self._netifaces_failed = True
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 @@ -743,6 +647,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 @@ -805,8 +726,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 self._address_is_lan(voter.sock_addr[0]):
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 Down Expand Up @@ -1648,7 +1568,7 @@ def estimate_lan_and_wan_addresses(self, sock_addr, lan_address, wan_address):
"""
assert is_valid_address(sock_addr), sock_addr

if self._address_is_lan(sock_addr[0]):
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
16 changes: 14 additions & 2 deletions tests/test_nat_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ def test_address_in_lan_function(self):
self.assertFalse(address_is_lan_without_netifaces("123.123.123.123"))
self.assertFalse(address_is_lan_without_netifaces("42.42.42.42"))

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 @@ -167,5 +181,3 @@ 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)