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
9 changes: 8 additions & 1 deletion OPERATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,13 +366,15 @@ Clean smoke:
| `--signet-challenge HEX` | `signet_challenge=` | default global Signet challenge |
| `--signet-block-time SECS` | `signet_block_time=` | 600; requires a custom challenge |
| `--listen ADDR` | `listen=` | bind later default port |
| `--no-listen` / `--listen=0` | `listen=0` / `no_listen=` | bind a loopback default; **off** = no P2P socket (outbound-only) |
| `--no-discover` | `no_discover=` | discover **on**; flag off = no self-announce / `localaddresses` |
| `--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 |
| `--max-inbound N` | `max_inbound=` | 125 inbound sessions; **0** = no inbound slots (outbound-only) |
| `--mempool-size-mb N` | `mempool_size_mb=` | ~300 MiB weight |
| `--conf FILE` | | none |
| `--log-level LEVEL` | `log_level=` | `info` |
Expand Down Expand Up @@ -436,6 +438,11 @@ 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.

`--listen=0` / `--no-listen` starts without a P2P TCP bind (no ISP port
forward). `--max-inbound 0` refuses inbound slots. `--no-discover` does not
self-announce even when `--external-ip` is set. A later onion inbound bind
does not require a public clearnet listen.

`--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
33 changes: 33 additions & 0 deletions crates/rbitcoin-net/src/peer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5475,6 +5475,39 @@ fn externalip_is_advertised_once_then_after_a_day() {
);
}

#[test]
fn no_discover_suppresses_self_announce() {
use crate::peers::{PeerConnType, PeerHub};
use bitcoin::p2p::address::Address;
use bitcoin::p2p::message_network::VersionMessage;
use bitcoin::p2p::ServiceFlags;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};

let hub = PeerHub::new();
let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 18444);
let ver = VersionMessage {
version: 70016,
services: ServiceFlags::NETWORK,
timestamp: 0,
receiver: Address::new(&addr, ServiceFlags::NONE),
sender: Address::new(&addr, ServiceFlags::NONE),
nonce: 1,
user_agent: "/rbitcoin:test/".into(),
start_height: 0,
relay: true,
};
let peer = hub.register(addr, addr, &ver, false, PeerConnType::OutboundFullRelay);
hub.set_listen_port(18445);
hub.set_external_ips(vec![IpAddr::V4(Ipv4Addr::new(42, 42, 42, 42))]);
hub.set_discover(false);
assert!(
peer.take_local_addr_due(1_000).is_none(),
"--no-discover must suppress self-announce"
);
assert!(peer.take_self_announce_msg().is_none());
assert!(hub.rpc_local_addresses().is_empty());
}

#[test]
fn redundant_verack_is_ignored_and_logged() {
use bitcoin::consensus::encode::serialize;
Expand Down
13 changes: 13 additions & 0 deletions crates/rbitcoin-net/src/peers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1073,6 +1073,8 @@
external_ips: Mutex<Vec<IpAddr>>,
/// P2P listen port used with advertised external IPs.
listen_port: AtomicU16,
/// Core `-discover`. Off: never self-announce, even with `--external-ip`.
discover: AtomicBool,
asmap: Mutex<Option<Arc<crate::asmap::AsMap>>>,
/// Tip-mode mempool for Core `EraseForPeer` on disconnect.
mempool: Mutex<Option<Weak<crate::tx_relay::MempoolHub>>>,
Expand Down Expand Up @@ -1147,6 +1149,7 @@
peer_timeout_secs: AtomicU64::new(60),
external_ips: Mutex::new(Vec::new()),
listen_port: AtomicU16::new(0),
discover: AtomicBool::new(true),
asmap: Mutex::new(None),
mempool: Mutex::new(None),
net_perms: Mutex::new(crate::net_permissions::NetPermTable::default()),
Expand Down Expand Up @@ -1206,24 +1209,34 @@
self.listen_port.store(port, Ordering::Relaxed);
}

pub fn set_discover(&self, on: bool) {
self.discover.store(on, Ordering::Relaxed);
}

/// `getnetworkinfo.localaddresses` rows for operator-advertised IPs.
pub fn rpc_local_addresses(&self) -> Vec<(String, u16, i32)> {
const LOCAL_MANUAL: i32 = 4;
if !self.discover.load(Ordering::Relaxed) {
return Vec::new();
}
let port = self.listen_port.load(Ordering::Relaxed);
if port == 0 {
return Vec::new();
}
let ips = self
.external_ips
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
ips.into_iter()
.map(|ip| (ip.to_string(), port, LOCAL_MANUAL))
.collect()

Check warning on line 1233 in crates/rbitcoin-net/src/peers.rs

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace PeerHub::rpc_local_addresses -> Vec<(String, u16, i32)> with vec![]
}

pub fn advertise_local_socket(&self) -> Option<SocketAddr> {
if !self.discover.load(Ordering::Relaxed) {
return None;
}
let port = self.listen_port.load(Ordering::Relaxed);
if port == 0 {
return None;
Expand Down
154 changes: 137 additions & 17 deletions crates/rbitcoin-net/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,35 +99,91 @@ impl P2PNode {
user_agent: String,
max_inbound: usize,
dialer: crate::socks::Dialer,
) -> Result<Self, NetError> {
let bind = (max_inbound > 0).then_some(listen);
Self::start_inner(
bind,
query,
params,
milestone,
user_agent,
max_inbound,
dialer,
)
.await
}

/// Outbound dials only: no P2P `TcpListener`. `local_addr` is `127.0.0.1:0`.
pub async fn start_outbound_only(
query: Query,
params: ChainParams,
milestone: Milestone,
user_agent: String,
max_inbound: usize,
dialer: crate::socks::Dialer,
) -> Result<Self, NetError> {
Self::start_inner(
None,
query,
params,
milestone,
user_agent,
max_inbound,
dialer,
)
.await
}

async fn start_inner(
listen: Option<SocketAddr>,
query: Query,
params: ChainParams,
milestone: Milestone,
user_agent: String,
max_inbound: usize,
dialer: crate::socks::Dialer,
) -> Result<Self, NetError> {
let magic = magic_for_params(&params);
let hub = Arc::new(ChainHub::new(query, params, milestone));
hub.ensure_genesis()?;
let cache = hub.cache.clone();
let query = hub.query.clone();
let listener = TcpListener::bind(listen).await?;
let local_addr = listener.local_addr()?;
let shutdown = Arc::new(AtomicBool::new(false));

let (listener, local_addr) = if let Some(addr) = listen {
let listener = TcpListener::bind(addr).await?;
let local_addr = listener.local_addr()?;
(Some(listener), local_addr)
} else {
(None, SocketAddr::from(([127, 0, 0, 1], 0)))
};

let peers = PeerHub::new();
let (dial_tx, mut dial_rx) = tokio::sync::mpsc::unbounded_channel::<DialRequest>();
peers.set_dialer(dial_tx);

let max_inbound = max_inbound.max(1);
let inbound_sem = inbound_semaphore(max_inbound);
let max_inbound = if listener.is_some() {
max_inbound.max(1)
} else {
0
};
let inbound_sem = inbound_semaphore(max_inbound.max(1));
let session_tasks = Arc::new(Mutex::new(Vec::<JoinHandle<()>>::new()));
let accept_task = spawn_inbound_accept(
listener,
local_addr,
hub.clone(),
peers.clone(),
user_agent.clone(),
magic,
max_inbound,
inbound_sem.clone(),
shutdown.clone(),
session_tasks.clone(),
);
let mut tasks = Vec::new();
if let Some(listener) = listener {
tasks.push(spawn_inbound_accept(
listener,
local_addr,
hub.clone(),
peers.clone(),
user_agent.clone(),
magic,
max_inbound.max(1),
inbound_sem.clone(),
shutdown.clone(),
session_tasks.clone(),
));
}

let follow_live = Arc::new(AtomicUsize::new(0));
let dial_hub = hub.clone();
Expand Down Expand Up @@ -158,6 +214,7 @@ impl P2PNode {
push_session_task(&sessions_dial, h);
}
});
tasks.push(dial_task);

Ok(Self {
cache,
Expand All @@ -167,7 +224,7 @@ impl P2PNode {
magic,
shutdown,
follow_live,
tasks: vec![accept_task, dial_task],
tasks,
session_tasks,
peers,
user_agent,
Expand Down Expand Up @@ -806,4 +863,67 @@ mod tests {
node.shutdown().await;
let _ = std::fs::remove_dir_all(&dir);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn p2p_outbound_only_dials_without_listener() {
let _live = live_p2p_lock().await;
let n = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("rbitcoin-outbound-only-{n}"));
std::fs::create_dir_all(dir.join("seed")).unwrap();
std::fs::create_dir_all(dir.join("follow")).unwrap();
let qa = Query::open_or_create_tiny(dir.join("seed")).unwrap();
let qb = Query::open_or_create_tiny(dir.join("follow")).unwrap();
let params = ChainParams::regtest();
let seeder = P2PNode::start_with_agent(
"127.0.0.1:0".parse().unwrap(),
qa,
params.clone(),
Milestone::NONE,
"/rbitcoin:0.1.0(seed)/".into(),
crate::DEFAULT_MAX_INBOUND,
)
.await
.unwrap();
let mut follower = P2PNode::start_outbound_only(
qb,
params,
Milestone::NONE,
"/rbitcoin:0.1.0(follow)/".into(),
0,
crate::socks::Dialer::Direct,
)
.await
.unwrap();
assert_eq!(
follower.local_addr,
"127.0.0.1:0".parse().unwrap(),
"outbound-only must not bind a P2P port"
);
assert_eq!(follower.max_inbound, 0);
let inbound_err = follower
.peers
.addconnection(seeder.local_addr, PeerConnType::Inbound)
.unwrap_err();
assert!(
inbound_err.contains("inbound"),
"addconnection inbound must refuse: {inbound_err}"
);

follower.follow_from(seeder.local_addr).await.unwrap();
let mut linked = false;
for _ in 0..100 {
if follower.follow_live_count() >= 1 && !seeder.peers.snapshot().is_empty() {
linked = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
seeder.shutdown().await;
follower.shutdown().await;
let _ = std::fs::remove_dir_all(&dir);
assert!(linked, "outbound-only follower must handshake the seeder");
}
}
Loading
Loading