diff --git a/docs/environment.rst b/docs/environment.rst index 8217fc638..3835d7f50 100644 --- a/docs/environment.rst +++ b/docs/environment.rst @@ -285,6 +285,14 @@ These environment variables are optional: version string. For example to drop versions from 1.0 to 1.2 use the regex ``1\.[0-2]\.\d+``. +.. envvar:: DROP_CLIENT_UNKNOWN + + Set to anything non-empty to deny serving clients which do not + identify themselves first by issuing the server.version method + call with a non-empty client identifier. The connection is dropped + on first actual method call. This might help to filter out simple + robots. This behavior is off by default. + Resource Usage Limits ===================== diff --git a/src/electrumx/server/env.py b/src/electrumx/server/env.py index 7ff248253..9d42dbdf3 100644 --- a/src/electrumx/server/env.py +++ b/src/electrumx/server/env.py @@ -73,6 +73,7 @@ def __init__(self, coin=None): self.log_level = self.default('LOG_LEVEL', 'info').upper() self.donation_address = self.default('DONATION_ADDRESS', '') self.drop_client = self.custom("DROP_CLIENT", None, re.compile) + self.drop_client_unknown = self.boolean('DROP_CLIENT_UNKNOWN', False) self.blacklist_url = self.default('BLACKLIST_URL', self.coin.BLACKLIST_URL) self.cache_MB = self.integer('CACHE_MB', 1200) self.reorg_limit = self.integer('REORG_LIMIT', self.coin.REORG_LIMIT) diff --git a/src/electrumx/server/session.py b/src/electrumx/server/session.py index 6de279d4b..517010543 100644 --- a/src/electrumx/server/session.py +++ b/src/electrumx/server/session.py @@ -1217,7 +1217,6 @@ def __init__( self.coin = self.env.coin self.client_longname = 'unknown' self.sv_seen = False # has seen 'server.version' message? - self.sv_negotiated = asyncio.Event() # done negotiating protocol version self.anon_logs = self.env.anon_logs self.txs_sent = 0 self.log_me = SessionBase.log_new @@ -1296,15 +1295,13 @@ async def handle_request(self, request: SingleRequest): handler = self.notification_handlers.get(request.method) method = 'invalid method' if handler is None else request.method - # Version negotiation must happen before any other messages. - if not self.sv_seen and method != 'server.version': - self.logger.info(f'closing session: server.version must be first msg. got: {method}') - await self._do_crash_old_electrum_client() + # If DROP_CLIENT_UNKNOWN is enabled, check if the client identified + # by calling server.version previously. If not, disconnect the session + if (self.env.drop_client_unknown and method != 'server.version' + and self.client_longname == 'unknown'): + self.logger.info(f'disconnecting because client is unknown') raise ReplyAndDisconnect(RPCError( BAD_REQUEST, f'use server.version to identify client')) - # Wait for version negotiation to finish before processing other messages. - if method != 'server.version' and not self.sv_negotiated.is_set(): - await self.sv_negotiated.wait() self._method_counts[method] += 1 self.session_mgr._method_counts[method] += 1 @@ -2197,7 +2194,6 @@ async def phandle_server_version( BAD_REQUEST, f'unsupported protocol version: {protocol_version}')) self.set_request_handlers(ptuple) - self.sv_negotiated.set() return electrumx.version, self.protocol_version_string() async def phandle_transaction_broadcast(self, raw_tx: str | Any) -> str: @@ -2429,9 +2425,7 @@ class LocalRPC(SessionBase): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.sv_seen = True assert get_running_loop() is not None, "must be running on asyncio thread" - self.sv_negotiated.set() self.client_longname = 'RPC' self.connection.max_response_size = 0 # note: self.request_handlers are set on the class, in SessionManager.__init__ diff --git a/tests/server/test_env.py b/tests/server/test_env.py index 12d51766e..3480485a9 100644 --- a/tests/server/test_env.py +++ b/tests/server/test_env.py @@ -428,3 +428,18 @@ def test_ban_versions(): def test_coin_class_provided(): e = Env(lib_coins.Bitcoin) assert e.coin == lib_coins.Bitcoin + + +def test_drop_unknown_clients(): + setup_base_env() + e = Env() + assert e.drop_client_unknown is False + os.environ['DROP_CLIENT_UNKNOWN'] = "" + e = Env() + assert e.drop_client_unknown is False + os.environ['DROP_CLIENT_UNKNOWN'] = "1" + e = Env() + assert e.drop_client_unknown is True + os.environ['DROP_CLIENT_UNKNOWN'] = "whatever" + e = Env() + assert e.drop_client_unknown is True