New feature: force peer discovery via proxy

Set FORCE_PROXY to non-empty to force peer discovery to go
through the proxy.  See docs/ENVIRONMENT.rst

Wait for an attempt at proxy discovery to be made before beginning
peer discovery.
This commit is contained in:
Neil Booth 2017-04-02 14:59:45 +09:00
parent 77a441ad06
commit a94d320e5d
5 changed files with 42 additions and 22 deletions

View File

@ -251,6 +251,15 @@ some of this.
peer discovery if it notices it is not present in the peer's peer discovery if it notices it is not present in the peer's
returned list. returned list.
* **FORCE_PROXY**
By default peer discovery happens over the clear internet. Set this
to non-empty to force peer discovery to be done via the proxy. This
might be useful if you are running a Tor service exclusively and
wish to keep your IP address private. **NOTE**: in such a case you
should leave **IRC** unset as IRC connections are *always* over the
normal internet.
* **TOR_PROXY_HOST** * **TOR_PROXY_HOST**
The host where your Tor proxy is running. Defaults to *localhost*. The host where your Tor proxy is running. Defaults to *localhost*.
@ -316,7 +325,8 @@ connectivity on IRC:
* **IRC** * **IRC**
Set to anything non-empty to advertise on IRC Set to anything non-empty to advertise on IRC. ElectrumX connects
to IRC over the clear internet, always.
* **IRC_NICK** * **IRC_NICK**

View File

@ -146,6 +146,7 @@ class SocksProxy(util.LoggedClass):
self.errors = 0 self.errors = 0
self.ip_addr = None self.ip_addr = None
self.lost_event = asyncio.Event() self.lost_event = asyncio.Event()
self.tried_event = asyncio.Event()
self.loop = loop or asyncio.get_event_loop() self.loop = loop or asyncio.get_event_loop()
self.set_lost() self.set_lost()
@ -209,6 +210,8 @@ class SocksProxy(util.LoggedClass):
self.logger.info('failed to detect proxy at {}: {}' self.logger.info('failed to detect proxy at {}: {}'
.format(util.address_string(paddress), e)) .format(util.address_string(paddress), e))
self.tried_event.set()
# Failed all ports? # Failed all ports?
if sock is None: if sock is None:
return return

View File

@ -53,6 +53,7 @@ class Env(LoggedClass):
# Peer discovery # Peer discovery
self.peer_discovery = bool(self.default('PEER_DISCOVERY', True)) self.peer_discovery = bool(self.default('PEER_DISCOVERY', True))
self.peer_announce = bool(self.default('PEER_ANNOUNCE', True)) self.peer_announce = bool(self.default('PEER_ANNOUNCE', True))
self.force_proxy = bool(self.default('FORCE_PROXY', False))
self.tor_proxy_host = self.default('TOR_PROXY_HOST', 'localhost') self.tor_proxy_host = self.default('TOR_PROXY_HOST', 'localhost')
self.tor_proxy_port = self.integer('TOR_PROXY_PORT', None) self.tor_proxy_port = self.integer('TOR_PROXY_PORT', None)
# The electrum client takes the empty string as unspecified # The electrum client takes the empty string as unspecified

View File

@ -195,15 +195,7 @@ class PeerSession(JSONSession):
def shutdown_connection(self): def shutdown_connection(self):
self.peer.last_connect = time.time() self.peer.last_connect = time.time()
is_good = not (self.failed or self.bad) is_good = not (self.failed or self.bad)
self.peer_mgr.set_connection_status(self.peer, is_good) self.peer_mgr.set_verification_status(self.peer, self.kind, is_good)
if self.peer.is_tor:
how = 'via {} over Tor'.format(self.kind)
else:
how = 'via {} at {}'.format(self.kind,
self.peer_addr(anon=False))
status = 'verified' if is_good else 'failed to verify'
elapsed = time.time() - self.peer.last_try
self.log_info('{} {} in {:.1f}s'.format(status, how, elapsed))
self.close_connection() self.close_connection()
@ -230,8 +222,8 @@ class PeerManager(util.LoggedClass):
self.peers = set() self.peers = set()
self.onion_peers = [] self.onion_peers = []
self.permit_onion_peer_time = time.time() self.permit_onion_peer_time = time.time()
self.tor_proxy = SocksProxy(env.tor_proxy_host, env.tor_proxy_port, self.proxy = SocksProxy(env.tor_proxy_host, env.tor_proxy_port,
loop=self.loop) loop=self.loop)
self.import_peers() self.import_peers()
def my_clearnet_peer(self): def my_clearnet_peer(self):
@ -462,13 +454,19 @@ class PeerManager(util.LoggedClass):
2) Verifying connectivity of new peers. 2) Verifying connectivity of new peers.
3) Retrying old peers at regular intervals. 3) Retrying old peers at regular intervals.
''' '''
self.ensure_future(self.tor_proxy.auto_detect_loop())
self.connect_to_irc() self.connect_to_irc()
if not self.env.peer_discovery: if not self.env.peer_discovery:
self.logger.info('peer discovery is disabled') self.logger.info('peer discovery is disabled')
return return
# Wait a few seconds after starting the proxy detection loop
# for proxy detection to succeed
self.ensure_future(self.proxy.auto_detect_loop())
await self.proxy.tried_event.wait()
self.logger.info('beginning peer discovery') self.logger.info('beginning peer discovery')
self.logger.info('force use of proxy: {}'.format(self.env.force_proxy))
try: try:
while True: while True:
timeout = self.loop.call_later(WAKEUP_SECS, timeout = self.loop.call_later(WAKEUP_SECS,
@ -516,11 +514,11 @@ class PeerManager(util.LoggedClass):
kind, port = port_pairs[0] kind, port = port_pairs[0]
sslc = ssl.SSLContext(ssl.PROTOCOL_TLS) if kind == 'SSL' else None sslc = ssl.SSLContext(ssl.PROTOCOL_TLS) if kind == 'SSL' else None
if peer.is_tor: if self.env.force_proxy or peer.is_tor:
# Don't attempt an onion connection if we don't have a tor proxy # Only attempt a proxy connection if the proxy is up
if not self.tor_proxy.is_up(): if not self.proxy.is_up():
return return
create_connection = self.tor_proxy.create_connection create_connection = self.proxy.create_connection
else: else:
create_connection = self.loop.create_connection create_connection = self.loop.create_connection
@ -546,10 +544,18 @@ class PeerManager(util.LoggedClass):
if port_pairs: if port_pairs:
self.retry_peer(peer, port_pairs) self.retry_peer(peer, port_pairs)
else: else:
self.set_connection_status(peer, False) self.maybe_forget_peer(peer)
def set_verification_status(self, peer, kind, good):
'''Called when a verification succeeded or failed.'''
if self.env.force_proxy or peer.is_tor:
how = 'via {} over Tor'.format(kind)
else:
how = 'via {} at {}'.format(kind, peer.ip_addr)
status = 'verified' if good else 'failed to verify'
elapsed = time.time() - peer.last_try
self.log_info('{} {} in {:.1f}s'.format(status, how, elapsed))
def set_connection_status(self, peer, good):
'''Called when a connection succeeded or failed.'''
if good: if good:
peer.try_count = 0 peer.try_count = 0
peer.source = 'peer' peer.source = 'peer'

View File

@ -277,9 +277,9 @@ class ElectrumX(SessionBase):
def is_tor(self): def is_tor(self):
'''Try to detect if the connection is to a tor hidden service we are '''Try to detect if the connection is to a tor hidden service we are
running.''' running.'''
tor_proxy = self.controller.peer_mgr.tor_proxy proxy = self.controller.peer_mgr.proxy
peer_info = self.peer_info() peer_info = self.peer_info()
return peer_info and peer_info[0] == tor_proxy.ip_addr return peer_info and peer_info[0] == proxy.ip_addr
async def replaced_banner(self, banner): async def replaced_banner(self, banner):
network_info = await self.controller.daemon_request('getnetworkinfo') network_info = await self.controller.daemon_request('getnetworkinfo')