Fix: Add some simple limits to Electrum RPC by default

This commit is contained in:
junderw 2026-05-04 14:59:15 +09:00
parent 8aa0cc06a6
commit 60fb1029c9
No known key found for this signature in database
GPG Key ID: B256185D3A971908
3 changed files with 118 additions and 12 deletions

View File

@ -65,6 +65,9 @@ pub struct Config {
pub rest_default_max_address_summary_txs: usize,
pub rest_max_mempool_page_size: usize,
pub rest_max_mempool_txid_page_size: usize,
pub electrum_max_line_size: usize,
pub electrum_max_subscriptions: usize,
pub electrum_max_clients: usize,
#[cfg(feature = "liquid")]
pub parent_network: BNetwork,
@ -278,6 +281,21 @@ impl Config {
.long("electrum-banner")
.help("Welcome banner for the Electrum server, shown in the console to clients.")
.takes_value(true)
).arg(
Arg::with_name("electrum_max_line_size")
.long("electrum-max-line-size")
.help("Maximum size of a single Electrum request line in bytes (default: 1 MiB).")
.default_value("1048576")
).arg(
Arg::with_name("electrum_max_subscriptions")
.long("electrum-max-subscriptions")
.help("Maximum number of scripthash subscriptions per client connection.")
.default_value("100")
).arg(
Arg::with_name("electrum_max_clients")
.long("electrum-max-clients")
.help("Maximum number of concurrent Electrum client connections.")
.default_value("10")
);
#[cfg(unix)]
@ -547,6 +565,9 @@ impl Config {
"rest_max_mempool_txid_page_size",
usize
),
electrum_max_line_size: value_t_or_exit!(m, "electrum_max_line_size", usize),
electrum_max_subscriptions: value_t_or_exit!(m, "electrum_max_subscriptions", usize),
electrum_max_clients: value_t_or_exit!(m, "electrum_max_clients", usize),
jsonrpc_import: m.is_present("jsonrpc_import"),
light_mode: m.is_present("light_mode"),
main_loop_delay: value_t_or_exit!(m, "main_loop_delay", u64),

View File

@ -122,17 +122,22 @@ struct Connection {
chan: SyncChannel<Message>,
stats: Arc<Stats>,
txs_limit: usize,
max_line_size: usize,
max_subscriptions: usize,
die_please: Option<Receiver<()>>,
#[cfg(feature = "electrum-discovery")]
discovery: Option<Arc<DiscoveryManager>>,
}
impl Connection {
#[allow(clippy::too_many_arguments)]
pub fn new(
query: Arc<Query>,
stream: ConnectionStream,
stats: Arc<Stats>,
txs_limit: usize,
max_line_size: usize,
max_subscriptions: usize,
die_please: Receiver<()>,
#[cfg(feature = "electrum-discovery")] discovery: Option<Arc<DiscoveryManager>>,
) -> Connection {
@ -144,6 +149,8 @@ impl Connection {
chan: SyncChannel::new(10),
stats,
txs_limit,
max_line_size,
max_subscriptions,
die_please: Some(die_please),
#[cfg(feature = "electrum-discovery")]
discovery,
@ -294,6 +301,16 @@ impl Connection {
fn blockchain_scripthash_subscribe(&mut self, params: &[Value]) -> Result<Value> {
let script_hash = hash_from_value(params.first()).chain_err(|| "bad script_hash")?;
// Enforce per-client subscription limit (don't count re-subscriptions to the same hash)
if !self.status_hashes.contains_key(&script_hash)
&& self.status_hashes.len() >= self.max_subscriptions
{
bail!(
"subscription limit reached ({} max per client)",
self.max_subscriptions
);
}
let history_txids = get_history(&self.query, &script_hash[..], self.txs_limit)?;
let status_hash = get_status_hash(history_txids, &self.query)
.map_or(Value::Null, |h| json!(hex::encode(full_hash(&h[..]))));
@ -623,15 +640,25 @@ impl Connection {
fn handle_requests(
mut reader: BufReader<ConnectionStream>,
tx: crossbeam_channel::Sender<Message>,
max_line_size: usize,
) -> Result<()> {
loop {
let mut line = Vec::<u8>::new();
reader
// Read up to max_line_size + 1 bytes to detect oversized lines
let mut limited = (&mut reader).take((max_line_size as u64).saturating_add(1));
limited
.read_until(b'\n', &mut line)
.chain_err(|| "failed to read a request")?;
if line.is_empty() {
tx.send(Message::Done).chain_err(|| "channel closed")?;
return Ok(());
} else if line.len() > max_line_size {
let _ = tx.send(Message::Done);
bail!(
"request line too large ({} bytes, max is {})",
line.len(),
max_line_size
)
} else {
if line.starts_with(&[22, 3, 1]) {
// (very) naive SSL handshake detection
@ -671,7 +698,10 @@ impl Connection {
let _ = reply_killer.send(());
});
let child = spawn_thread("reader", || Connection::handle_requests(reader, tx));
let max_line_size = self.max_line_size;
let child = spawn_thread("reader", move || {
Connection::handle_requests(reader, tx, max_line_size)
});
if let Err(e) = self.handle_replies(reply_receiver) {
error!(
"[{}] connection handling failed: {}",
@ -855,6 +885,9 @@ impl RPC {
});
let txs_limit = config.electrum_txs_limit;
let max_line_size = config.electrum_max_line_size;
let max_subscriptions = config.electrum_max_subscriptions;
let max_clients = config.electrum_max_clients;
RPC {
notification: notification.sender(),
@ -872,10 +905,33 @@ impl RPC {
acceptor_shutdown_sender,
);
let mut threads = HashMap::new();
let mut threads: HashMap<thread::ThreadId, (thread::JoinHandle<()>, Sender<()>)> =
HashMap::new();
let (garbage_sender, garbage_receiver) = crossbeam_channel::unbounded();
while let Some(stream) = acceptor.receiver().recv().unwrap() {
// Clean up finished threads before checking connection limit
while let Ok(id) = garbage_receiver.try_recv() {
if let Some((thread, killer)) = threads.remove(&id) {
let _ = killer.send(());
if let Err(error) = thread.join() {
error!("failed to join {:?}: {:?}", id, error);
}
}
}
// Enforce maximum connection limit
if threads.len() >= max_clients {
warn!(
"[{}] rejecting connection: max clients reached ({}/{})",
stream.addr_string(),
threads.len(),
max_clients
);
let _ = stream.shutdown(Shutdown::Both);
continue;
}
let addr = stream.addr_string();
// explicitely scope the shadowed variables for the new thread
let query = Arc::clone(&query);
@ -898,6 +954,8 @@ impl RPC {
stream,
stats,
txs_limit,
max_line_size,
max_subscriptions,
peace_receiver,
#[cfg(feature = "electrum-discovery")]
discovery,
@ -911,15 +969,6 @@ impl RPC {
trace!("[{}] spawned {:?}", addr, spawned.thread().id());
threads.insert(spawned.thread().id(), (spawned, killer));
while let Ok(id) = garbage_receiver.try_recv() {
if let Some((thread, killer)) = threads.remove(&id) {
trace!("[{}] joining {:?}", addr, id);
let _ = killer.send(());
if let Err(error) = thread.join() {
error!("failed to join {:?}: {:?}", id, error);
}
}
}
}
// Drop these
drop(acceptor);

36
start
View File

@ -153,6 +153,9 @@ do
# prepare run-time variables
UTXOS_LIMIT=500
ELECTRUM_TXS_LIMIT=500
ELECTRUM_MAX_LINE_SIZE=1048576 # 1 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100
ELECTRUM_MAX_CLIENTS=10
MAIN_LOOP_DELAY=500
DAEMON_CONF="${HOME}/${DAEMON}.conf"
HTTP_SOCKET_FILE="${HOME}/socket/esplora-${DAEMON}-${NETWORK}"
@ -167,43 +170,73 @@ do
if [ "${NODENAME}" = "node201" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
MAIN_LOOP_DELAY=14000
fi
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "sg1" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node204" ] && [ "${LOCATION}" = "hnl" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node206" ] && [ "${LOCATION}" = "tk7" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node211" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node212" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node213" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NODENAME}" = "node214" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${NETWORK}" = "testnet4" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ "${LOCATION}" = "fmt" ];then
UTXOS_LIMIT=9000
ELECTRUM_TXS_LIMIT=9000
ELECTRUM_MAX_LINE_SIZE=16777216 # 16 MiB
ELECTRUM_MAX_SUBSCRIPTIONS=100000
ELECTRUM_MAX_CLIENTS=10000
fi
if [ ! -e "${POPULAR_SCRIPTS_FILE}" ];then
@ -229,6 +262,9 @@ do
--address-search \
--utxos-limit "${UTXOS_LIMIT}" \
--electrum-txs-limit "${ELECTRUM_TXS_LIMIT}" \
--electrum-max-line-size "${ELECTRUM_MAX_LINE_SIZE}" \
--electrum-max-subscriptions "${ELECTRUM_MAX_SUBSCRIPTIONS}" \
--electrum-max-clients "${ELECTRUM_MAX_CLIENTS}" \
-vv
sleep 1
done