From 2a165380bb7214afdcfd95b74dce83660bad53ff Mon Sep 17 00:00:00 2001 From: Peter Surda Date: Sat, 16 Nov 2019 11:52:36 +0100 Subject: [PATCH] Restrict outbound connections on network groups Logic borrowed from bitcoin, see CNetAddr::GetGroup() in src/netaddress.cpp Simplified, so may not work fully identically but for our purposes it's good enough. Won't connect to more than one host from a /16 subnet on IPv4 and a /32 subnet on IPv6. --- src/network/bmproto.py | 2 ++ src/network/connectionpool.py | 14 ++++++++++++ src/network/tcp.py | 1 + src/protocol.py | 28 ++++++++++++++++++++++++ src/tests/test_networkgroup.py | 39 ++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 src/tests/test_networkgroup.py diff --git a/src/network/bmproto.py b/src/network/bmproto.py index bf0b5742..11e96fd6 100644 --- a/src/network/bmproto.py +++ b/src/network/bmproto.py @@ -71,6 +71,8 @@ class BMProto(AdvancedDispatcher, ObjectTracker): # packet/connection from a local IP self.local = False self.pendingUpload = RandomTrackingDict() + # canonical identifier of network group + self.network_group = None def bm_proto_reset(self): """Reset the bitmessage object parser""" diff --git a/src/network/connectionpool.py b/src/network/connectionpool.py index 654b74a1..6264191d 100644 --- a/src/network/connectionpool.py +++ b/src/network/connectionpool.py @@ -229,6 +229,7 @@ class BMConnectionPool(object): def loop(self): # pylint: disable=too-many-branches,too-many-statements """Main Connectionpool's loop""" + # pylint: disable=too-many-locals # defaults to empty loop if outbound connections are maxed spawnConnections = False acceptConnections = True @@ -297,6 +298,19 @@ class BMConnectionPool(object): # don't connect to self if chosen in state.ownAddresses: continue + # don't connect to the hosts from the same + # network group, defense against sibyl attacks + host_network_group = protocol.network_group( + chosen.host) + same_group = False + for j in self.outboundConnections.values(): + if host_network_group == j.network_group: + same_group = True + if chosen.host == j.destination.host: + knownnodes.decreaseRating(chosen) + break + if same_group: + continue try: if chosen.host.endswith(".onion") and Proxy.onion_proxy: diff --git a/src/network/tcp.py b/src/network/tcp.py index 97b00784..31d20dea 100644 --- a/src/network/tcp.py +++ b/src/network/tcp.py @@ -84,6 +84,7 @@ class TCPConnection(BMProto, TLSDispatcher): ) except socket.error: pass # it's probably a hostname + self.network_group = protocol.network_group(self.destination.host) ObjectTracker.__init__(self) # pylint: disable=non-parent-init-called self.bm_proto_reset() self.set_state("bm_header", expectBytes=protocol.Header.size) diff --git a/src/protocol.py b/src/protocol.py index ef101a72..ec8fc9dd 100644 --- a/src/protocol.py +++ b/src/protocol.py @@ -105,6 +105,34 @@ def networkType(host): return 'IPv6' +def network_group(host): + """Canonical identifier of network group + simplified, borrowed from + GetGroup() in src/netaddresses.cpp in bitcoin core""" + if not isinstance(host, str): + return None + network_type = networkType(host) + try: + raw_host = encodeHost(host) + except socket.error: + return host + if network_type == 'IPv4': + decoded_host = checkIPv4Address(raw_host[12:], True) + if decoded_host: + # /16 subnet + return raw_host[12:14] + elif network_type == 'IPv6': + decoded_host = checkIPv6Address(raw_host, True) + if decoded_host: + # /32 subnet + return raw_host[0:12] + else: + # just host, e.g. for tor + return host + # global network type group for local, private, unroutable + return network_type + + def checkIPAddress(host, private=False): """Returns hostStandardFormat if it is a valid IP address, otherwise returns False""" if host[0:12] == '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\xFF': diff --git a/src/tests/test_networkgroup.py b/src/tests/test_networkgroup.py new file mode 100644 index 00000000..76cfb033 --- /dev/null +++ b/src/tests/test_networkgroup.py @@ -0,0 +1,39 @@ +""" +Test for network group +""" +import unittest + + +class TestNetworkGroup(unittest.TestCase): + """ + Test case for network group + """ + def test_network_group(self): + """Test various types of network groups""" + from pybitmessage.protocol import network_group + + test_ip = '1.2.3.4' + self.assertEqual('\x01\x02', network_group(test_ip)) + + test_ip = '127.0.0.1' + self.assertEqual('IPv4', network_group(test_ip)) + + test_ip = '0102:0304:0506:0708:090A:0B0C:0D0E:0F10' + self.assertEqual( + '\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C', + network_group(test_ip)) + + test_ip = 'bootstrap8444.bitmessage.org' + self.assertEqual( + 'bootstrap8444.bitmessage.org', + network_group(test_ip)) + + test_ip = 'quzwelsuziwqgpt2.onion' + self.assertEqual( + test_ip, + network_group(test_ip)) + + test_ip = None + self.assertEqual( + None, + network_group(test_ip))