Skip to content

Commit 04d517a

Browse files
authored
Merge pull request #325 from SomberNight/202510_protocol_serverversion
protocol 1.6: "server.version" must be first msg, must tolerate unknown args
2 parents 592c4dc + b2d6d04 commit 04d517a

4 files changed

Lines changed: 38 additions & 41 deletions

File tree

docs/environment.rst

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -281,14 +281,6 @@ These environment variables are optional:
281281
version string. For example to drop versions from 1.0 to 1.2 use
282282
the regex ``1\.[0-2]\.\d+``.
283283

284-
.. envvar:: DROP_CLIENT_UNKNOWN
285-
286-
Set to anything non-empty to deny serving clients which do not
287-
identify themselves first by issuing the server.version method
288-
call with a non-empty client identifier. The connection is dropped
289-
on first actual method call. This might help to filter out simple
290-
robots. This behavior is off by default.
291-
292284

293285
Resource Usage Limits
294286
=====================

src/electrumx/server/env.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ def __init__(self, coin=None):
7272
self.log_level = self.default('LOG_LEVEL', 'info').upper()
7373
self.donation_address = self.default('DONATION_ADDRESS', '')
7474
self.drop_client = self.custom("DROP_CLIENT", None, re.compile)
75-
self.drop_client_unknown = self.boolean('DROP_CLIENT_UNKNOWN', False)
7675
self.blacklist_url = self.default('BLACKLIST_URL', self.coin.BLACKLIST_URL)
7776
self.cache_MB = self.integer('CACHE_MB', 1200)
7877
self.reorg_limit = self.integer('REORG_LIMIT', self.coin.REORG_LIMIT)

src/electrumx/server/session.py

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,8 @@ def __init__(
964964
self.env = session_mgr.env
965965
self.coin = self.env.coin
966966
self.client = 'unknown'
967+
self.sv_seen = False # has seen 'server.version' message?
968+
self.sv_negotiated = asyncio.Event() # done negotiating protocol version
967969
self.anon_logs = self.env.anon_logs
968970
self.txs_sent = 0
969971
self.log_me = SessionBase.log_new
@@ -1027,12 +1029,15 @@ async def handle_request(self, request):
10271029
handler = None
10281030
method = 'invalid method' if handler is None else request.method
10291031

1030-
# If DROP_CLIENT_UNKNOWN is enabled, check if the client identified
1031-
# by calling server.version previously. If not, disconnect the session
1032-
if self.env.drop_client_unknown and method != 'server.version' and self.client == 'unknown':
1033-
self.logger.info(f'disconnecting because client is unknown')
1032+
# Version negotiation must happen before any other messages.
1033+
if not self.sv_seen and method != 'server.version':
1034+
self.logger.info(f'closing session: server.version must be first msg. got: {method}')
1035+
await self._do_crash_old_electrum_client()
10341036
raise ReplyAndDisconnect(RPCError(
10351037
BAD_REQUEST, f'use server.version to identify client'))
1038+
# Wait for version negotiation to finish before processing other messages.
1039+
if method != 'server.version' and not self.sv_negotiated.is_set():
1040+
await self.sv_negotiated.wait()
10361041

10371042
self.session_mgr._method_counts[method] += 1
10381043
coro = handler_invocation(handler, request)()
@@ -1041,6 +1046,21 @@ async def handle_request(self, request):
10411046
def protocol_version_string(self) -> str:
10421047
raise NotImplementedError()
10431048

1049+
async def maybe_crash_old_client(self, ptuple, crash_client_ver):
1050+
if crash_client_ver:
1051+
client_ver = util.protocol_tuple(self.client)
1052+
is_old_protocol = ptuple is None or ptuple <= (1, 2)
1053+
is_old_client = client_ver != (0,) and client_ver <= crash_client_ver
1054+
if is_old_protocol and is_old_client:
1055+
await self._do_crash_old_electrum_client()
1056+
1057+
async def _do_crash_old_electrum_client(self):
1058+
self.logger.info(f'attempting to crash old client with version {self.client}')
1059+
# this can crash electrum client 2.6 <= v < 3.1.2
1060+
await self.send_notification('blockchain.relayfee', ())
1061+
# this can crash electrum client (v < 2.8.2) UNION (3.0.0 <= v < 3.3.0)
1062+
await self.send_notification('blockchain.estimatefee', ())
1063+
10441064

10451065
class ElectrumX(SessionBase):
10461066
'''A TCP server that handles incoming Electrum connections.'''
@@ -1474,11 +1494,20 @@ async def ping(self):
14741494
self.bump_cost(0.1)
14751495
return None
14761496

1477-
async def server_version(self, client_name='', protocol_version=None):
1497+
async def server_version(
1498+
self,
1499+
client_name='',
1500+
protocol_version=None,
1501+
*extra_args,
1502+
**extra_kwargs,
1503+
):
14781504
'''Returns the server version as a string.
14791505
14801506
client_name: a string identifying the client
14811507
protocol_version: the protocol version spoken by the client
1508+
1509+
note: extraneous unknown args for 'server.version' MUST be tolerated
1510+
and ignored by the server, to allow for future extensions.
14821511
'''
14831512
self.bump_cost(0.5)
14841513
if self.sv_seen:
@@ -1498,7 +1527,7 @@ async def server_version(self, client_name='', protocol_version=None):
14981527
ptuple, client_min = util.protocol_version(
14991528
protocol_version, self.PROTOCOL_MIN, self.PROTOCOL_MAX)
15001529

1501-
await self.crash_old_client(ptuple, self.env.coin.CRASH_CLIENT_VER)
1530+
await self.maybe_crash_old_client(ptuple, self.env.coin.CRASH_CLIENT_VER)
15021531

15031532
if ptuple is None:
15041533
if client_min > self.PROTOCOL_MIN:
@@ -1509,20 +1538,9 @@ async def server_version(self, client_name='', protocol_version=None):
15091538
BAD_REQUEST, f'unsupported protocol version: {protocol_version}'))
15101539
self.set_request_handlers(ptuple)
15111540

1541+
self.sv_negotiated.set()
15121542
return electrumx.version, self.protocol_version_string()
15131543

1514-
async def crash_old_client(self, ptuple, crash_client_ver):
1515-
if crash_client_ver:
1516-
client_ver = util.protocol_tuple(self.client)
1517-
is_old_protocol = ptuple is None or ptuple <= (1, 2)
1518-
is_old_client = client_ver != (0,) and client_ver <= crash_client_ver
1519-
if is_old_protocol and is_old_client:
1520-
self.logger.info(f'attempting to crash old client with version {self.client}')
1521-
# this can crash electrum client 2.6 <= v < 3.1.2
1522-
await self.send_notification('blockchain.relayfee', ())
1523-
# this can crash electrum client (v < 2.8.2) UNION (3.0.0 <= v < 3.3.0)
1524-
await self.send_notification('blockchain.estimatefee', ())
1525-
15261544
async def transaction_broadcast(self, raw_tx):
15271545
'''Broadcast a raw transaction to the network.
15281546
@@ -1695,6 +1713,8 @@ class LocalRPC(SessionBase):
16951713

16961714
def __init__(self, *args, **kwargs):
16971715
super().__init__(*args, **kwargs)
1716+
self.sv_seen = True
1717+
self.sv_negotiated.set()
16981718
self.client = 'RPC'
16991719
self.connection.max_response_size = 0
17001720

tests/server/test_env.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -415,17 +415,3 @@ def test_ban_versions():
415415
def test_coin_class_provided():
416416
e = Env(lib_coins.BitcoinSV)
417417
assert e.coin == lib_coins.BitcoinSV
418-
419-
420-
def test_drop_unknown_clients():
421-
e = Env()
422-
assert e.drop_client_unknown is False
423-
os.environ['DROP_CLIENT_UNKNOWN'] = ""
424-
e = Env()
425-
assert e.drop_client_unknown is False
426-
os.environ['DROP_CLIENT_UNKNOWN'] = "1"
427-
e = Env()
428-
assert e.drop_client_unknown is True
429-
os.environ['DROP_CLIENT_UNKNOWN'] = "whatever"
430-
e = Env()
431-
assert e.drop_client_unknown is True

0 commit comments

Comments
 (0)