Skip to content

Commit bd81446

Browse files
authored
Merge pull request rust-bitcoin#147 from nyonson/traffic-stats-interface
Rework traffic stats
2 parents 1182ecf + 89c8e0f commit bd81446

6 files changed

Lines changed: 195 additions & 118 deletions

File tree

proxy/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ spec = "config_spec.toml"
1414
bitcoin = { version = "0.32.4" }
1515
tokio = { version = "1", features = ["full"] }
1616
hex = { package = "hex-conservative", version = "0.2.0" }
17-
bip324 = { path = "../protocol", features = ["tokio"] }
17+
bip324 = { version = "0.10.0", path = "../protocol", features = ["tokio"] }
1818
configure_me = "0.4.0"
1919
log = "0.4.8"
2020
env_logger = "0.10"

traffic/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Changelog
2+
3+
## v0.1.0
4+
5+
Initial release.
6+
7+
* The heart of the API is the sans-io `TrafficShaper`.
8+
* I/O driving wrappers are also exposed `io::ShapedProtocol` and `futures::ShapedProtocol`.

traffic/README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
A traffic analysis resistance layer for BIP-324 encrypted bitcoin connections. Decoy packet and padding strategies are used to obscure communication patterns, attempting to make it harder for network observers to analyze bitcoin peer-to-peer communication patterns. The library follows a *sans I/O* design, but provides I/O drivers for easy entry points.
44

55
* **High-level I/O** - `io::ShapedProtocol` (sync) and `futures::ShapedProtocol` (async) handle the complete encrypted connection life-cycle with automatic traffic shaping, including handshake, padding, and decoy generation.
6-
* **Lower-level components** - `TrafficShaper` manages the timing and generation of traffic obfuscation.
6+
* **Lower-level components** - `TrafficShaper` manages the timing and generation of traffic obfuscation. It can be used directly if the caller wishes to drive the I/O themselves.
77

88
## Feature Flags
99

@@ -20,17 +20,20 @@ When using the `tokio` feature, the effective MSRV may be higher depending on th
2020

2121
The BIP-324 specification defines *decoy packets* as the primary mechanism for hiding the shape of encrypted traffic. Even with encryption, bitcoin p2p traffic patterns can be highly distinctive to a third party observer. Transactions and blocks have characteristic sizes, message exchanges follow predictable patterns (like initial handshake sequences, pings and pongs), and the timing between messages can reveal protocol state machines. Also, a new block propagates about every ten minutes. An observer monitoring encrypted traffic could potentially identify bitcoin nodes, track transaction propagation, or infer network topology.
2222

23-
This library uses decoy packets in two complementary ways. The first is *padding*, where the library immediately follows genuine packet sends with a decoy packet. This makes it hard for observers to determine where one message ends and another begins. And the second is consistent *background noise*. A separate thread (or task) continuously sends decoy packets, attempting to prevent traffic analysis based on communication patterns and timing.
23+
This library uses decoy packets in two complementary ways, *packet padding* and *cover traffic*. Padding is where the library immediately follows a genuine packet send with a decoy packet. This makes it hard for observers to determine where one message ends and another begins, hiding the genuine packet size. Cover traffic is decoys continuously sent by a separate thread (or task) attempting to prevent traffic analysis based on communication patterns.
2424

2525
The goal is not to make bitcoin traffic completely unidentifiable, that maybe impossible, but rather to significantly increase the computational and analytical costs for observers. This creates a game of economic tradeoffs, how much bandwidth and processing power should nodes spend on decoy bytes versus how much effort observers must expend to identify bitcoin traffic? The ideal strategies would use minimal decoy bytes to impose maximum analysis costs on observers. Finding these efficient strategies remains an open research area, but even simple random noise raises the bar for passive network surveillance.
2626

27+
*No hard analysis has been done yet on the effectiveness of the following strategies to hide bitcoin p2p channel shape.*
28+
2729
### Padding Strategies
2830

2931
* `Disabled` - No padding, genuine packet sizes only (default).
30-
* `Random` - Adds randomly sized decoy packet. Pure noise, no hard analysis has been done on how effective this hides bitcoin p2p channel shape.
32+
* `Random` - Adds randomly sized decoy packet. Pure noise.
33+
* *TODO* `Fixed` - Pad a message size to its nearest power of two. A mixnet strategy to make all messages look identical and avoid a randomness pattern which leaks information.
3134

3235
### Decoy Strategies
3336

3437
* `Disabled` - No automatic decoy packets, genuine writes only (default).
35-
* `Random` - Sends randomly sized decoy packets at random intervals. Pure noise, no hard analysis has been done on how effective this hides bitcoin p2p channel shape.
36-
38+
* `Random` - Sends randomly sized decoy packets at random intervals. Pure noise.
39+
* *TODO* `Mimic` - Instead of hiding the bitcoin p2p traffic, embrace it but send even more bitcoin looking things.This could help break any sort of tracking analysis.

traffic/src/futures.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use bip324::futures::{Protocol, ProtocolReader, ProtocolWriter};
1010
use bip324::io::{Payload, ProtocolError, ProtocolFailureSuggestion};
1111
use bip324::{Network, Role};
1212

13-
use crate::{TrafficConfig, TrafficShaper, TrafficStats, DEFAULT_CHECK_INTERVAL_MS};
13+
use crate::{
14+
AtomicTrafficStats, TrafficConfig, TrafficShaper, TrafficStats, DEFAULT_CHECK_INTERVAL_MS,
15+
};
1416

1517
/// Command sent to the writer task.
1618
struct WriteCommand {
@@ -26,7 +28,7 @@ where
2628
R: AsyncRead + Unpin + Send,
2729
{
2830
reader: ProtocolReader<R>,
29-
stats: Arc<TrafficStats>,
31+
stats: Arc<AtomicTrafficStats>,
3032
}
3133

3234
/// Traffic-shaped async protocol writer half.
@@ -112,17 +114,18 @@ where
112114
where
113115
W: AsyncWrite + Unpin + Send + 'static,
114116
{
115-
let stats = Arc::new(TrafficStats::new());
116-
let mut shaper = TrafficShaper::new(config, stats.clone());
117-
let (garbage, decoys) = shaper.handshake();
117+
let stats = Arc::new(AtomicTrafficStats::new());
118+
let mut shaper = TrafficShaper::new(config);
119+
let (garbage, decoys) = shaper.handshake(&stats);
118120

119121
let protocol = Protocol::new(network, role, garbage, decoys, reader, writer).await?;
120122
let (protocol_reader, protocol_writer) = protocol.into_split();
121123
let (write_tx, write_rx) = mpsc::unbounded_channel();
122124

125+
let stats_clone = stats.clone();
123126
// Task will run as long as the write_tx isn't dropped.
124127
tokio::spawn(async move {
125-
writer_task(write_rx, protocol_writer, shaper).await;
128+
writer_task(write_rx, protocol_writer, shaper, stats_clone).await;
126129
});
127130

128131
Ok(Self {
@@ -196,12 +199,14 @@ impl ShapedProtocolWriter {
196199
}
197200

198201
/// Writer task that handles both genuine writes and decoy generation.
199-
async fn writer_task<W>(
202+
async fn writer_task<W, R>(
200203
mut write_rx: mpsc::UnboundedReceiver<WriteCommand>,
201204
mut writer: ProtocolWriter<W>,
202-
mut shaper: TrafficShaper,
205+
mut shaper: TrafficShaper<R>,
206+
stats: Arc<AtomicTrafficStats>,
203207
) where
204208
W: AsyncWrite + Unpin + Send + 'static,
209+
R: rand::Rng,
205210
{
206211
let mut decoy_interval =
207212
tokio::time::interval(Duration::from_millis(DEFAULT_CHECK_INTERVAL_MS));
@@ -213,7 +218,7 @@ async fn writer_task<W>(
213218
let mut result = Ok(());
214219

215220
// First, send decoy packet if padding is enabled.
216-
if let Some(decoy) = shaper.pad(&cmd.payload) {
221+
if let Some(decoy) = shaper.pad(&cmd.payload, &stats) {
217222
if let Err(e) = writer.write(&decoy).await {
218223
result = Err(e);
219224
}
@@ -232,7 +237,7 @@ async fn writer_task<W>(
232237

233238
// Concurrently send decoys.
234239
_ = decoy_interval.tick() => {
235-
if let Some(decoy) = shaper.decoy() {
240+
if let Some(decoy) = shaper.decoy(&stats) {
236241
// Ignore write errors for decoys - they're best-effort.
237242
let _ = writer.write(&decoy).await;
238243
}

traffic/src/io.rs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
//! Blocking I/O traffic shaping wrapper for BIP-324 protocol.
22
3+
use core::time::Duration;
34
use std::io::{Read, Write};
45
use std::sync::atomic::{AtomicBool, Ordering};
56
use std::sync::{Arc, Mutex};
67
use std::thread;
7-
use std::time::Duration;
88

99
use bip324::io::{Payload, Protocol, ProtocolError, ProtocolReader, ProtocolWriter};
1010
use bip324::{Network, Role};
1111
use rand::Rng;
1212

13-
use crate::{TrafficConfig, TrafficShaper, TrafficStats, DEFAULT_CHECK_INTERVAL_MS};
13+
use crate::{
14+
AtomicTrafficStats, TrafficConfig, TrafficShaper, TrafficStats, DEFAULT_CHECK_INTERVAL_MS,
15+
};
1416

1517
/// Shared writer state protected by a mutex.
1618
struct WriterState<W, R = rand::rngs::StdRng> {
@@ -24,7 +26,7 @@ where
2426
R: Read,
2527
{
2628
reader: ProtocolReader<R>,
27-
stats: Arc<TrafficStats>,
29+
stats: Arc<AtomicTrafficStats>,
2830
}
2931

3032
/// Traffic-shaped protocol writer half.
@@ -33,6 +35,7 @@ where
3335
W: Write,
3436
{
3537
writer_state: Arc<Mutex<WriterState<W>>>,
38+
stats: Arc<AtomicTrafficStats>,
3639
shutdown: Arc<AtomicBool>,
3740
decoy_handle: Option<thread::JoinHandle<()>>,
3841
}
@@ -110,9 +113,9 @@ where
110113
reader: R,
111114
writer: W,
112115
) -> Result<Self, ProtocolError> {
113-
let stats = Arc::new(TrafficStats::new());
114-
let mut shaper = TrafficShaper::new(config, stats.clone());
115-
let (garbage, decoys) = shaper.handshake();
116+
let stats = Arc::new(AtomicTrafficStats::new());
117+
let mut shaper = TrafficShaper::new(config);
118+
let (garbage, decoys) = shaper.handshake(&stats);
116119

117120
let protocol = Protocol::new(network, role, garbage, decoys, reader, writer)?;
118121
let (protocol_reader, protocol_writer) = protocol.into_split();
@@ -126,23 +129,24 @@ where
126129
let shutdown = Arc::new(AtomicBool::new(false));
127130
let writer_state_clone = writer_state.clone();
128131
let shutdown_clone = shutdown.clone();
132+
let stats_clone = stats.clone();
129133
let decoy_handle = thread::spawn(move || {
130-
decoy_thread(writer_state_clone, shutdown_clone);
134+
decoy_thread(writer_state_clone, stats_clone, shutdown_clone);
131135
});
132136

133137
Ok(Self {
134138
reader: ShapedProtocolReader {
135139
reader: protocol_reader,
136-
stats,
140+
stats: stats.clone(),
137141
},
138142
writer: ShapedProtocolWriter {
139143
writer_state,
140144
shutdown,
141145
decoy_handle: Some(decoy_handle),
146+
stats,
142147
},
143148
})
144149
}
145-
146150
/// Read a packet from the protocol.
147151
pub fn read(&mut self) -> Result<Payload, ProtocolError> {
148152
self.reader.read()
@@ -179,7 +183,7 @@ where
179183
/// Write a payload with traffic shaping.
180184
pub fn write(&mut self, payload: &Payload) -> Result<(), ProtocolError> {
181185
let mut state = self.writer_state.lock().unwrap();
182-
if let Some(decoy) = state.shaper.pad(payload) {
186+
if let Some(decoy) = state.shaper.pad(payload, &self.stats) {
183187
state.writer.write(&decoy)?;
184188
}
185189
state.writer.write(payload)?;
@@ -202,8 +206,11 @@ where
202206
}
203207

204208
/// Decoy thread that periodically sends decoy packets.
205-
fn decoy_thread<W, R>(writer_state: Arc<Mutex<WriterState<W, R>>>, shutdown: Arc<AtomicBool>)
206-
where
209+
fn decoy_thread<W, R>(
210+
writer_state: Arc<Mutex<WriterState<W, R>>>,
211+
stats: Arc<AtomicTrafficStats>,
212+
shutdown: Arc<AtomicBool>,
213+
) where
207214
W: Write,
208215
R: Rng,
209216
{
@@ -218,7 +225,7 @@ where
218225
Err(_) => continue,
219226
};
220227

221-
if let Some(decoy) = state.shaper.decoy() {
228+
if let Some(decoy) = state.shaper.decoy(&stats) {
222229
let _ = state.writer.write(&decoy);
223230
}
224231
}

0 commit comments

Comments
 (0)