Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions OPERATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,9 @@ Clean smoke:
| `--signet-block-time SECS` | `signet_block_time=` | 600; requires a custom challenge |
| `--listen ADDR` | `listen=` | bind later default port |
| `--connect ADDR` | `connect=` (repeatable) | seeds |
| `--proxy HOST:PORT` | `proxy=` | unset — SOCKS5 for all P2P outbound |
| `--onion HOST:PORT` | `onion=` | unset — SOCKS5 for onion destinations |
| `--proxy-randomize[=0\|1]` | `proxy_randomize=` | **on** — fresh SOCKS username per peer (Tor circuit isolation) |
| `--milestone HEIGHT` | `milestone=` | network default (mainnet 840000) |
| `--max-outbound N` | `max_outbound=` | 16 live download peers |
| `--max-inbound N` | `max_inbound=` | 125 inbound sessions |
Expand Down Expand Up @@ -425,6 +428,14 @@ max_inbound=64
mempool_size_mb=100
```

### P2P via system Tor SOCKS

`--proxy 127.0.0.1:9050` sends every P2P outbound through SOCKS5 CONNECT
(system `tor`, not Arti). DNS seeds are not resolved locally on that path —
pass `--connect ADDR` (or reuse a `peers` file). `--proxy-randomize` (default
on) uses a fresh SOCKS username per peer so Tor isolates circuits.
`--onion HOST:PORT` stores a separate SOCKS endpoint for onion destinations.

`--datadir` holds the node root (`store/`, `mempool/`, `peers`, `rpc.token`, `rpc.sock`).
Omit `--datadir-cold` and cold files live there too. Set it to put the large
rarely-read Class A **inwit** stem (`inwit.body` + `inwit.loc`, ~486 GiB + loc
Expand Down
1 change: 1 addition & 0 deletions crates/rbitcoin-net/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ bitcoin = { workspace = true }
bip324 = { workspace = true }
tokio = { workspace = true }
arc-swap = { workspace = true }
getrandom = "0.4"

[target.'cfg(target_os = "linux")'.dependencies]
# POLLRDHUP: peer FIN with unread bytes still in the receive buffer.
Expand Down
6 changes: 5 additions & 1 deletion crates/rbitcoin-net/src/ibd/dial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ pub(crate) async fn dial_batch(
sinks: PeerEventSinks,
connect_timeout: Duration,
cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
dialer: crate::socks::Dialer,
) -> DialBatchResult {
let mut out = DialBatchResult {
slots: Vec::new(),
Expand Down Expand Up @@ -278,8 +279,9 @@ pub(crate) async fn dial_batch(
"{}",
trying_connection_log(PeerConnType::OutboundFullRelay, addr)
);
let dialer = dialer.clone();
handles.push(tokio::spawn(async move {
let fut = spawn_peer(id, addr, magic, local_addr, tip_h, sinks);
let fut = spawn_peer(id, addr, magic, local_addr, tip_h, sinks, dialer);
match tokio::time::timeout(connect_timeout, fut).await {
Ok(Ok(slot)) => Ok(slot),
Ok(Err(e)) => {
Expand Down Expand Up @@ -968,6 +970,7 @@ mod tests {
sinks.clone(),
Duration::from_millis(50),
None,
crate::socks::Dialer::Direct,
));
assert!(r.slots.is_empty() && r.failed.is_empty());
let r2 = rt.block_on(dial_batch(
Expand All @@ -982,6 +985,7 @@ mod tests {
sinks,
Duration::from_millis(50),
None,
crate::socks::Dialer::Direct,
));
assert!(r2.slots.is_empty() && r2.failed.is_empty());
}
Expand Down
8 changes: 7 additions & 1 deletion crates/rbitcoin-net/src/ibd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@
/// Optional shared peer book (discovered addrs + flags). Seeded at start and
/// written back on IBD exit so the node can persist across runs.
pub peers: Option<std::sync::Arc<std::sync::Mutex<crate::seeds::AddrMan>>>,
/// Outbound TCP: direct or SOCKS5.
pub dialer: crate::socks::Dialer,
}

impl Default for IbdConfig {
Expand All @@ -176,6 +178,7 @@
headers_batch: MAX_HEADERS_RESULTS,
connect_timeout: Duration::from_secs(8),
peers: None,
dialer: crate::socks::Dialer::Direct,
}
}
}
Expand All @@ -183,15 +186,16 @@
impl IbdConfig {
/// Smaller window / short dials for tests (no multi-second connect stalls).
pub fn for_test() -> Self {
Self {
window: 32,
per_peer: 8,
target_peers: 4,
stall: Duration::from_secs(3),
headers_batch: MAX_HEADERS_RESULTS,
connect_timeout: Duration::from_millis(400),
peers: None,
dialer: crate::socks::Dialer::Direct,
}

Check warning on line 198 in crates/rbitcoin-net/src/ibd/mod.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace IbdConfig::for_test -> Self with Default::default()
}
}

Expand Down Expand Up @@ -301,6 +305,7 @@
sinks.clone(),
cfg.connect_timeout,
cancel.as_ref().map(Arc::clone),
cfg.dialer.clone(),
)
.await;
apply_dial_result(peer_sess.book_mut(), &initial);
Expand Down Expand Up @@ -736,10 +741,11 @@
let sinks_r = sinks.clone();
let cto = cfg.connect_timeout;
let cancel_c = cancel.as_ref().map(Arc::clone);
let dialer_c = cfg.dialer.clone();
redial_handle = Some(tokio::spawn(async move {
dial_batch(
&book, &next_id, want, already, &occupied, magic, local_addr, tip_h, sinks_r,
cto, cancel_c,
cto, cancel_c, dialer_c,
)
.await
}));
Expand Down
4 changes: 2 additions & 2 deletions crates/rbitcoin-net/src/ibd/peer_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use std::net::SocketAddr;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;

Expand Down Expand Up @@ -159,8 +158,9 @@ pub(crate) async fn spawn_peer(
local: SocketAddr,
tip_h: Option<u32>,
sinks: PeerEventSinks,
dialer: crate::socks::Dialer,
) -> Result<PeerSlot, NetError> {
let stream = TcpStream::connect(addr).await?;
let stream = dialer.connect(addr).await?;
let ua = rbitcoin_primitives::rbitcoin_subversion(env!("CARGO_PKG_VERSION"), &[] as &[&str])
.unwrap_or_else(|_| format!("/rbitcoin:{}/", env!("CARGO_PKG_VERSION")));
let (ver, reader, writer, _wire, _tcp_shutdown) = connect_and_handshake_timed(
Expand Down
9 changes: 6 additions & 3 deletions crates/rbitcoin-net/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod reactor;
mod seeds;
mod serve_perf;
mod service;
mod socks;
mod tip_accept;
mod tx_relay;
mod v2;
Expand Down Expand Up @@ -49,18 +50,20 @@ pub use peer::{
};
pub use peer_dos::DEFAULT_MAX_INBOUND;
pub use peers::{
parse_peer_addr, pick_stale_follow_evict, DialRequest, LivePeer, PeerConnType, PeerHub,
PeerInfo, PeerOut, PingAction,
parse_peer_addr, pick_stale_follow_evict, DialRequest, DialTarget, LivePeer, PeerConnType,
PeerHub, PeerInfo, PeerOut, PingAction,
};
pub use rbitcoin_mempool::AcceptError;
pub(crate) use rbitcoin_mempool::MempoolGraphStats;
pub use reactor::BlockingRegion;
pub use seeds::{
default_port, default_rpc_port, dns_seeds, fixed_seed_hosts, resolve_all_seeds,
resolve_dns_seeds, resolve_fixed_seeds, AddrMan, PeerEntry, PeerFlags, MAX_ADDR_MAN,
resolve_dns_seeds, resolve_fixed_seeds, socks_dns_seed_dests, AddrMan, PeerEntry, PeerFlags,
MAX_ADDR_MAN,
};
pub use serve_perf::{format_serve_perf, sample_reset_serve_perf, ServePerfSample};
pub use service::P2PNode;
pub use socks::Dialer;
pub use tx_relay::{
ElectrumMempoolItem, MempoolAnnounce, MempoolHub, MempoolPerfSample, MempoolTxSnapEntry,
MempoolTxSnapshot,
Expand Down
51 changes: 48 additions & 3 deletions crates/rbitcoin-net/src/peers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,34 @@ pub(crate) fn trying_connection_log(typ: PeerConnType, addr: impl std::fmt::Disp
format!("p2p: trying connection ({}) to {addr}", typ.as_str())
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DialTarget {
Socket(SocketAddr),
Domain { host: String, port: u16 },
}

impl DialTarget {
pub fn peer_hint(&self) -> SocketAddr {
match self {
Self::Socket(addr) => *addr,
Self::Domain { port, .. } => SocketAddr::from(([0, 0, 0, 0], *port)),
}
}
}

impl std::fmt::Display for DialTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Socket(addr) => write!(f, "{addr}"),
Self::Domain { host, port } => write!(f, "{host}:{port}"),
}
}
}

/// Request that the node dial `addr` as `typ`.
#[derive(Clone, Debug)]
pub struct DialRequest {
pub addr: SocketAddr,
pub target: DialTarget,
pub typ: PeerConnType,
}

Expand Down Expand Up @@ -1980,8 +2004,29 @@ impl PeerHub {
pub fn dial(&self, addr: SocketAddr, typ: PeerConnType) -> Result<(), String> {
let g = self.dial_tx.lock().unwrap_or_else(|e| e.into_inner());
let tx = g.as_ref().ok_or("no dialer attached")?;
tx.send(DialRequest { addr, typ })
.map_err(|_| "dialer closed".to_string())
tx.send(DialRequest {
target: DialTarget::Socket(addr),
typ,
})
.map_err(|_| "dialer closed".to_string())
}

pub fn dial_domain(
&self,
host: impl Into<String>,
port: u16,
typ: PeerConnType,
) -> Result<(), String> {
let g = self.dial_tx.lock().unwrap_or_else(|e| e.into_inner());
let tx = g.as_ref().ok_or("no dialer attached")?;
tx.send(DialRequest {
target: DialTarget::Domain {
host: host.into(),
port,
},
typ,
})
.map_err(|_| "dialer closed".to_string())
}
}

Expand Down
33 changes: 33 additions & 0 deletions crates/rbitcoin-net/src/seeds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,17 @@ pub fn resolve_all_seeds(network: Network) -> Vec<SocketAddr> {
out
}

/// DNS seed hostnames with the network default port for SOCKS domain CONNECT.
///
/// Does not call [`ToSocketAddrs`] — the proxy performs remote DNS.
pub fn socks_dns_seed_dests(network: Network) -> Vec<(String, u16)> {
let port = default_port(network);
dns_seeds(network)
.iter()
.map(|host| ((*host).to_string(), port))
.collect()
}

/// Informational peer flags packed into one byte (more bits reserved for later).
///
/// | bit | name | meaning |
Expand Down Expand Up @@ -1233,6 +1244,28 @@ mod tests {
assert!(resolve_all_seeds(Network::Regtest).is_empty());
}

#[test]
fn dns_seeds_not_resolved_locally_when_proxy() {
for net in [
Network::Mainnet,
Network::Testnet,
Network::Signet,
Network::Regtest,
] {
let dests = socks_dns_seed_dests(net);
let names = dns_seeds(net);
assert_eq!(dests.len(), names.len());
for ((host, port), want) in dests.iter().zip(names.iter()) {
assert_eq!(host, want);
assert_eq!(*port, default_port(net));
assert!(
host.parse::<std::net::IpAddr>().is_err(),
"SOCKS seed dest must stay a hostname, got {host}"
);
}
}
}

#[test]
fn peer_flags_set_remove_and_mid_range_speed() {
let mut f = PeerFlags::empty();
Expand Down
Loading
Loading