Red-team review of the (byte-exact) v1 port found replay, cross-session frame portability, pre-auth allocation amplification, and a symmetric key that let either peer forge as itself — flaws inherent to the v1 wire format. v2 closes them: - fixed 69-byte OVX2 hello; server validates before replying; v1 and wrong-version peers see an immediate close - HKDF(ephemeral X25519 DH, PSK, transcript) -> direction-separated session keys; forward secrecy per connection - strictly increasing u64 sequence nonces: any replay or reorder drops the connection (regression tests in tests/protocol_v2.rs) - Config: message-size cap (64 KiB default), connection semaphore, idle/body-stall/handshake/send timeouts on both roles (client-side timers retire the old no-client-timeouts quirk) - message processing inline per connection; user callbacks panic- isolated; poison-tolerant mutexes; demo key refused in release builds - PROTOCOL-V2.md supersedes PARITY-CONTRACT.md (retired, kept as port history); Cereal payload encoding is unchanged Payload serialization remains byte-exact with the original C++ (the golden scrypt/cereal vectors still govern). Zero unsafe. Verified live: server<->client handshake, encrypted echo, clean teardown. Cuts over from the C++ implementation, which is deleted (phase 5 of the port contract); it remains in the original gitea repository.
78 lines
2.4 KiB
Rust
78 lines
2.4 KiB
Rust
//! Port of OmniVox-client.cpp: connects, sends "Hello", prints what the
|
|
//! server echoes, then stops after 15 seconds like the C++ example.
|
|
|
|
#[path = "common/mod.rs"]
|
|
mod common;
|
|
|
|
use common::{DatagramPayload, PASSWORD, SALT, SERVER_ADDR, SERVER_PORT};
|
|
use omnivox::{Config, ConnectionContext, ConnectionStatus, OmniVox, derive_encrypt_key};
|
|
use std::sync::Arc;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
tracing_subscriber::fmt()
|
|
.with_target(false)
|
|
.without_time()
|
|
.init();
|
|
|
|
tracing::info!("Connecting to server at {SERVER_ADDR}:{SERVER_PORT}");
|
|
|
|
let client = Arc::new(OmniVox::<DatagramPayload>::new());
|
|
let key = derive_encrypt_key(PASSWORD.as_bytes(), SALT, 17, 8, 1)?;
|
|
client.set_encrypt_key(key);
|
|
|
|
let data_cb = Arc::new(|_data: &DatagramPayload, ctx: &ConnectionContext| {
|
|
tracing::info!("Server ID: {}, sent data!", ctx.id);
|
|
});
|
|
|
|
let connector = Arc::clone(&client);
|
|
let connection_cb = Arc::new(move |ctx: &ConnectionContext| match ctx.connected {
|
|
ConnectionStatus::Connected => {
|
|
tracing::info!(
|
|
"[onConnect] Successfully connected to: {}:{}",
|
|
ctx.ip,
|
|
ctx.port
|
|
);
|
|
let client = Arc::clone(&connector);
|
|
tokio::spawn(async move {
|
|
let payload = DatagramPayload {
|
|
id: 1,
|
|
v1: 0,
|
|
payload: b"Hello".to_vec(),
|
|
};
|
|
if client.send_to_server(&payload).await {
|
|
tracing::info!("Sent initial data to server");
|
|
}
|
|
});
|
|
}
|
|
ConnectionStatus::ConnectionFailed => {
|
|
tracing::info!(
|
|
"[onConnect] Connection failed or timed out to: {}:{}",
|
|
ctx.ip,
|
|
ctx.port
|
|
);
|
|
}
|
|
_ => {
|
|
tracing::info!(
|
|
"[onConnect] Unexpected connection status to: {}:{}",
|
|
ctx.ip,
|
|
ctx.port
|
|
);
|
|
}
|
|
});
|
|
|
|
client
|
|
.connect_to_server(
|
|
SERVER_ADDR,
|
|
SERVER_PORT,
|
|
Config::default(),
|
|
Some(data_cb),
|
|
connection_cb,
|
|
)
|
|
.await?;
|
|
|
|
tokio::time::sleep(std::time::Duration::from_secs(15)).await;
|
|
client.stop().await;
|
|
Ok(())
|
|
}
|