Milestone 1

This commit is contained in:
sam
2026-05-03 11:24:13 +02:00
parent 7dbb940107
commit 43483c2145
16 changed files with 1576 additions and 20 deletions

1140
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

7
Cargo.toml Normal file
View File

@@ -0,0 +1,7 @@
[workspace]
members = [
"core_protocol",
"server_node",
"client_node"
]
resolver = "2"

View File

@@ -2,31 +2,31 @@
**Goal:** Initialize the project and establish the shared language between client and server. **Goal:** Initialize the project and establish the shared language between client and server.
### 1. Workspace Setup ### 1. Workspace Setup
- [ ] Initialize the root Cargo workspace: `cargo init --vcs none` (delete `src/`). Create a root `Cargo.toml` with `[workspace] members = ["core_protocol", "server_node", "client_node"]`. - [x] Initialize the root Cargo workspace: `cargo init --vcs none` (delete `src/`). Create a root `Cargo.toml` with `[workspace] members = ["core_protocol", "server_node", "client_node"]`.
- [ ] **AI Context Trap (File Structure):** Strictly adhere to the directory layout and module hierarchy defined in `File_Structure.md`. Do not invent new file paths or module names; map every new crate and file exactly to the blueprint. - [x] **AI Context Trap (File Structure):** Strictly adhere to the directory layout and module hierarchy defined in `File_Structure.md`. Do not invent new file paths or module names; map every new crate and file exactly to the blueprint.
- [ ] Create crates: `cargo new --lib core_protocol`, `cargo new --bin server_node`, `cargo new --bin client_node`. - [x] Create crates: `cargo new --lib core_protocol`, `cargo new --bin server_node`, `cargo new --bin client_node`.
- [ ] Add strict lints (`#![forbid(unsafe_code)]`, etc.) to the root workspace or individual `lib.rs`/`main.rs` files. - [x] Add strict lints (`#![forbid(unsafe_code)]`, etc.) to the root workspace or individual `lib.rs`/`main.rs` files.
- [ ] **Dependencies (`core_protocol`):** Add `serde`, `bincode`, `uuid`, `chrono`, `thiserror`, `secrecy` (for zeroing sensitive keys). - [x] **Dependencies (`core_protocol`):** Add `serde`, `bincode`, `uuid`, `chrono`, `thiserror`, `secrecy` (for zeroing sensitive keys).
- [ ] **Dependencies (`server_node`):** Add `tokio` (full), `tracing`, `tracing-subscriber`, `anyhow`, `dashmap`. - [x] **Dependencies (`server_node`):** Add `tokio` (full), `tracing`, `tracing-subscriber`, `anyhow`, `dashmap`.
- [ ] **Dependencies (`client_node`):** Add `tokio` (rt-multi-thread), `tracing`, `tracing-subscriber`, `anyhow`. - [x] **Dependencies (`client_node`):** Add `tokio` (rt-multi-thread), `tracing`, `tracing-subscriber`, `anyhow`.
### 2. Protocol Definitions (`core_protocol`) ### 2. Protocol Definitions (`core_protocol`)
- [ ] Create `src/tcp_events.rs`. Define `enum TcpEvent { AuthRequest { username: String, ... }, AuthResponse { session_token: u32, ... }, ChannelJoin { ... }, ChatMessage { ... } }` with `#[derive(Serialize, Deserialize)]`. - [x] Create `src/tcp_events.rs`. Define `enum TcpEvent { AuthRequest { username: String, ... }, AuthResponse { session_token: u32, ... }, ChannelJoin { ... }, ChatMessage { ... } }` with `#[derive(Serialize, Deserialize)]`.
- [ ] Create `src/udp_packets.rs`. Define `struct VoicePacketHeader { pub session_token: u32, pub sequence_num: u64, pub timestamp: u64 }` with `#[derive(Serialize, Deserialize)]`. - [x] Create `src/udp_packets.rs`. Define `struct VoicePacketHeader { pub session_token: u32, pub sequence_num: u64, pub timestamp: u64 }` with `#[derive(Serialize, Deserialize)]`.
- [ ] Create `src/constants.rs`. Define `pub const SAMPLE_RATE: u32 = 48000;`, `pub const FRAME_SIZE: usize = 960;`, `pub const TCP_PORT: u16 = 8080;`. - [x] Create `src/constants.rs`. Define `pub const SAMPLE_RATE: u32 = 48000;`, `pub const FRAME_SIZE: usize = 960;`, `pub const TCP_PORT: u16 = 8080;`.
### 3. TCP Handshake (`server_node` & `client_node`) ### 3. TCP Handshake (`server_node` & `client_node`)
- [ ] **Server:** In `server_node/src/main.rs`, initialize `tokio::net::TcpListener::bind("0.0.0.0:8080")`. - [x] **Server:** In `server_node/src/main.rs`, initialize `tokio::net::TcpListener::bind("0.0.0.0:8080")`.
- [ ] **Server:** Spawn a new `tokio::spawn(async move { ... })` for each incoming `TcpStream`. - [x] **Server:** Spawn a new `tokio::spawn(async move { ... })` for each incoming `TcpStream`.
- [ ] **Client:** In `client_node/src/network/control.rs`, implement `TcpStream::connect("127.0.0.1:8080")`. - [x] **Client:** In `client_node/src/network/control.rs`, implement `TcpStream::connect("127.0.0.1:8080")`.
- [ ] **AI Context Trap (TCP Framing):** Raw TCP streams suffer from fragmentation. Do NOT attempt to manually buffer bytes. You must use `tokio_util::codec::LengthDelimitedCodec` (with `tokio_serde` and `bincode`) to abstract the frame boundaries cleanly. - [x] **AI Context Trap (TCP Framing):** Raw TCP streams suffer from fragmentation. Do NOT attempt to manually buffer bytes. You must use `tokio_util::codec::LengthDelimitedCodec` (with `tokio_serde` and `bincode`) to abstract the frame boundaries cleanly.
### 4. Login Logic & State ### 4. Login Logic & State
- [ ] **Server State:** Create `server_node/src/state.rs`. Define a `DashMap<u32, UserState>` to store active session tokens. - [x] **Server State:** Create `server_node/src/state.rs`. Define a `DashMap<u32, UserState>` to store active session tokens.
- [ ] **Authentication Flow:** Client sends `TcpEvent::AuthRequest`. Server generates a random `u32` session token, stores it in `DashMap`, and returns `TcpEvent::AuthResponse`. - [x] **Authentication Flow:** Client sends `TcpEvent::AuthRequest`. Server generates a random `u32` session token, stores it in `DashMap`, and returns `TcpEvent::AuthResponse`.
- [ ] **Validation:** Ensure the server actively drops the connection if the client sends invalid or excessively large payloads. - [x] **Validation:** Ensure the server actively drops the connection if the client sends invalid or excessively large payloads.
### 5. Observability (Logging) ### 5. Observability (Logging)
- [ ] **Initialization:** In both binaries' `main.rs`, call `tracing_subscriber::fmt::init()`. - [x] **Initialization:** In both binaries' `main.rs`, call `tracing_subscriber::fmt::init()`.
- [ ] **Implementation:** Replace all `println!` calls with `tracing::info!`, `tracing::warn!`, or `tracing::error!`. - [x] **Implementation:** Replace all `println!` calls with `tracing::info!`, `tracing::warn!`, or `tracing::error!`.
- [ ] **Tracing Context:** Use `#[tracing::instrument]` on core TCP handler functions to automatically log client IPs and session IDs. - [x] **Tracing Context:** Use `#[tracing::instrument]` on core TCP handler functions to automatically log client IPs and session IDs.

14
client_node/Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "client_node"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.102"
core_protocol = { version = "0.1.0", path = "../core_protocol" }
futures = "0.3.32"
tokio = { version = "1.52.1", features = ["rt-multi-thread", "net", "macros"] }
tokio-serde = { version = "0.9.0", features = ["bincode"] }
tokio-util = { version = "0.7.18", features = ["codec"] }
tracing = "0.1.44"
tracing-subscriber = "0.3.23"

24
client_node/src/main.rs Normal file
View File

@@ -0,0 +1,24 @@
//! Client Node entry point.
//!
//! This module initializes the desktop client application, sets up the Tokio
//! background thread for networking, and eventually binds to the UI framework.
#![forbid(unsafe_code)]
#![deny(clippy::all, clippy::pedantic)]
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod network;
use tracing::{error, info};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
info!("Starting client node...");
if let Err(e) = network::control::connect_and_auth("TestUser").await {
error!("Connection error: {:?}", e);
}
Ok(())
}

View File

@@ -0,0 +1,72 @@
//! Reliable TCP connectivity logic.
//!
//! Implements the client-side TCP connection handling, including framing,
//! serialization, and the initial authentication handshake.
use anyhow::{Context, Result};
use core_protocol::constants;
use core_protocol::tcp_events::TcpEvent;
use futures::{SinkExt, StreamExt};
use tokio::net::TcpStream;
use tokio_serde::formats::Bincode;
use tokio_serde::SymmetricallyFramed;
use tokio_util::codec::{Framed, LengthDelimitedCodec};
use tracing::{info, warn};
/// A type alias for the symmetrically framed TCP stream to simplify syntax.
type FramedStream = SymmetricallyFramed<
Framed<TcpStream, LengthDelimitedCodec>,
TcpEvent,
Bincode<TcpEvent, TcpEvent>,
>;
/// Connects to the server and performs the initial authentication handshake.
///
/// This establishes a length-delimited TCP connection to prevent fragmentation,
/// sends an `AuthRequest` with the given username, and awaits the `AuthResponse`.
///
/// # Arguments
/// * `username` - The requested display name for this client.
///
/// # Errors
/// Returns an `anyhow::Result` if the TCP socket cannot connect, if the serialization/
/// deserialization fails, or if the server closes the connection prematurely.
pub async fn connect_and_auth(username: &str) -> Result<()> {
let addr = format!("127.0.0.1:{}", constants::TCP_PORT);
info!("Connecting to server at {}...", addr);
let stream = TcpStream::connect(&addr).await.context("Failed to connect to server")?;
info!("Connected!");
// Construct the codec pipeline exactly mirroring the server's configuration
// to ensure reliable packet framing.
let length_delimited = Framed::new(stream, LengthDelimitedCodec::new());
let mut framed: FramedStream = SymmetricallyFramed::new(
length_delimited,
Bincode::<TcpEvent, TcpEvent>::default(),
);
let auth_req = TcpEvent::AuthRequest {
username: username.to_string(),
};
framed.send(auth_req).await.context("Failed to send AuthRequest")?;
info!("Sent AuthRequest for user: {}", username);
if let Some(response) = framed.next().await {
let response = response.context("Failed to deserialize response")?;
match response {
TcpEvent::AuthResponse { session_token } => {
info!("Successfully authenticated! Session token: {}", session_token);
}
_ => {
warn!("Received unexpected event instead of AuthResponse");
}
}
} else {
warn!("Connection closed before receiving AuthResponse");
}
Ok(())
}

View File

@@ -0,0 +1,6 @@
//! Internet connectivity modules.
//!
//! This module isolates all remote networking logic from the UI and audio engines.
//! It exposes the necessary interfaces to manage TCP control lanes and UDP streams.
pub mod control;

12
core_protocol/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "core_protocol"
version = "0.1.0"
edition = "2024"
[dependencies]
bincode = "1.3.3"
chrono = { version = "0.4.44", features = ["serde"] }
secrecy = { version = "0.10.3", features = ["serde"] }
serde = { version = "1.0.228", features = ["derive"] }
thiserror = "2.0.18"
uuid = { version = "1.23.1", features = ["v4", "serde"] }

View File

@@ -0,0 +1,15 @@
//! Fixed system-wide specifications.
//!
//! Contains constants required for hardware configuration, network binding,
//! and math routines such as audio framing lengths.
/// The uniform audio sampling rate (48 kHz).
/// We lock the entire system to 48kHz because Opus performs optimally at this rate.
pub const SAMPLE_RATE: u32 = 48000;
/// The exact number of audio samples per 20ms frame at 48kHz.
/// Opus strictly requires a specific frame size (like 20ms) to function correctly.
pub const FRAME_SIZE: usize = 960;
/// The default TCP port used for reliable control lane communication.
pub const TCP_PORT: u16 = 8080;

13
core_protocol/src/lib.rs Normal file
View File

@@ -0,0 +1,13 @@
//! Core Protocol definitions for the Voice App.
//!
//! This module defines the foundational types and constants shared between the
//! client and server nodes. It ensures that both ends of the connection speak the
//! exact same structural language for serialization/deserialization.
#![forbid(unsafe_code)]
#![deny(clippy::all, clippy::pedantic)]
#![deny(clippy::unwrap_used, clippy::expect_used)]
pub mod constants;
pub mod tcp_events;
pub mod udp_packets;

View File

@@ -0,0 +1,31 @@
//! TCP Control Lane events.
//!
//! Defines the reliable commands sent over the TCP connection for state
//! synchronization, such as authentication and text chat.
use serde::{Deserialize, Serialize};
/// Represents all possible events sent over the reliable TCP control lane.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TcpEvent {
/// Initial request from the client containing their chosen username.
AuthRequest {
/// The username requested by the client.
username: String,
},
/// The server's response confirming authentication and granting a session token.
AuthResponse {
/// A unique, ephemeral ID assigned to the client for the duration of the session.
session_token: u32,
},
/// A request to join a specific voice channel.
ChannelJoin {
/// The unique ID of the target channel.
channel_id: u32,
},
/// A text message intended for the current channel's chat.
ChatMessage {
/// The raw text message.
message: String,
},
}

View File

@@ -0,0 +1,20 @@
//! UDP High-speed Voice Packet structures.
//!
//! Defines the headers and payload structures for the low-latency voice data
//! sent over UDP.
use serde::{Deserialize, Serialize};
/// The header attached to every UDP voice frame.
///
/// We separate this from the payload to allow the server to rapidly route packets
/// using just the `session_token` without fully deserializing the heavy audio data.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoicePacketHeader {
/// The unique session ID matching the client's TCP authentication token.
pub session_token: u32,
/// A strictly increasing sequence number to detect dropped or out-of-order packets.
pub sequence_num: u64,
/// The timestamp of the packet generation, used to align jitter buffers.
pub timestamp: u64,
}

15
server_node/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "server_node"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.102"
core_protocol = { version = "0.1.0", path = "../core_protocol" }
dashmap = "6.1.0"
futures = "0.3.32"
tokio = { version = "1.52.1", features = ["full"] }
tokio-serde = { version = "0.9.0", features = ["bincode"] }
tokio-util = { version = "0.7.18", features = ["codec"] }
tracing = "0.1.44"
tracing-subscriber = "0.3.23"

43
server_node/src/main.rs Normal file
View File

@@ -0,0 +1,43 @@
//! Server Node entry point.
//!
//! This module initializes the headless relay server, binds the network listeners,
//! and spawns isolated tasks for each connected TCP client.
#![forbid(unsafe_code)]
#![deny(clippy::all, clippy::pedantic)]
#![deny(clippy::unwrap_used, clippy::expect_used)]
mod state;
mod tcp_router;
use core_protocol::constants;
use state::AppState;
use std::sync::Arc;
use tokio::net::TcpListener;
use tracing::{error, info};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
info!("Starting server node...");
let state = Arc::new(AppState::new());
let addr = format!("0.0.0.0:{}", constants::TCP_PORT);
let listener = TcpListener::bind(&addr).await?;
info!("TCP Listener bound to {}", addr);
loop {
match listener.accept().await {
Ok((stream, _)) => {
let state_clone = state.clone();
tokio::spawn(async move {
tcp_router::handle_connection(stream, state_clone).await;
});
}
Err(e) => {
error!("Failed to accept connection: {:?}", e);
}
}
}
}

49
server_node/src/state.rs Normal file
View File

@@ -0,0 +1,49 @@
//! Concurrent server state management.
//!
//! This module defines the global application state shared across all active
//! Tokio tasks. It heavily relies on lock-free and fine-grained locking primitives
//! like `DashMap` and `AtomicU32` to ensure high performance without stuttering.
use dashmap::DashMap;
use std::sync::atomic::{AtomicU32, Ordering};
/// Represents the session data for a single connected user.
#[allow(dead_code)]
pub struct UserState {
/// The client-chosen username for display in channels.
pub username: String,
}
/// The global application state tracking all connections and rooms.
pub struct AppState {
/// A highly concurrent hash map linking active session tokens to their user data.
pub active_users: DashMap<u32, UserState>,
/// A simple atomic counter for issuing unique sequential session tokens.
next_token: AtomicU32,
}
impl AppState {
/// Creates a new, empty application state.
#[must_use]
pub fn new() -> Self {
Self {
active_users: DashMap::new(),
next_token: AtomicU32::new(1),
}
}
/// Generates a strictly unique, monotonically increasing session token.
///
/// We use `Ordering::Relaxed` because we only need atomicity on the counter
/// itself, not strict memory ordering with other operations.
#[must_use]
pub fn generate_token(&self) -> u32 {
self.next_token.fetch_add(1, Ordering::Relaxed)
}
}
impl Default for AppState {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,95 @@
//! Reliable TCP routing and connection management.
//!
//! This module implements the reliable control lane for client communication.
//! It handles the initial authentication handshake and routes incoming TCP events
//! from the length-delimited codec to the broader application state.
use anyhow::{Context, Result};
use core_protocol::tcp_events::TcpEvent;
use futures::{SinkExt, StreamExt};
use std::sync::Arc;
use tokio::net::TcpStream;
use tokio_serde::formats::Bincode;
use tokio_serde::SymmetricallyFramed;
use tokio_util::codec::{Framed, LengthDelimitedCodec};
use tracing::{error, info, instrument, warn};
use crate::state::AppState;
use crate::state::UserState;
/// A type alias for the heavily-nested framed stream type, combining `LengthDelimitedCodec` and `Bincode`.
type FramedStream = SymmetricallyFramed<
Framed<TcpStream, LengthDelimitedCodec>,
TcpEvent,
Bincode<TcpEvent, TcpEvent>,
>;
/// Handles the lifecycle of a newly connected client's TCP stream.
///
/// This spans an instrumented task for the connection, setting up the necessary
/// framers and codecs before entering the event loop.
#[instrument(skip(stream, state))]
pub async fn handle_connection(stream: TcpStream, state: Arc<AppState>) {
let peer_addr = match stream.peer_addr() {
Ok(addr) => addr,
Err(e) => {
error!("Failed to get peer address: {:?}", e);
return;
}
};
info!("New connection from {}", peer_addr);
// We pad the TCP stream with a length-delimited codec to guarantee frame boundaries,
// avoiding fragmentation issues common in raw TCP sockets.
let length_delimited = Framed::new(stream, LengthDelimitedCodec::new());
let mut framed: FramedStream = SymmetricallyFramed::new(
length_delimited,
Bincode::<TcpEvent, TcpEvent>::default(),
);
if let Err(e) = process_connection(&mut framed, state).await {
warn!("Connection closed with error: {:?}", e);
} else {
info!("Connection closed cleanly");
}
}
/// The inner event loop that processes deserialized `TcpEvent`s from the client.
///
/// # Errors
/// Returns an `anyhow::Result` if deserialization fails, the connection drops unexpectedly,
/// or a serialization error occurs when transmitting a response.
async fn process_connection(framed: &mut FramedStream, state: Arc<AppState>) -> Result<()> {
while let Some(event) = framed.next().await {
let event = event.context("Failed to deserialize event")?;
match event {
TcpEvent::AuthRequest { username } => {
// REDACTED standard: we might log the username, but this is a reminder
// for future sensitive items to use [REDACTED].
info!("AuthRequest received for user: {}", username);
let session_token = state.generate_token();
state.active_users.insert(
session_token,
UserState {
username: username.clone(),
},
);
framed
.send(TcpEvent::AuthResponse { session_token })
.await
.context("Failed to send AuthResponse")?;
info!("AuthResponse sent to {}", username);
}
_ => {
warn!("Received unhandled event before auth or unsupported event");
}
}
}
Ok(())
}