From c226acb2db2e69bfd66ac9a056be986c7169ed4f Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 18 Aug 2025 14:38:37 -0700 Subject: [PATCH 1/2] fix: require tokio feature for bufreader example --- protocol/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index f6cf6bc..846947c 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -30,3 +30,8 @@ chacha20-poly1305 = { version = "0.1.1", default-features = false } bitcoind = { package = "corepc-node", version = "0.7.1", default-features = false, features = ["26_0","download"] } hex = { package = "hex-conservative", version = "0.2.0" } tokio = { version = "1", features = ["io-util", "net", "rt-multi-thread", "macros"] } + +# Examples that require tokio features +[[example]] +name = "bufreader" +required-features = ["tokio"] From 4693e67eb31aae83b0be59b3923b1c88ab1d2b04 Mon Sep 17 00:00:00 2001 From: Nick Johnson Date: Mon, 18 Aug 2025 14:27:53 -0700 Subject: [PATCH 2/2] fix: split proxy off into its own repository It was a bit awkward having a binary in the workspace. Binaries want specific versions for dependencies, whereas libraries work on ranges of dependency constraints. Splitting it off to simplify the dependency tree. --- Cargo.toml | 4 +- README.md | 5 +- proxy/CHANGELOG.md | 9 -- proxy/Cargo.toml | 23 ----- proxy/README.md | 29 ------ proxy/build.rs | 3 - proxy/config_spec.toml | 28 ------ proxy/src/bin/proxy.rs | 177 ---------------------------------- proxy/src/lib.rs | 211 ----------------------------------------- 9 files changed, 6 insertions(+), 483 deletions(-) delete mode 100644 proxy/CHANGELOG.md delete mode 100644 proxy/Cargo.toml delete mode 100644 proxy/README.md delete mode 100644 proxy/build.rs delete mode 100644 proxy/config_spec.toml delete mode 100644 proxy/src/bin/proxy.rs delete mode 100644 proxy/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 83a03bd..5df354e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,4 @@ [workspace] -members = ["protocol", "proxy", "protocol/fuzz", "traffic"] -default-members = ["protocol", "proxy", "traffic"] +members = ["protocol", "protocol/fuzz", "traffic"] +default-members = ["protocol", "traffic"] resolver = "2" diff --git a/README.md b/README.md index eb9e2f9..e75191f 100644 --- a/README.md +++ b/README.md @@ -15,5 +15,8 @@ BIP-324 - "V2" - encrypted communication protects against the above issues incre ## Crates * [`protocol`](./protocol) - Exports the `bip324` client library. -* [`proxy`](./proxy) - A small side-car application to enable V2 communication for V1-only applications. * [`traffic`](./traffic) - Traffic shape hiding layer over the base client. + +## Proxy + +A BIP-324 proxy application is available at [nyonson/bip324-proxy](https://github.com/nyonson/bip324-proxy). diff --git a/proxy/CHANGELOG.md b/proxy/CHANGELOG.md deleted file mode 100644 index 036172b..0000000 --- a/proxy/CHANGELOG.md +++ /dev/null @@ -1,9 +0,0 @@ -# Changelog - -## v0.4.0 - -* Pin the protocol crate version for stability. - -## v0.3.0 - -* The new `--v1-fallback` flag allows a proxy connection to fallback to the V1 protocol if a remote doesn't support V2. diff --git a/proxy/Cargo.toml b/proxy/Cargo.toml deleted file mode 100644 index 068a576..0000000 --- a/proxy/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "bip324-proxy" -version = "0.4.0" -edition = "2021" -license = "CC0-1.0" -description = "BIP-324 proxy enabling v1-only clients to use the v2 bitcoin p2p protocol" -repository = "https://github.com/rust-bitcoin/bip324" -readme = "README.md" - -[package.metadata.configure_me] -spec = "config_spec.toml" - -[dependencies] -bitcoin = { version = "0.32.4" } -tokio = { version = "1", features = ["full"] } -hex = { package = "hex-conservative", version = "0.2.0" } -bip324 = { version = "0.10.0", path = "../protocol", features = ["tokio"] } -configure_me = "0.4.0" -log = "0.4.8" -env_logger = "0.10" - -[build-dependencies] -configure_me_codegen = { version = "0.4.8", default-features = false } diff --git a/proxy/README.md b/proxy/README.md deleted file mode 100644 index 19d6b9f..0000000 --- a/proxy/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# V2 Proxy - -A proxy sidecar process which allows V1-only clients to communicate over the V2 protocol. The process listens on port `1324` for V1 connections and requires the V1 client to send along the remote peer's IP address in the `addr_recv` field. - -## Running the Proxy - -`cargo run --bin proxy` - -The `--v1-fallback=true` flag can be used to fallback to the V1 protocol if the remote client does not support V2. - -## Testing with Nakamoto - -[Nakamoto](https://github.com/cloudhead/nakamoto) is a BIP-157/BIP-158 Light Client that communicates over the Bitcoin P2P network. With a single change, Nakamoto may be modified to use the proxy. This patch hardcodes Nakamoto to connect to the localhost on port 1324 where the proxy should be running. - -```diff -diff --git a/net/poll/src/reactor.rs b/net/poll/src/reactor.rs - ---- a/net/poll/src/reactor.rs -+++ b/net/poll/src/reactor.rs -@@ -468,7 +468,7 @@ fn dial(addr: &net::SocketAddr) -> Result { - sock.set_write_timeout(Some(WRITE_TIMEOUT))?; - sock.set_nonblocking(true)?; - -- match sock.connect(&(*addr).into()) { -+ match sock.connect(&net::SocketAddr::from(([127, 0, 0, 1], 1324)).into()) { - Ok(()) => {} - Err(e) if e.raw_os_error() == Some(libc::EINPROGRESS) => {} - Err(e) if e.raw_os_error() == Some(libc::EALREADY) => { -``` diff --git a/proxy/build.rs b/proxy/build.rs deleted file mode 100644 index f6d955c..0000000 --- a/proxy/build.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - configure_me_codegen::build_script_auto().unwrap_or_else(|e| e.report_and_exit()); -} diff --git a/proxy/config_spec.toml b/proxy/config_spec.toml deleted file mode 100644 index d8fa299..0000000 --- a/proxy/config_spec.toml +++ /dev/null @@ -1,28 +0,0 @@ -[general] -env_prefix = "BIP324_PROXY" -conf_file_param = "conf" -conf_dir_param = "conf_dir" - -[[param]] -name = "bind_port" -type = "u16" -default = "1324" -doc = "The port to listen on" - -[[param]] -name = "bind_host" -type = "String" -default = "\"127.0.0.1\".into()" -doc = "The address to listen on" - -[[param]] -name = "network" -type = "String" -default = "\"bitcoin\".into()" -doc = "The bitcoin network to operate on" - -[[param]] -name = "v1_fallback" -type = "bool" -default = "false" -doc = "Fallback to the V1 protocol if V2 fails" diff --git a/proxy/src/bin/proxy.rs b/proxy/src/bin/proxy.rs deleted file mode 100644 index 8df624b..0000000 --- a/proxy/src/bin/proxy.rs +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -// configure_me generated code has lint issues. -#![allow(clippy::uninlined_format_args)] - -use std::str::FromStr; - -use bip324::{ - futures::Protocol, - io::{Payload, ProtocolFailureSuggestion}, - serde::{deserialize, serialize}, - PacketType, Role, -}; -use bip324_proxy::{V1ProtocolReader, V1ProtocolWriter}; -use bitcoin::Network; -use log::{debug, error, info}; -use tokio::{ - net::{TcpListener, TcpStream}, - select, -}; - -configure_me::include_config!(); - -/// A v1 to v1 proxy for use as a fallback. -async fn v1_proxy(client: TcpStream, network: Network) -> Result<(), bip324_proxy::Error> { - let remote_ip = bip324_proxy::peek_addr(&client, network).await?; - - info!("Initialing remote connection {}.", remote_ip); - let remote = TcpStream::connect(remote_ip).await?; - - let (client_reader, client_writer) = client.into_split(); - let (remote_reader, remote_writer) = remote.into_split(); - - let mut v1_client_reader = V1ProtocolReader::new(client_reader); - let mut v1_client_writer = V1ProtocolWriter::new(network, client_writer); - let mut v1_remote_reader = V1ProtocolReader::new(remote_reader); - let mut v1_remote_writer = V1ProtocolWriter::new(network, remote_writer); - - info!("Setting up V1 proxy."); - - loop { - select! { - result = v1_client_reader.read() => { - let msg = result?; - debug!( - "Read {} message from client, writing to remote.", - msg.command() - ); - v1_remote_writer.write(msg).await?; - }, - result = v1_remote_reader.read() => { - let msg = result?; - debug!( - "Read {} message from remote, writing to client.", - msg.command() - ); - v1_client_writer.write(msg).await?; - }, - } - } -} - -/// Validate and bootstrap a v1 to v2 proxy connection. -async fn v2_proxy( - client: TcpStream, - network: Network, - v1_fallback: bool, -) -> Result<(), bip324_proxy::Error> { - let remote_ip = bip324_proxy::peek_addr(&client, network) - .await - .expect("peek address"); - - info!("Reaching out to {}.", remote_ip); - let remote = TcpStream::connect(remote_ip) - .await - .expect("connect to remote"); - - info!("Initiating handshake."); - let (remote_reader, remote_writer) = remote.into_split(); - - let protocol = match Protocol::new( - network, - Role::Initiator, - None, - None, - remote_reader, - remote_writer, - ) - .await - { - Ok(p) => p, - Err(bip324::io::ProtocolError::Io(_, ProtocolFailureSuggestion::RetryV1)) - if v1_fallback => - { - info!("V2 protocol failed, falling back to V1..."); - return v1_proxy(client, network).await; - } - Err(e) => return Err(e.into()), - }; - - let (client_reader, client_writer) = client.into_split(); - let mut v1_client_reader = V1ProtocolReader::new(client_reader); - let mut v1_client_writer = V1ProtocolWriter::new(network, client_writer); - - let (mut v2_remote_reader, mut v2_remote_writer) = protocol.into_split(); - - info!("Setting up V2 proxy."); - - loop { - select! { - result = v1_client_reader.read() => { - let msg = result?; - debug!( - "Read {} message from client, writing to remote.", - msg.command() - ); - - v2_remote_writer - .write(&Payload::genuine(serialize(msg))) - .await - .expect("write to remote"); - }, - result = v2_remote_reader.read() => { - let payload = result.expect("read packet"); - // Ignore decoy packets. - if payload.packet_type() == PacketType::Genuine { - let msg = deserialize(payload.contents()) - .expect("deserializable contents into network message"); - debug!( - "Read {} message from remote, writing to client.", - msg.command() - ); - v1_client_writer.write(msg).await.expect("write to client"); - } - }, - } - } -} - -#[tokio::main] -async fn main() { - env_logger::init(); - - let (config, _) = Config::including_optional_config_files::<&[&str]>(&[]).unwrap_or_exit(); - let network = Network::from_str(&config.network).expect("parse-able network"); - - let local = TcpListener::bind((&*config.bind_host, config.bind_port)) - .await - .expect("Failed to bind to proxy port."); - info!( - "Listening for connections on {}:{} with V1 fallback {}.", - config.bind_host, - config.bind_port, - if config.v1_fallback { - "enabled" - } else { - "disabled" - }, - ); - loop { - let (stream, _) = local - .accept() - .await - .expect("Failed to accept inbound connection."); - // Spawn a new task per connection. - tokio::spawn(async move { - match v2_proxy(stream, network, config.v1_fallback).await { - Ok(_) => { - info!("Proxy establilshed."); - } - Err(e) => { - error!("Connection ended with error: {e}."); - } - }; - }); - } -} diff --git a/proxy/src/lib.rs b/proxy/src/lib.rs deleted file mode 100644 index cd8fa6f..0000000 --- a/proxy/src/lib.rs +++ /dev/null @@ -1,211 +0,0 @@ -// SPDX-License-Identifier: CC0-1.0 - -//! Helper functions for bitcoin p2p proxies. -//! -//! The V1 and V2 p2p protocols have different header encodings, so a proxy has to do -//! a little more work than just encrypt/decrypt. The [`NetworkMessage`] -//! type is the intermediate state for messages. The V1 side can use the RawNetworkMessage wrapper, but the V2 side -//! cannot since things like the checksum are not relevant (those responsibilites are pushed -//! onto the transport in V2). - -use std::fmt; -use std::net::SocketAddr; - -use bitcoin::consensus::{Decodable, Encodable}; -use bitcoin::p2p::message::{NetworkMessage, RawNetworkMessage}; -use bitcoin::p2p::Address; -use bitcoin::Network; -use hex::prelude::*; -use log::debug; -use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tokio::net::TcpStream; - -/// All V1 messages have a 24 byte header. -const V1_HEADER_BYTES: usize = 24; -/// Hex encoding of ascii version command. -const VERSION_COMMAND: [u8; 12] = [ - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, -]; - -/// An error occured while establishing the proxy connection or during the main loop. -#[derive(Debug)] -pub enum Error { - WrongNetwork, - WrongCommand, - Serde, - Io(std::io::Error), - Protocol(bip324::io::ProtocolError), -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::WrongNetwork => write!(f, "recieved message on wrong network"), - Error::Io(e) => write!(f, "network {e:?}"), - Error::WrongCommand => write!(f, "recieved message with wrong command"), - Error::Protocol(e) => write!(f, "protocol error {e:?}"), - Error::Serde => write!(f, "unable to serialize command"), - } - } -} - -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Error::WrongNetwork => None, - Error::WrongCommand => None, - Error::Serde => None, - Error::Io(e) => Some(e), - Error::Protocol(e) => Some(e), - } - } -} - -impl From for Error { - fn from(e: bip324::io::ProtocolError) -> Self { - Error::Protocol(e) - } -} - -// Convert IO errors. -impl From for Error { - fn from(e: std::io::Error) -> Self { - Error::Io(e) - } -} - -/// Peek the input stream and pluck the remote address based on the version message. -pub async fn peek_addr(client: &TcpStream, network: Network) -> Result { - // Peek the first 70 bytes, 24 for the header and 46 for the first part of the version message. - let mut peek_bytes = [0; 70]; - client.peek(&mut peek_bytes).await?; - - // Check network magic. - debug!("Got magic: {}", &peek_bytes[0..4].to_lower_hex_string()); - if network.magic().to_bytes().ne(&peek_bytes[0..4]) { - return Err(Error::WrongNetwork); - } - - // Check command. - debug!("Got command: {}", &peek_bytes[4..16].to_lower_hex_string()); - if VERSION_COMMAND.ne(&peek_bytes[4..16]) { - return Err(Error::WrongCommand); - } - - // Pull off address from the addr_recv field of the version message. - let mut addr_bytes = &peek_bytes[44..]; - let remote_addr = Address::consensus_decode(&mut addr_bytes).expect("network address bytes"); - let socket_addr = remote_addr.socket_addr().expect("ip address"); - - Ok(socket_addr) -} - -/// State machine of an asynchronous helps make functions cancellation safe. -#[derive(Debug)] -enum ReadState { - ReadingLength { - header_bytes: [u8; V1_HEADER_BYTES], - bytes_read: usize, - }, - ReadingPayload { - packet_bytes: Vec, - bytes_read: usize, - }, -} - -impl Default for ReadState { - fn default() -> Self { - ReadState::ReadingLength { - header_bytes: [0u8; V1_HEADER_BYTES], - bytes_read: 0, - } - } -} - -/// Read messages on the V1 protocol. -pub struct V1ProtocolReader { - input: T, - state: ReadState, -} - -impl V1ProtocolReader { - /// New V1 message reader. - pub fn new(input: T) -> Self { - Self { - input, - state: ReadState::default(), - } - } - - /// Read a v1 message off of the input stream. - pub async fn read(&mut self) -> Result { - loop { - match &mut self.state { - ReadState::ReadingLength { - header_bytes, - bytes_read, - } => { - while *bytes_read < V1_HEADER_BYTES { - let n = self.input.read(&mut header_bytes[*bytes_read..]).await?; - *bytes_read += n; - } - - let payload_len = u32::from_le_bytes( - header_bytes[16..20] - .try_into() - .expect("4 header length bytes"), - ) as usize; - - let mut packet_bytes = vec![0u8; V1_HEADER_BYTES + payload_len]; - packet_bytes[..V1_HEADER_BYTES].copy_from_slice(header_bytes); - - self.state = ReadState::ReadingPayload { - packet_bytes, - bytes_read: V1_HEADER_BYTES, - }; - } - ReadState::ReadingPayload { - packet_bytes, - bytes_read, - } => { - while *bytes_read < packet_bytes.len() { - let n = self.input.read(&mut packet_bytes[*bytes_read..]).await?; - *bytes_read += n; - } - - let message = RawNetworkMessage::consensus_decode(&mut &packet_bytes[..]) - .expect("decode v1"); - - self.state = ReadState::default(); - // The RawNetworkMessage type doesn't have a nice way to pull - // out the payload, so using a clone here. - return Ok(message.payload().clone()); - } - } - } - } -} - -/// Write messages on the V1 protocol. -pub struct V1ProtocolWriter { - network: Network, - output: T, -} - -impl V1ProtocolWriter { - /// New V1 message writer. - pub fn new(network: Network, output: T) -> Self { - Self { network, output } - } - - /// Write message to the output stream using v1. - pub async fn write(&mut self, msg: NetworkMessage) -> Result<(), Error> { - let raw = RawNetworkMessage::new(self.network.magic(), msg); - let mut buffer = vec![]; - raw.consensus_encode(&mut buffer) - .map_err(|_| Error::Serde)?; - self.output.write_all(&buffer[..]).await?; - self.output.flush().await?; - Ok(()) - } -}