Skip to content

Commit

Permalink
Merge pull request #14 from cculianu/master
Browse files Browse the repository at this point in the history
Version 1.10.1 (21 April 2019)
  • Loading branch information
cculianu authored Apr 21, 2019
2 parents d69245a + 3870c93 commit f877639
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 26 deletions.
13 changes: 13 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@
we feel is too restrictive for normal BCH usage of the server.


Version 1.10.1 (21 April 2019)
==============================

* Added PEER_DISCOVERY_TOR environment variable (default off). If on, then
ElectronX will behave as before this version and always forward/discover
*.onion peers. If off, then it will completely ignore *.onion peers and
never forward them. This policy change has been implemented because of the
ease with which sybil attacks are possible if PEER_DISCOVER_TOR is enabled.
(cculianu)
* Added more stuff to `getenv` rpc command display (cculinau)
* Optimized the ban handling a little bit to refuse peers earlier in the
lifecycle of peer_discovery if a host is banned. (cculianu)
Version 1.10.0 (14 April 2019)
==============================

Expand Down
6 changes: 3 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
VERSION="ElectronX 1.10.0"
VERSION="ElectronX 1.10.1"

# -- Project information -----------------------------------------------------

project = 'ElectronX'
copyright = '2016-2018, Neil Booth'
author = 'Neil Booth'
copyright = '2016-2018, Neil Booth; 2019, Electron Cash LLC'
author = 'Neil Booth & Electron Cash LLC'

# The full version including branding
release = VERSION
Expand Down
14 changes: 14 additions & 0 deletions docs/environment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,20 @@ some of this.
discovery is disabled and the server will only return itself in the
peers list.

.. envvar:: PEER_DISCOVERY_TOR

This environment variable is case-insensitive and defaults to
``off`` (or ``0``). It is identical to PEER_DISCOVERY except it affects
*.onion peers only.
The default is ``off`` because of the sybil attack vector this represents.

If ``on``, then ElectrumX will accept peers with *.onion hostnames and attempt
to verify them, and forward its discoveries to its peers.
If ``off`` (the default), then ElectrumX will never discover or announce any
*.onion peers, and will pretend they simply do not exist.
.. envvar:: PEER_ANNOUNCE

Set this environment variable to empty to disable announcing itself.
Expand Down
2 changes: 1 addition & 1 deletion electrumx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 'ElectronX 1.10.0'
version = 'ElectronX 1.10.1'
version_short = version.split()[-1]

from electrumx.server.controller import Controller
Expand Down
37 changes: 37 additions & 0 deletions electrumx/server/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ def __init__(self, coin=None):
self.force_proxy = self.boolean('FORCE_PROXY', False)
self.tor_proxy_host = self.default('TOR_PROXY_HOST', 'localhost')
self.tor_proxy_port = self.integer('TOR_PROXY_PORT', None)
self.peer_discovery_tor = self.peer_discovery_tor_parse()
# The electrum client takes the empty string as unspecified
self.donation_address = self.default('DONATION_ADDRESS', '')
# Server limits to help prevent DoS
Expand Down Expand Up @@ -177,3 +178,39 @@ def peer_discovery_enum(self):
return self.PD_SELF
else:
return self.PD_ON

def peer_discovery_tor_parse(self):
tpd = self.default('PEER_DISCOVERY_TOR', 'off').strip().lower() # if 'on', we forward .onion peers we hear about. otherwise off (due to sybil attack issues)
if tpd in ('on', 'yes', 'true', '1', 'enabled', 'enable'):
return True
return False


def get_info(self):
''' Used by rpc_getenv to get all the env vars we are using. '''
env = {}
for name in dir(self).copy():
if name.startswith('_'):
continue
value = getattr(self, name, None)
if not isinstance(value, (str, int, float, bool)):
continue
if isinstance(value, bool):
value = "1" if value else "" # Map bools to non-empty/empty strings to match how they are parsed
env[name.upper()] = value
# now mogrify some values
env['PEER_DISCOVERY'] = {
self.PD_ON : "on", self.PD_OFF : "off", self.PD_SELF : "self"
}.get(self.peer_discovery, 'on')
env['PEER_DISCOVERY_TOR'] = "on" if self.peer_discovery_tor else "off"
clear_id = self.clearnet_identity()
tor_id = self.tor_identity(clear_id)
if tor_id is not None:
env['REPORT_HOST_TOR'] = str(tor_id.host)
env['REPORT_TCP_PORT_TOR'] = str(tor_id.tcp_port)
env['REPORT_SSL_PORT_TOR'] = str(tor_id.ssl_port)
if clear_id is not None:
env['REPORT_HOST'] = str(clear_id.host)
env['REPORT_TCP_PORT'] = str(clear_id.tcp_port)
env['REPORT_SSL_PORT'] = str(clear_id.ssl_port)
return env
48 changes: 38 additions & 10 deletions electrumx/server/peers.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class BannedPeer(BadPeerError):
class DupePeer(BadPeerError):
pass

class TorPeerDiscoveryDisabled(BadPeerError):
pass

def assert_good(message, result, instance):
if not isinstance(result, instance):
raise BadPeerError(f'{message} returned bad result type '
Expand Down Expand Up @@ -121,12 +124,15 @@ def _features_to_register(self, peer, remote_peers):
return my.features

def _permit_new_onion_peer(self):
'''Accept a new onion peer only once per random time interval.'''
'''Accept a new onion peer only once per random time interval, and
only iff self.env.peer_discovery_tor is True.'''
if not self.env.peer_discovery_tor:
return False, 'tor peer discovery disabled'
now = time.time()
if now < self.permit_onion_peer_time:
return False
return False, 'rate limiting'
self.permit_onion_peer_time = now + random.randrange(0, 1200)
return True
return True, 'ok'

async def _import_peers(self):
'''Import hard-coded peers from a file or the coin defaults.'''
Expand Down Expand Up @@ -166,7 +172,18 @@ async def _note_peers(self, peers, limit=2, check_ports=False,
new_peers = []
match_set = self.peers.copy()
for peer in peers:
if not peer.is_public or (peer.is_tor and not self.proxy):
if not peer.is_public:
continue
if peer.is_tor:
if not self.proxy:
# silently ignore tor if no tor proxy
continue
if not self.env.peer_discovery_tor:
self.logger.warning(f'refusing peer "{peer}" (tor peer discovery is disabled)')
continue
banned_suffix = self.session_mgr.does_peer_match_hostname_ban(peer)
if banned_suffix:
self.logger.warning(f'refusing peer "{peer}" (banned: {banned_suffix})')
continue

matches = peer.matches(match_set)
Expand Down Expand Up @@ -210,6 +227,10 @@ async def _monitor_peer(self, peer):
peer.retry_event.clear()

async def _should_drop_peer(self, peer):
banned_suffix = self.session_mgr.does_peer_match_hostname_ban(peer)
if banned_suffix:
self.logger.warning(f'Peer {peer} matches banned hostname suffix {banned_suffix} (dropping)')
return True
peer.try_count += 1
is_good = False
for kind, port, family in peer.connection_tuples():
Expand Down Expand Up @@ -247,6 +268,9 @@ async def _should_drop_peer(self, peer):
except BannedPeer as e:
self.logger.error(f'{peer_text} is banned: ({e})')
return True # It's banend, so should drop it
except TorPeerDiscoveryDisabled as e:
self.logger.error(f'{peer_text} tor discovery: ({e})')
return True # Cut off tor peers from the get-go
except BadPeerError as e:
self.logger.error(f'{peer_text} marking bad: ({e})')
peer.mark_bad()
Expand Down Expand Up @@ -304,6 +328,8 @@ async def _verify_peer(self, session, peer):
dupes = self._dupes_for_peer(peer.ip_addr)
if dupes:
raise DupePeer(f'Peer {peer} is a dupe! {len(dupes)} other peers with IP {peer.ip_addr} were found!')
elif not self.env.peer_discovery_tor and peer.host not in [i.host for i in self.env.identities if i.nick_suffix == '_tor']:
raise TorPeerDiscoveryDisabled(f'Tor peer discovery is disabled: {peer.host}')
banned_suffix = self.session_mgr.does_peer_match_hostname_ban(peer)
if banned_suffix:
raise BannedPeer(f'Peer matches banned hostname suffix {banned_suffix}')
Expand Down Expand Up @@ -462,8 +488,7 @@ async def on_add_peer(self, features, source_info):
peer = peers[0]
host = peer.host
if peer.is_tor:
permit = self._permit_new_onion_peer()
reason = 'rate limiting'
permit, reason = self._permit_new_onion_peer() # check if peer_discovery_tor is enabled and also if sufficient time has passed since we added tor peers (we rate limit tor even if discovery is enabled)
else:
getaddrinfo = asyncio.get_event_loop().getaddrinfo
try:
Expand Down Expand Up @@ -524,11 +549,14 @@ def on_peers_subscribe(self, is_tor):
random.shuffle(bucket_peers)
peers.update(bucket_peers[:2])

# Add up to 20% onion peers (but up to 10 is OK anyway)
random.shuffle(onion_peers)
max_onion = 50 if is_tor else max(10, len(peers) // 4)
if self.env.peer_discovery_tor:
# Add up to 20% onion peers (but up to 10 is OK anyway)
random.shuffle(onion_peers)
max_onion = 50 if is_tor else max(10, len(peers) // 4)

peers.update(onion_peers[:max_onion])
peers.update(onion_peers[:max_onion])
elif onion_peers:
self.logger.info(f'Not forwarding {len(onion_peers)} .onion peers (tor peer discovery is disabled)')

return [peer.to_tuple() for peer in peers]

Expand Down
12 changes: 1 addition & 11 deletions electrumx/server/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,17 +691,7 @@ async def rpc_getinfo(self):

async def rpc_getenv(self):
'''Return all the env vars used to configure the server. '''
env = {}
for name in dir(self.env).copy():
if name.startswith('_'):
continue
value = getattr(self.env, name, None)
if not isinstance(value, (str, int, float, bool)):
continue
if isinstance(value, bool):
value = "1" if value else "" # Map bools to non-empty/empty strings to match how they are parsed
env[name.upper()] = value
return env
return self.env.get_info()

async def rpc_groups(self):
'''Return statistics about the session groups.'''
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import setuptools
version = '1.10.0'
version = '1.10.1'

setuptools.setup(
name='electronX',
Expand Down

0 comments on commit f877639

Please sign in to comment.