diff --git a/lib/socks.py b/lib/socks.py index 86fddb6..d31a5d1 100644 --- a/lib/socks.py +++ b/lib/socks.py @@ -137,44 +137,101 @@ class Socks(util.LoggedClass): class SocksProxy(util.LoggedClass): def __init__(self, host, port, loop=None): - '''Host can be an IPv4 address, IPv6 address, or a host name.''' + '''Host can be an IPv4 address, IPv6 address, or a host name. + Port can be None, in which case one is auto-detected.''' super().__init__() + # Host and port of the proxy self.host = host - self.port = port + self.try_ports = [port, 9050, 9150, 1080] + self.errors = 0 self.ip_addr = None + self.lost_event = asyncio.Event() self.loop = loop or asyncio.get_event_loop() - - async def create_connection(self, protocol_factory, host, port, ssl=None): - '''All arguments are as to asyncio's create_connection method.''' - if self.port is None: - proxy_ports = [9050, 9150, 1080] - else: - proxy_ports = [self.port] - - for proxy_port in proxy_ports: - address = (self.host, proxy_port) - sock = socket.socket() - sock.setblocking(False) - try: - await self.loop.sock_connect(sock, address) - except OSError as e: - if proxy_port == proxy_ports[-1]: - raise - continue - + self.set_lost() + + async def auto_detect_loop(self): + '''Try to detect a proxy at regular intervals until one is found. + If one is found, do nothing until one is lost.''' + while True: + await self.lost_event.wait() + self.lost_event.clear() + tries = 0 + while True: + tries += 1 + log_failure = tries % 10 == 1 + await self.detect_proxy(log_failure=log_failure) + if self.is_up(): + break + await asyncio.sleep(600) + + def is_up(self): + '''Returns True if we have a good proxy.''' + return self.port is not None + + def set_lost(self): + '''Called when the proxy appears lost/down.''' + self.port = None + self.lost_event.set() + + async def connect_via_proxy(self, host, port, proxy_address=None): + '''Connect to a (host, port) pair via the proxy. Returns the + connected socket on success.''' + proxy_address = proxy_address or (self.host, self.port) + sock = socket.socket() + sock.setblocking(False) + try: + await self.loop.sock_connect(sock, proxy_address) socks = Socks(self.loop, sock, host, port) + await socks.handshake() + return sock + except Exception: + sock.close() + raise + + async def detect_proxy(self, host='www.google.com', port=80, + log_failure=True): + '''Attempt to detect a proxy by establishing a connection through it + to the given target host / port pair. + ''' + if self.is_up(): + return + + sock = None + for proxy_port in self.try_ports: + if proxy_port is None: + continue + paddress = (self.host, proxy_port) try: - await socks.handshake() - if self.port is None: - self.ip_addr = sock.getpeername()[0] - self.port = proxy_port - self.logger.info('detected proxy at {} ({})' - .format(util.address_string(address), - self.ip_addr)) + sock = await self.connect_via_proxy(host, port, paddress) break except Exception as e: - sock.close() - raise + if log_failure: + self.logger.info('failed to detect proxy at {}: {}' + .format(util.address_string(paddress), e)) + + # Failed all ports? + if sock is None: + return + + peername = sock.getpeername() + sock.close() + self.ip_addr = peername[0] + self.port = proxy_port + self.errors = 0 + self.logger.info('detected proxy at {} ({})' + .format(util.address_string(paddress), self.ip_addr)) + + async def create_connection(self, protocol_factory, host, port, ssl=None): + '''All arguments are as to asyncio's create_connection method.''' + try: + sock = await self.connect_via_proxy(host, port) + self.errors = 0 + except Exception: + self.errors += 1 + # If we have 3 consecutive errors, consider the proxy undetected + if self.errors == 3: + self.set_lost() + raise hostname = host if ssl else None return await self.loop.create_connection( diff --git a/server/peers.py b/server/peers.py index 91eee89..47786c6 100644 --- a/server/peers.py +++ b/server/peers.py @@ -230,7 +230,6 @@ class PeerManager(util.LoggedClass): self.peers = set() self.onion_peers = [] self.permit_onion_peer_time = time.time() - self.last_tor_retry_time = 0 self.tor_proxy = SocksProxy(env.tor_proxy_host, env.tor_proxy_port, loop=self.loop) self.import_peers() @@ -463,6 +462,7 @@ class PeerManager(util.LoggedClass): 2) Verifying connectivity of new peers. 3) Retrying old peers at regular intervals. ''' + self.ensure_future(self.tor_proxy.auto_detect_loop()) self.connect_to_irc() if not self.env.peer_discovery: self.logger.info('peer discovery is disabled') @@ -492,10 +492,6 @@ class PeerManager(util.LoggedClass): nearly_stale_time = (now - STALE_SECS) + WAKEUP_SECS * 2 def should_retry(peer): - # Try some Tor at startup to determine the proxy so we can - # serve the right banner file - if self.tor_proxy.port is None and self.is_coin_onion_peer(peer): - return True # Retry a peer whose ports might have updated if peer.other_port_pairs: return True @@ -507,14 +503,6 @@ class PeerManager(util.LoggedClass): peers = [peer for peer in self.peers if should_retry(peer)] - # If we don't have a tor proxy drop tor peers, but retry - # occasionally - if self.tor_proxy.port is None: - if now < self.last_tor_retry_time + 3600: - peers = [peer for peer in peers if not peer.is_tor] - elif any(peer.is_tor for peer in peers): - self.last_tor_retry_time = now - for peer in peers: peer.try_count += 1 pairs = peer.connection_port_pairs() @@ -529,6 +517,9 @@ class PeerManager(util.LoggedClass): sslc = ssl.SSLContext(ssl.PROTOCOL_TLS) if kind == 'SSL' else None if peer.is_tor: + # Don't attempt an onion connection if we don't have a tor proxy + if not self.tor_proxy.is_up(): + return create_connection = self.tor_proxy.create_connection else: create_connection = self.loop.create_connection