mirror of
https://github.com/romanz/electrs.git
synced 2026-08-14 12:42:52 +02:00
Warn when bitcoind has not whitelisted electrs
If electrs is not whitelisted with `download` permission, bitcoind silently ignores its `getheaders` messages during IBD and may disconnect it when `maxuploadtarget` is reached, leading to opaque sync failures. After the p2p handshake, query `getpeerinfo`, locate electrs's own peer entry by matching the local socket address, and `warn!` if the `permissions` array does not contain `"download"`, suggesting `whitelist=download@127.0.0.1` in bitcoin.conf. Detection failures (RPC error, no matching peer, missing field) are logged at `debug!` only and never affect startup. Closes #400
This commit is contained in:
parent
d81dc35ae8
commit
a63e0b15af
2 changed files with 203 additions and 5 deletions
197
src/daemon.rs
197
src/daemon.rs
|
|
@ -11,6 +11,7 @@ use serde_json::{json, value::RawValue, Value};
|
|||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::{
|
||||
|
|
@ -139,11 +140,9 @@ impl Daemon {
|
|||
bail!("electrs requires non-pruned bitcoind node");
|
||||
}
|
||||
|
||||
let p2p = Mutex::new(Connection::connect(
|
||||
config.daemon_p2p_addr,
|
||||
metrics,
|
||||
config.magic,
|
||||
)?);
|
||||
let p2p = Connection::connect(config.daemon_p2p_addr, metrics, config.magic)?;
|
||||
warn_if_not_whitelisted(&rpc, p2p.local_addr());
|
||||
let p2p = Mutex::new(p2p);
|
||||
Ok(Self { p2p, rpc })
|
||||
}
|
||||
|
||||
|
|
@ -305,6 +304,77 @@ impl Daemon {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum WhitelistStatus {
|
||||
Whitelisted,
|
||||
NotWhitelisted,
|
||||
Unknown(&'static str),
|
||||
}
|
||||
|
||||
fn canonicalize(addr: SocketAddr) -> SocketAddr {
|
||||
SocketAddr::new(addr.ip().to_canonical(), addr.port())
|
||||
}
|
||||
|
||||
/// Inspect a `getpeerinfo` result and report whether the peer matching `local_addr`
|
||||
/// has the `download` permission. Pure function for unit testing.
|
||||
fn check_whitelist_status(peers: &[Value], local_addr: SocketAddr) -> WhitelistStatus {
|
||||
let target = canonicalize(local_addr);
|
||||
for peer in peers {
|
||||
let Some(addr_str) = peer.get("addr").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(addr) = addr_str.parse::<SocketAddr>() else {
|
||||
continue;
|
||||
};
|
||||
if canonicalize(addr) != target {
|
||||
continue;
|
||||
}
|
||||
// Skip peers bitcoind connected out to; electrs is always inbound from its POV.
|
||||
if peer.get("inbound").and_then(Value::as_bool) == Some(false) {
|
||||
continue;
|
||||
}
|
||||
let Some(perms) = peer.get("permissions").and_then(Value::as_array) else {
|
||||
return WhitelistStatus::Unknown(
|
||||
"matched peer has no `permissions` field (bitcoind too old?)",
|
||||
);
|
||||
};
|
||||
let has_download = perms.iter().any(|p| p.as_str() == Some("download"));
|
||||
return if has_download {
|
||||
WhitelistStatus::Whitelisted
|
||||
} else {
|
||||
WhitelistStatus::NotWhitelisted
|
||||
};
|
||||
}
|
||||
WhitelistStatus::Unknown("no matching peer in getpeerinfo output")
|
||||
}
|
||||
|
||||
/// Log a warning if bitcoind reports that electrs is connected without `download` permission.
|
||||
/// Missing whitelisting can cause bitcoind to disconnect electrs or ignore `getheaders`
|
||||
/// during IBD or once `maxuploadtarget` is reached.
|
||||
/// See https://github.com/romanz/electrs/issues/400 for background.
|
||||
fn warn_if_not_whitelisted(rpc: &Client, local_addr: SocketAddr) {
|
||||
let peers: Value = match rpc.call("getpeerinfo", &[]) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
debug!("getpeerinfo failed, skipping whitelist check: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let Some(peers) = peers.as_array() else {
|
||||
debug!("getpeerinfo returned non-array, skipping whitelist check");
|
||||
return;
|
||||
};
|
||||
match check_whitelist_status(peers, local_addr) {
|
||||
WhitelistStatus::Whitelisted => debug!("electrs is whitelisted by bitcoind"),
|
||||
WhitelistStatus::NotWhitelisted => warn!(
|
||||
"electrs is not whitelisted by bitcoind: it may be disconnected during IBD \
|
||||
or once `maxuploadtarget` is reached. Consider adding `whitelist=download@127.0.0.1` \
|
||||
to bitcoin.conf. See https://github.com/romanz/electrs/issues/400 for details."
|
||||
),
|
||||
WhitelistStatus::Unknown(reason) => debug!("whitelist check inconclusive: {}", reason),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type RpcError = bitcoincore_rpc::jsonrpc::error::RpcError;
|
||||
|
||||
pub(crate) fn extract_bitcoind_error(err: &bitcoincore_rpc::Error) -> Option<&RpcError> {
|
||||
|
|
@ -342,3 +412,120 @@ where
|
|||
Err(err) => bail!("batch {} request failed: {}", name, err),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{check_whitelist_status, WhitelistStatus};
|
||||
use serde_json::json;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
fn local() -> SocketAddr {
|
||||
"127.0.0.1:38234".parse().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whitelisted_when_download_permission_present() {
|
||||
let peers = vec![json!({
|
||||
"addr": "127.0.0.1:38234",
|
||||
"inbound": true,
|
||||
"permissions": ["noban", "download", "mempool"]
|
||||
})];
|
||||
assert_eq!(
|
||||
check_whitelist_status(&peers, local()),
|
||||
WhitelistStatus::Whitelisted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_whitelisted_when_download_permission_missing() {
|
||||
let peers = vec![json!({
|
||||
"addr": "127.0.0.1:38234",
|
||||
"inbound": true,
|
||||
"permissions": ["relay"]
|
||||
})];
|
||||
assert_eq!(
|
||||
check_whitelist_status(&peers, local()),
|
||||
WhitelistStatus::NotWhitelisted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_when_no_matching_peer() {
|
||||
let peers = vec![json!({
|
||||
"addr": "10.0.0.5:8333",
|
||||
"inbound": false,
|
||||
"permissions": ["download"]
|
||||
})];
|
||||
assert!(matches!(
|
||||
check_whitelist_status(&peers, local()),
|
||||
WhitelistStatus::Unknown(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_when_permissions_field_missing() {
|
||||
let peers = vec![json!({
|
||||
"addr": "127.0.0.1:38234",
|
||||
"inbound": true,
|
||||
})];
|
||||
assert!(matches!(
|
||||
check_whitelist_status(&peers, local()),
|
||||
WhitelistStatus::Unknown(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matches_ipv4_mapped_ipv6_addr() {
|
||||
// bitcoind may report the peer as an IPv4-mapped IPv6 address on dual-stack systems.
|
||||
let peers = vec![json!({
|
||||
"addr": "[::ffff:127.0.0.1]:38234",
|
||||
"inbound": true,
|
||||
"permissions": ["download"]
|
||||
})];
|
||||
assert_eq!(
|
||||
check_whitelist_status(&peers, local()),
|
||||
WhitelistStatus::Whitelisted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_outbound_peers_with_matching_addr() {
|
||||
// Defensive: an outbound peer at the same addr can't be electrs.
|
||||
let peers = vec![
|
||||
json!({
|
||||
"addr": "127.0.0.1:38234",
|
||||
"inbound": false,
|
||||
"permissions": ["download"]
|
||||
}),
|
||||
json!({
|
||||
"addr": "127.0.0.1:38234",
|
||||
"inbound": true,
|
||||
"permissions": []
|
||||
}),
|
||||
];
|
||||
assert_eq!(
|
||||
check_whitelist_status(&peers, local()),
|
||||
WhitelistStatus::NotWhitelisted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_unrelated_peers() {
|
||||
let peers = vec![
|
||||
json!({"addr": "1.2.3.4:8333", "inbound": false, "permissions": []}),
|
||||
json!({"addr": "127.0.0.1:38234", "inbound": true, "permissions": ["download"]}),
|
||||
];
|
||||
assert_eq!(
|
||||
check_whitelist_status(&peers, local()),
|
||||
WhitelistStatus::Whitelisted
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_unknown_on_empty_peer_list() {
|
||||
assert!(matches!(
|
||||
check_whitelist_status(&[], local()),
|
||||
WhitelistStatus::Unknown(_)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
src/p2p.rs
11
src/p2p.rs
|
|
@ -59,6 +59,7 @@ pub(crate) struct Connection {
|
|||
blocks_recv: Receiver<SerBlock>,
|
||||
headers_recv: Receiver<Vec<BlockHeader>>,
|
||||
new_block_recv: Receiver<()>,
|
||||
local_addr: SocketAddr,
|
||||
|
||||
blocks_duration: Histogram,
|
||||
}
|
||||
|
|
@ -138,6 +139,9 @@ impl Connection {
|
|||
pub(crate) fn connect(address: SocketAddr, metrics: &Metrics, magic: Magic) -> Result<Self> {
|
||||
let recv_conn = TcpStream::connect(address)
|
||||
.with_context(|| format!("p2p failed to connect: {:?}", address))?;
|
||||
let local_addr = recv_conn
|
||||
.local_addr()
|
||||
.context("failed to read p2p local address")?;
|
||||
let mut send_conn = recv_conn
|
||||
.try_clone()
|
||||
.context("failed to clone connection")?;
|
||||
|
|
@ -310,9 +314,16 @@ impl Connection {
|
|||
blocks_recv,
|
||||
headers_recv,
|
||||
new_block_recv,
|
||||
local_addr,
|
||||
blocks_duration,
|
||||
})
|
||||
}
|
||||
|
||||
/// Local socket address of the outbound TCP connection to bitcoind.
|
||||
/// Matches the `addr` field of the corresponding `getpeerinfo` entry on the daemon side.
|
||||
pub(crate) fn local_addr(&self) -> SocketAddr {
|
||||
self.local_addr
|
||||
}
|
||||
}
|
||||
|
||||
fn build_version_message() -> NetworkMessage {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue