Skip to content
Open
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
13 changes: 10 additions & 3 deletions OPERATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,8 +366,9 @@ 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` |
| `--no-listen` / `--listen=0` | `listen=0` / `no_listen=` | bind a loopback default; **off** = no clearnet P2P socket |
| `--listen-onion` | `listen_onion=` | **off** — loopback P2P + `ADD_ONION` (`{datadir}/onion/p2p.priv`); needs `--tor-control` and `--max-inbound` > 0 |
| `--no-discover` | `no_discover=` | discover **on**; flag off = no home-IP self-announce; P2P/wallet onions still listed |
| `--only-net NET` | `only_net=` | all nets; repeatable `ipv4` / `ipv6` / `onion` / `i2p` (`cjdns` later) |
| `--connect ADDR` | `connect=` (repeatable) | seeds; `IP:port`, Tor v3 `.onion:port`, or `{52}.b32.i2p:port` |
| `--proxy HOST:PORT` | `proxy=` | unset — SOCKS5 for all P2P outbound |
Expand Down Expand Up @@ -458,7 +459,13 @@ error when the v3 checksum is invalid. The peers file is `rbitcoin-peers-v2`
(v1 IPv4/IPv6 still loads).

`--listen=0` / `--no-listen` starts without a P2P TCP bind (no ISP port
forward). `--max-inbound 0` refuses inbound slots. `--no-discover` does not
forward). `--listen-onion` still binds **127.0.0.1** (ephemeral port) and
`ADD_ONION`s the network default P2P port (8333 / signet 38333 / …) to
that loopback (`{datadir}/onion/p2p.priv`, 0600). Needs `--tor-control`
and `--max-inbound` > 0. `--no-discover` still gossips that onion via
`addrv2` and lists it in `getnetworkinfo.localaddresses`; it does not
gossip `--external-ip`. `--max-inbound 0` refuses `--listen-onion`.
`--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.

Expand Down
14 changes: 13 additions & 1 deletion crates/rbitcoin-net/src/netaddr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::error::NetError;
use bitcoin::p2p::address::{AddrV2, AddrV2Message};
use sha3::{Digest, Sha3_256};
use std::fmt;
use std::net::SocketAddr;
use std::net::{IpAddr, SocketAddr};
use std::str::FromStr;

const B32: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
Expand Down Expand Up @@ -96,6 +96,17 @@ impl NetAddr {
}
}

pub fn to_addrv2(self) -> AddrV2 {
match self {
NetAddr::Ip(s) => match s.ip() {
IpAddr::V4(v4) => AddrV2::Ipv4(v4),
IpAddr::V6(v6) => AddrV2::Ipv6(v6),
},
NetAddr::Onion { pk, .. } => AddrV2::TorV3(pk),
NetAddr::I2p { dest, .. } => AddrV2::I2p(dest),
}
}

pub fn socket_addr(self) -> Option<SocketAddr> {
match self {
NetAddr::Ip(s) => Some(s),
Expand Down Expand Up @@ -372,6 +383,7 @@ mod tests {
};
let a = NetAddr::from_addrv2(&msg).expect("i2p addrv2");
assert_eq!(a, NetAddr::I2p { dest, port: 8333 });
assert_eq!(a.to_addrv2(), AddrV2::I2p(dest));
let s = a.to_string();
assert!(s.ends_with(".b32.i2p:8333"), "{s}");
assert_eq!(s.parse::<NetAddr>().unwrap(), a);
Expand Down
53 changes: 53 additions & 0 deletions crates/rbitcoin-net/src/peer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5600,6 +5600,59 @@ fn no_discover_suppresses_self_announce() {
assert!(hub.rpc_local_addresses().is_empty());
}

#[test]
fn self_announce_onion_not_external_ip() {
use crate::peers::{PeerConnType, PeerHub};
use bitcoin::p2p::address::{AddrV2, Address};
use bitcoin::p2p::message::NetworkMessage;
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);
peer.set_wants_addrv2();
let onion: crate::NetAddr =
"pg6mmjiyjmcrsslvykfwnntlaru7p5svn6y2ymmju6nubxndf4pscryd.onion:18444"
.parse()
.unwrap();
hub.set_p2p_onion(onion.host_str(), onion.port());
hub.set_discover(false);
hub.set_clearnet_listen(false);
hub.set_external_ips(vec![IpAddr::V4(Ipv4Addr::new(42, 42, 42, 42))]);
hub.set_listen_port(18445);
assert!(hub.advertise_local_socket().is_none());
match peer.take_self_announce_msg().expect("onion announce") {
NetworkMessage::AddrV2(v) => {
assert_eq!(v.len(), 1, "{v:?}");
assert!(matches!(v[0].addr, AddrV2::TorV3(_)));
assert_eq!(v[0].port, 18444);
}
other => panic!("expected AddrV2 onion, got {other:?}"),
}
let rows = hub.rpc_local_addresses();
assert_eq!(rows.len(), 1, "{rows:?}");
assert!(rows[0].0.ends_with(".onion"));
assert_eq!(rows[0].1, 18444);
hub.set_discover(true);
assert!(
hub.advertise_local_socket().is_none(),
"onion-only loopback must not gossip --external-ip"
);
}

#[test]
fn redundant_verack_is_ignored_and_logged() {
use bitcoin::consensus::encode::serialize;
Expand Down
102 changes: 83 additions & 19 deletions crates/rbitcoin-net/src/peers.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Live P2P session table for RPC (`getpeerinfo` / `addnode` / `disconnectnode`).

use crate::error::NetError;
use bitcoin::p2p::address::{AddrV2, AddrV2Message, Address};
use bitcoin::p2p::address::{AddrV2Message, Address};
use bitcoin::p2p::message::NetworkMessage;
use bitcoin::p2p::message_network::VersionMessage;
use bitcoin::p2p::ServiceFlags;
Expand Down Expand Up @@ -428,25 +428,56 @@
) {
return None;
}
let sock = self.take_local_addr_due(self.clock_now())?;
rbitcoin_log::debug!("{}", crate::peer::advertising_address_log(sock, self.id));
let now = self.clock_now() as u32;
Some(if self.wants_addrv2() {
NetworkMessage::AddrV2(vec![AddrV2Message {
time: now,
services: crate::peer::local_service_flags(),
addr: match sock.ip() {
IpAddr::V4(v) => AddrV2::Ipv4(v),
IpAddr::V6(v) => AddrV2::Ipv6(v),
},
port: sock.port(),
}])
let hub = self.owner.upgrade()?;
let onion = hub.p2p_onion();
let sock = hub.advertise_local_socket();
if onion.is_none() && sock.is_none() {
return None;
}
const DAY: u64 = 24 * 60 * 60;

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace * with / in LivePeer::take_self_announce_msg

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace * with + in LivePeer::take_self_announce_msg

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace * with / in LivePeer::take_self_announce_msg

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace * with + in LivePeer::take_self_announce_msg
let now = self.clock_now();
let prev = self.next_local_addr_send.load(Ordering::Relaxed);
if prev != 0 && now < prev {

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace < with <= in LivePeer::take_self_announce_msg

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace < with > in LivePeer::take_self_announce_msg

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace < with == in LivePeer::take_self_announce_msg

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace != with == in LivePeer::take_self_announce_msg

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

View workflow job for this annotation

GitHub Actions / mutants-pr

Missed mutant

replace && with || in LivePeer::take_self_announce_msg
return None;
}
let next = now.saturating_add(DAY).max(1);
self.next_local_addr_send
.compare_exchange(prev, next, Ordering::Relaxed, Ordering::Relaxed)
.ok()?;
let services = crate::peer::local_service_flags();
let t = now as u32;
if self.wants_addrv2() {
let mut v = Vec::new();
if let Some(addr) = onion {
rbitcoin_log::debug!("{}", crate::peer::advertising_address_log(addr, self.id));
v.push(AddrV2Message {
time: t,
services,
addr: addr.to_addrv2(),
port: addr.port(),
});
}
if let Some(sock) = sock {
rbitcoin_log::debug!("{}", crate::peer::advertising_address_log(sock, self.id));
v.push(AddrV2Message {
time: t,
services,
addr: crate::NetAddr::Ip(sock).to_addrv2(),
port: sock.port(),
});
}
if v.is_empty() {
return None;
}
Some(NetworkMessage::AddrV2(v))
} else {
NetworkMessage::Addr(vec![(
now,
Address::new(&sock, crate::peer::local_service_flags()),
)])
})
let sock = sock?;
rbitcoin_log::debug!("{}", crate::peer::advertising_address_log(sock, self.id));
Some(NetworkMessage::Addr(vec![(
t,
Address::new(&sock, services),
)]))
}
}

pub fn queue_self_announce_if_due(&self) {
Expand Down Expand Up @@ -1091,10 +1122,13 @@
/// Addresses we advertise (`getnetworkinfo.localaddresses`).
external_ips: Mutex<Vec<IpAddr>>,
wallet_onions: Mutex<Vec<(String, u16)>>,
p2p_onion: Mutex<Option<(String, u16)>>,
/// P2P listen port used with advertised external IPs.
listen_port: AtomicU16,
/// Core `-discover`. Off: never self-announce, even with `--external-ip`.
discover: AtomicBool,
/// Clearnet P2P bind (not onion-only loopback). Needed to gossip `--external-ip`.
clearnet_listen: 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 @@ -1169,8 +1203,10 @@
peer_timeout_secs: AtomicU64::new(60),
external_ips: Mutex::new(Vec::new()),
wallet_onions: Mutex::new(Vec::new()),
p2p_onion: Mutex::new(None),
listen_port: AtomicU16::new(0),
discover: AtomicBool::new(true),
clearnet_listen: AtomicBool::new(true),
asmap: Mutex::new(None),
mempool: Mutex::new(None),
net_perms: Mutex::new(crate::net_permissions::NetPermTable::default()),
Expand Down Expand Up @@ -1233,6 +1269,23 @@
.push((host, port));
}

pub fn set_p2p_onion(&self, host: String, port: u16) {
*self.p2p_onion.lock().unwrap_or_else(|e| e.into_inner()) = Some((host, port));
}

pub fn p2p_onion(&self) -> Option<crate::NetAddr> {
let (host, port) = self
.p2p_onion
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()?;
format!("{host}:{port}").parse().ok()
}

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

pub fn set_listen_port(&self, port: u16) {
self.listen_port.store(port, Ordering::Relaxed);
}
Expand All @@ -1252,6 +1305,14 @@
.cloned()
.map(|(address, port)| (address, port, LOCAL_MANUAL))
.collect();
if let Some((host, port)) = self
.p2p_onion
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
{
rows.push((host, port, LOCAL_MANUAL));
}
if !self.discover.load(Ordering::Relaxed) {
return rows;
}
Expand All @@ -1275,6 +1336,9 @@
if !self.discover.load(Ordering::Relaxed) {
return None;
}
if !self.clearnet_listen.load(Ordering::Relaxed) {
return None;
}
let port = self.listen_port.load(Ordering::Relaxed);
if port == 0 {
return None;
Expand Down
82 changes: 82 additions & 0 deletions crates/rbitcoin-net/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,4 +956,86 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
assert!(linked, "outbound-only follower must handshake the seeder");
}

#[tokio::test]
async fn listen_onion_binds_loopback_when_nolisten() {
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-listen-onion-bind-{n}"));
std::fs::create_dir_all(&dir).unwrap();
let q = Query::open_or_create_tiny(&dir).unwrap();
let node = P2PNode::start_with_dialer(
"127.0.0.1:0".parse().unwrap(),
q,
ChainParams::regtest(),
Milestone::NONE,
"/rbitcoin:0.1.0(onion)/".into(),
crate::DEFAULT_MAX_INBOUND,
crate::socks::Dialer::Direct,
)
.await
.unwrap();
assert_eq!(node.local_addr.ip(), std::net::Ipv4Addr::LOCALHOST);
assert_ne!(node.local_addr.port(), 0);
node.shutdown().await;
let _ = std::fs::remove_dir_all(&dir);
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn listen_onion_loopback_inbound_handshake() {
use crate::peer::{connect_and_handshake_timed, HandshakePolicy};
use bitcoin::p2p::Magic;
use tokio::net::TcpStream;

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-listen-onion-hs-{n}"));
std::fs::create_dir_all(&dir).unwrap();
let q = Query::open_or_create_tiny(&dir).unwrap();
let node = P2PNode::start_with_dialer(
"127.0.0.1:0".parse().unwrap(),
q,
ChainParams::regtest(),
Milestone::NONE,
"/rbitcoin:0.1.0(onion)/".into(),
crate::DEFAULT_MAX_INBOUND,
crate::socks::Dialer::Direct,
)
.await
.unwrap();
let stream = TcpStream::connect(node.local_addr).await.unwrap();
let _hs = connect_and_handshake_timed(
Duration::from_secs(5),
stream,
Magic::REGTEST,
node.local_addr,
node.local_addr,
0,
false,
"/rbitcoin:test/",
HandshakePolicy::plain(),
)
.await
.unwrap();
let mut inbound = false;
for _ in 0..200 {
if node.peers.snapshot().iter().any(|p| p.inbound) {
inbound = true;
break;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
node.shutdown().await;
let _ = std::fs::remove_dir_all(&dir);
assert!(
inbound,
"loopback client must show as inbound in getpeerinfo"
);
}
}
Loading
Loading