diff --git a/src/config.rs b/src/config.rs index 813c641..00982a4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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), diff --git a/src/electrum/server.rs b/src/electrum/server.rs index 134d53b..7a63600 100644 --- a/src/electrum/server.rs +++ b/src/electrum/server.rs @@ -122,17 +122,22 @@ struct Connection { chan: SyncChannel, stats: Arc, txs_limit: usize, + max_line_size: usize, + max_subscriptions: usize, die_please: Option>, #[cfg(feature = "electrum-discovery")] discovery: Option>, } impl Connection { + #[allow(clippy::too_many_arguments)] pub fn new( query: Arc, stream: ConnectionStream, stats: Arc, txs_limit: usize, + max_line_size: usize, + max_subscriptions: usize, die_please: Receiver<()>, #[cfg(feature = "electrum-discovery")] discovery: Option>, ) -> 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 { 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, tx: crossbeam_channel::Sender, + max_line_size: usize, ) -> Result<()> { loop { let mut line = Vec::::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, 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); diff --git a/start b/start index 5acccb6..340da7d 100755 --- a/start +++ b/start @@ -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