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.
96 lines
3.1 KiB
Rust
96 lines
3.1 KiB
Rust
//! Port of OmniVox-server.cpp: echoes each client payload back with the
|
|
//! client id, refusing no client that holds the key.
|
|
|
|
#[path = "common/mod.rs"]
|
|
mod common;
|
|
|
|
use common::{DatagramPayload, PASSWORD, SALT, SERVER_PORT};
|
|
use omnivox::{Config, ConnectionContext, Decision, OmniVox, derive_encrypt_key};
|
|
use std::{
|
|
collections::HashMap,
|
|
sync::{Arc, Mutex},
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
tracing_subscriber::fmt()
|
|
.with_target(false)
|
|
.without_time()
|
|
.init();
|
|
|
|
tracing::info!("Starting server on port {SERVER_PORT}");
|
|
|
|
let server = Arc::new(OmniVox::<DatagramPayload>::new());
|
|
let key = derive_encrypt_key(PASSWORD.as_bytes(), SALT, 17, 8, 1)?;
|
|
server.set_encrypt_key(key);
|
|
|
|
// The C++ example's g_clientHalt map, kept for behavioral parity.
|
|
let client_halt: Arc<Mutex<HashMap<u32, bool>>> = Arc::new(Mutex::new(HashMap::new()));
|
|
|
|
let responder = Arc::clone(&server);
|
|
let received = Arc::new(move |data: &DatagramPayload, ctx: &ConnectionContext| {
|
|
tracing::info!(
|
|
"Received data from client {}: Payload size={}",
|
|
ctx.id,
|
|
data.payload.len()
|
|
);
|
|
if !data.payload.is_empty() {
|
|
tracing::info!(
|
|
"Payload content: {}",
|
|
String::from_utf8_lossy(&data.payload)
|
|
);
|
|
}
|
|
let response = DatagramPayload {
|
|
id: ctx.id as u16,
|
|
v1: 0,
|
|
payload: data.payload.clone(),
|
|
};
|
|
let server = Arc::clone(&responder);
|
|
let id = ctx.id;
|
|
// C++ echoed synchronously inside the callback; the Rust callback is
|
|
// sync, so the echo rides a task — per-connection socket order is
|
|
// still serialized by the client's write lock (contract B3).
|
|
tokio::spawn(async move {
|
|
if server.send_to_client(&response, id).await {
|
|
tracing::info!("Sent response to client {id}");
|
|
} else {
|
|
tracing::info!("Error sending data to client {id}");
|
|
}
|
|
});
|
|
});
|
|
|
|
let halt = Arc::clone(&client_halt);
|
|
let disconnect = Arc::new(move |ctx: &ConnectionContext| {
|
|
tracing::info!(
|
|
"Client {} {}:{} is disconnecting from the server cleaning up objects.",
|
|
ctx.id,
|
|
ctx.ip,
|
|
ctx.port
|
|
);
|
|
*halt.lock().unwrap().entry(ctx.id).or_default() = true;
|
|
});
|
|
|
|
let halt = Arc::clone(&client_halt);
|
|
let connect = Arc::new(move |ctx: &ConnectionContext| {
|
|
*halt.lock().unwrap().entry(ctx.id).or_default() = false;
|
|
tracing::info!("New client has connected - {}:{}", ctx.ip, ctx.port);
|
|
Decision::Allow
|
|
});
|
|
|
|
server
|
|
.start_server(
|
|
SERVER_PORT,
|
|
Config::default(),
|
|
received,
|
|
disconnect,
|
|
connect,
|
|
)
|
|
.await?;
|
|
|
|
tracing::info!("Server started successfully");
|
|
|
|
// C++ slept u32::MAX hours; we serve until killed, like the reference.
|
|
std::future::pending::<()>().await;
|
|
Ok(())
|
|
}
|