CI/CD / Build Frontend (push) Failing after 11s
CI/CD / Test (macos-latest) (push) Has been cancelled
CI/CD / Test (windows-latest) (push) Has been cancelled
CI/CD / Build Desktop (linux) (push) Has been cancelled
CI/CD / Build Desktop (macos) (push) Has been cancelled
CI/CD / Build Desktop (windows) (push) Has been cancelled
CI/CD / Release (push) Has been cancelled
CI/CD / Test (ubuntu-latest) (push) Failing after 2s
- tscore: Protocol implementation (packets, crypto, connection handshake) - tsaudio: Audio engine (capture, playback, codec, VAD, jitter buffer) - tsdb: SQLite database (identities, bookmarks, messages, settings) - shared: Core types and events - tauri-app: Tauri v2 desktop application with React frontend - docs: SRS, SAD, SDD documentation - CI/CD: GitHub Actions workflow - 32 unit tests passing
31 KiB
31 KiB
Software Design Document (SDD)
ReTeamSpeak - Cross-Platform TeamSpeak Client
Version: 1.0.0
Date: 2026-05-12
Status: Based on actual implementation
1. Detailed Design
1.1 shared Module Design
1.1.1 Core Type System
┌─────────────────────────────────────────────────────────────┐
│ Type Hierarchy │
├─────────────────────────────────────────────────────────────┤
│ Identifier Types (Newtype Pattern) │
│ ├── ClientId(u16) // In-session client ID │
│ ├── ChannelId(u64) // Channel identifier │
│ ├── ServerGroupId(u64) // Server group │
│ ├── ChannelGroupId(u64) // Channel group │
│ ├── ClientDbId(u64) // Database client ID │
│ ├── Uid(String) // Unique identity (base64) │
│ ├── PermissionId(u32) // Permission ID │
│ └── IconId(i32) // Icon identifier │
│ │
│ Enumerations │
│ ├── Codec { SpeexNB, SpeexWB, SpeexUWB, Celt, OpusVoice, │
│ │ OpusMusic } │
│ ├── ChannelType { Permanent, SemiPermanent, Temporary } │
│ ├── ClientType { Normal, Query { admin } } │
│ ├── ConnectionState { Disconnected, Connecting, ... } │
│ ├── Reason { None, Moved, LostConnection, KickChannel, ... }│
│ ├── CodecEncryptionMode { PerChannel, ForcedOff, ForcedOn }│
│ ├── HostMessageMode { None, Log, Modal, Modalquit } │
│ ├── GroupType { Template, Regular, Query } │
│ └── GroupNamingMode { None, Before, After } │
│ │
│ Data Structures │
│ ├── ServerInfo { id, name, platform, version, max_clients, │
│ │ clients_online, ... } │
│ ├── ChannelInfo { id, parent_id, name, codec, max_clients, │
│ │ channel_type, ... } │
│ ├── ClientInfo { id, channel_id, uid, name, muted, ... } │
│ ├── ChatMessage { id, timestamp, invoker, target, message }│
│ └── AppConfig { nickname, audio, hotkeys, theme, ... } │
└─────────────────────────────────────────────────────────────┘
1.1.2 Event System Design
AppEvent
├── Connection(ConnectionEvent)
│ ├── Connecting { address }
│ ├── Connected { server, own_client }
│ ├── StateChanged { state }
│ ├── DisconnectedTemporarily { reason }
│ ├── Disconnected { reason }
│ └── ConnectionFailed { error }
│
├── Client(ClientEvent)
│ ├── EnteredView { client, reason }
│ ├── LeftView { client_id, reason, reason_message }
│ ├── Updated { client_id, changes: ClientChanges }
│ ├── Moved { client_id, from_channel, to_channel, reason }
│ ├── StartedTalking { client_id }
│ ├── StoppedTalking { client_id }
│ ├── ServerGroupChanged { client_id, group_id, added }
│ └── ChannelGroupChanged { client_id, group_id }
│
├── Channel(ChannelEvent)
│ ├── Created { channel }
│ ├── Deleted { channel_id }
│ ├── Updated { channel_id, changes: ChannelChanges }
│ ├── Moved { channel_id, new_parent, new_order }
│ ├── PasswordChanged { channel_id }
│ ├── DescriptionChanged { channel_id }
│ └── Subscribed { channel_id, subscribed }
│
├── Server(ServerEvent)
│ ├── Updated { changes: ServerChanges }
│ ├── ServerGroupList { groups }
│ └── ChannelGroupList { groups }
│
├── Message(MessageEvent)
│ ├── Received { message }
│ ├── Sent { message }
│ ├── Read { message_id }
│ └── UnreadCountChanged { count }
│
├── Audio(AudioEvent)
│ ├── InputDeviceChanged { device }
│ ├── OutputDeviceChanged { device }
│ ├── InputVolumeChanged { volume }
│ ├── OutputVolumeChanged { volume }
│ ├── InputMutedChanged { muted }
│ ├── OutputMutedChanged { muted }
│ ├── DeviceList { input_devices, output_devices }
│ ├── InputLevel { level }
│ └── OutputLevel { level }
│
├── FileTransfer(FileTransferEvent)
│ ├── Started { transfer_id, file_name, file_size, is_upload }
│ ├── Progress { transfer_id, progress }
│ ├── Completed { transfer_id }
│ ├── Failed { transfer_id, error }
│ └── Cancelled { transfer_id }
│
└── Error(ErrorEvent)
├── Protocol { code, message }
├── Network { message }
├── Audio { message }
├── Database { message }
└── Other { message }
1.2 tscore Module Design
1.2.1 Packet Processing Pipeline
SEND RECEIVE
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ Command String │ │ UDP Packet │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ Command::serialize() │ │ InPacket::parse() │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ QuickLZ compress │ │ AES-EAX decrypt │
│ (if Command/CommandLow) │ │ (or fake decrypt) │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ Fragment (if > 500 bytes) │ │ QuickLZ decompress │
│ │ │ │ (if COMPRESSED flag) │
│ ▼ │ │ │ │
│ AES-EAX encrypt │ │ ▼ │
│ (or fake encrypt) │ │ Defragment │
│ │ │ │ (if FRAGMENTED flag) │
│ ▼ │ │ │ │
│ Assign Packet ID │ │ ▼ │
│ │ │ │ Command::parse() │
│ ▼ │ │ │ │
│ UDP Send │ │ ▼ │
└──────────────────────────────┘ │ Application Layer │
└──────────────────────────────┘
1.2.2 Encryption Key Derivation
Input: packet_type, direction, generation_id, shared_iv[64]
│
▼
┌───────────────────────────────────────────────────────┐
│ temp[0] = direction_byte (0x30=S2C, 0x31=C2S) │
│ temp[1] = packet_type.u8() │
│ temp[2..6] = generation_id.to_be_bytes() │
│ temp[6..70] = shared_iv[0..64] │
│ │
│ key_nonce = SHA-256(temp) │
│ key = key_nonce[0..16] │
│ nonce = key_nonce[16..32] │
│ │
│ key[0] ^= (packet_id >> 8) as u8 │
│ key[1] ^= (packet_id & 0xFF) as u8 │
└───────────────────────────────────────────────────────┘
│
▼
Output: key[16], nonce[16] → AES-128-EAX
1.2.3 Connection State Machine
┌──────────────┐
│ Disconnected │
└──────┬───────┘
│ start_handshake()
▼
┌──────────────┐
┌──────│ Connecting │◄─────────────────┐
│ └──────┬───────┘ │
│ │ Init1 received │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ IdentityLevelIncreasing│ │
│ └──────┬───────────────┘ │
│ │ Init3 received │
│ ▼ │
│ ┌──────────────┐ │
│ │ Connected │──────────────────┤
│ └──────┬───────┘ │
│ │ channellistfinished │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ ChannelListFinished │ │
│ └──────┬──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────┐ │
└─────>│ DisconnectedTemporarily │──────┘
└──────┬───────────────────┘
│ timeout / manual
▼
┌──────────────┐
│ Error │
└──────┬───────┘
│
▼
┌──────────────┐
│ Disconnected │
└──────────────┘
1.2.4 RSA Puzzle Solver
/// Solves y = x^(2^level) mod n
///
/// Algorithm:
/// y = x
/// for i in 0..level:
/// y = (y * y) mod n
///
/// Time complexity: O(level * M(n)) where M(n) is multiplication cost
/// Space complexity: O(n) for big integer storage
fn solve_rsa_puzzle(x: &[u8; 64], n: &[u8; 64], level: u32) -> [u8; 64] {
let x_big = BigUint::from_bytes_be(x);
let n_big = BigUint::from_bytes_be(n);
let mut y = x_big;
for _ in 0..level {
y = (y.clone() * y) % &n_big;
}
// Convert back to 64-byte array (big-endian, zero-padded)
}
1.2.5 Retransmission System
┌─────────────────────────────────────────────────────────┐
│ ResendManager │
├─────────────────────────────────────────────────────────┤
│ pending: BTreeMap<PacketId, SentPacket> │
│ max_retries: u32 (default: 10) │
│ connection_timeout: Duration (default: 30s) │
├─────────────────────────────────────────────────────────┤
│ add_sent(id, data) → Add to pending queue │
│ ack(id) → bool → Remove from pending │
│ get_retransmissions() → Vec<(id, data)> to resend │
│ is_connection_timeout()→ Check for dead connection │
├─────────────────────────────────────────────────────────┤
│ │
│ SentPacket: │
│ data: Vec<u8> │
│ sent_at: Instant │
│ retry_count: u32 │
│ timeout: Duration (starts at 500ms, doubles) │
│ │
│ RttEstimator: │
│ srtt: Duration (smoothed RTT) │
│ rtt_var: Duration (RTT variance) │
│ rto: Duration (retransmission timeout) │
│ update(measured_rtt) → recalculate SRTT, RTO │
└─────────────────────────────────────────────────────────┘
1.3 tsaudio Module Design
1.3.1 Audio Pipeline
CAPTURE PIPELINE:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ cpal │───>│ VAD │───>│ Opus │───>│ Packet │
│ capture │ │ detect │ │ encode │ │ output │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
PLAYBACK PIPELINE:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Packet │───>│ Jitter │───>│ Opus │───>│ cpal │
│ input │ │ buffer │ │ decode │ │ playback │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
1.3.2 Voice Activity Detection
pub struct VadDetector {
threshold: f32,
state: VadState,
}
impl VadDetector {
pub fn detect(&mut self, samples: &[f32]) -> VadState {
let energy = samples.iter().map(|s| s * s).sum::<f32>()
/ samples.len() as f32;
if energy > self.threshold {
self.state = VadState::Speaking;
} else {
self.state = VadState::Silent;
}
self.state
}
}
1.3.3 Jitter Buffer
┌─────────────────────────────────────────────────────────┐
│ JitterBuffer │
├─────────────────────────────────────────────────────────┤
│ buffer: Vec<Option<AudioFrame>> (ring buffer) │
│ head: usize │
│ tail: usize │
│ size: usize │
│ capacity: usize │
├─────────────────────────────────────────────────────────┤
│ push(frame) → Result<()> // Add frame │
│ pop() → Option<AudioFrame> // Get next frame │
│ len() → usize // Current buffer size │
│ is_empty() → bool │
│ is_full() → bool │
│ clear() // Reset buffer │
└─────────────────────────────────────────────────────────┘
1.4 tsdb Module Design
1.4.1 Database Schema
-- Identity storage
CREATE TABLE identities (
id TEXT PRIMARY KEY, -- UUID
name TEXT NOT NULL, -- Display name
private_key TEXT NOT NULL, -- Base64 encoded ECC private key
counter INTEGER DEFAULT 0, -- Hash Cash counter
max_counter INTEGER DEFAULT 0, -- Maximum counter tried
created_at TEXT NOT NULL, -- ISO 8601 timestamp
updated_at TEXT NOT NULL -- ISO 8601 timestamp
);
-- Server bookmarks
CREATE TABLE bookmarks (
id TEXT PRIMARY KEY, -- UUID
name TEXT NOT NULL, -- Display name
address TEXT NOT NULL, -- Server address
port INTEGER DEFAULT 9987, -- Server port
nickname TEXT, -- Preferred nickname
server_password TEXT, -- Encrypted server password
channel TEXT, -- Default channel
channel_password TEXT, -- Encrypted channel password
default_token TEXT, -- Permission token
auto_connect INTEGER DEFAULT 0,-- Auto-connect on startup
last_connected TEXT, -- Last connection timestamp
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
-- Message history
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
server_address TEXT NOT NULL, -- Server address
invoker_id INTEGER NOT NULL, -- Client ID
invoker_name TEXT NOT NULL, -- Display name
invoker_uid TEXT NOT NULL, -- Unique ID
target_type TEXT NOT NULL, -- "server", "channel", "client"
target_id INTEGER, -- Target ID
message TEXT NOT NULL, -- Message content
is_read INTEGER DEFAULT 0, -- Read status
timestamp TEXT NOT NULL -- ISO 8601 timestamp
);
-- Key-value settings
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
updated_at TEXT NOT NULL
);
1.4.2 CRUD Operations
impl DatabaseManager {
// Identity operations
fn create_identity(&self, name, private_key) -> DatabaseResult<Identity>;
fn get_identity(&self, id) -> DatabaseResult<Identity>;
fn get_all_identities(&self) -> DatabaseResult<Vec<Identity>>;
fn update_identity(&self, id, name?, counter?) -> DatabaseResult<()>;
fn delete_identity(&self, id) -> DatabaseResult<()>;
// Bookmark operations
fn create_bookmark(&self, name, address, port, nickname?) -> DatabaseResult<Bookmark>;
fn get_bookmark(&self, id) -> DatabaseResult<Bookmark>;
fn get_all_bookmarks(&self) -> DatabaseResult<Vec<Bookmark>>;
fn update_bookmark(&self, id, name?, address?, port?, nickname?) -> DatabaseResult<()>;
fn delete_bookmark(&self, id) -> DatabaseResult<()>;
// Message operations
fn create_message(&self, server_address, invoker_id, invoker_name,
invoker_uid, target_type, target_id?, message) -> DatabaseResult<Message>;
fn get_server_messages(&self, server_address, limit, offset) -> DatabaseResult<Vec<Message>>;
fn mark_message_read(&self, id) -> DatabaseResult<()>;
// Settings operations
fn get_setting(&self, key) -> DatabaseResult<Option<String>>;
fn set_setting(&self, key, value) -> DatabaseResult<()>;
}
1.5 tauri-app Design
1.5.1 Tauri Command Architecture
┌─────────────────────────────────────────────────────────────┐
│ Frontend (React) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ invoke("get_bookmarks") → Promise<BookmarkInfo[]> │ │
│ │ invoke("connect", {addr, port, nick, pass}) │ │
│ │ invoke("send_message", {target, message}) │ │
│ └───────────────────────────────────────────────────────┘ │
└───────────────────────────┬─────────────────────────────────┘
│ Tauri IPC
▼
┌─────────────────────────────────────────────────────────────┐
│ Tauri Shell (Rust) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ #[tauri::command] │ │
│ │ async fn get_bookmarks(state: State<AppState>) │ │
│ │ -> Result<Vec<BookmarkInfo>, String> │ │
│ │ { │ │
│ │ state.db.get_all_bookmarks() │ │
│ │ .map(|b| b.into_iter().map(Into::into)) │ │
│ │ .map_err(|e| e.to_string()) │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ AppState { │ │
│ │ db: DatabaseManager, │ │
│ │ connection_state: Mutex<ConnectionState>, │ │
│ │ } │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
1.5.2 React Component Structure
App
├── Header
│ ├── Logo
│ ├── ConnectionStatus
│ └── SettingsButton
│
├── Sidebar
│ ├── BookmarkList
│ │ └── BookmarkItem (clickable)
│ └── RecentServers
│
├── MainContent
│ ├── ConnectForm (when no connection)
│ │ ├── AddressInput
│ │ ├── NicknameInput
│ │ ├── PasswordInput
│ │ └── ConnectButton
│ │
│ └── ChatView (when connected)
│ ├── ChannelTree
│ ├── ClientList
│ ├── MessageList
│ └── MessageInput
│
└── StatusBar
├── ConnectionInfo
├── AudioStatus
└── LatencyDisplay
2. Algorithm Specifications
2.1 Shared Secret Computation (Old Protocol <3.1)
Input: alpha[10], beta[10], shared_data[32]
Output: SharedIV[64], SharedMac[8]
1. SharedIV[0..20] = SHA-1(shared_data)
2. SharedIV[0..10] ^= alpha[0..10]
3. SharedIV[10..20] ^= beta[0..10]
4. SharedMac[0..8] = SHA-1(SharedIV)[0..8]
2.2 Shared Secret Computation (New Protocol ≥3.1)
Input: alpha[10], beta[54], shared_data[32]
Output: SharedIV[64], SharedMac[8]
1. SharedIV[0..64] = SHA-512(shared_data)
2. SharedIV[0..10] ^= alpha[0..10]
3. SharedIV[10..64] ^= beta[0..54]
4. SharedMac[0..8] = SHA-1(SharedIV)[0..8]
2.3 Hash Cash Level Computation
Input: omega (public key string), offset (u64)
Output: level (u8)
1. data = SHA-1(omega + offset.to_string())
2. level = 0
3. for byte in data:
4. if byte == 0:
5. level += 8
6. else:
7. level += trailing_zeros(byte)
8. break
9. return level
2.4 UID Computation
Input: publicKey (ASN.1-DER encoded)
Output: uid (base64 string)
1. hash = SHA-1(publicKey)
2. uid = base64(hash)
3. Interface Specifications
3.1 Tauri IPC Interface
// TypeScript interface for Tauri commands
interface ITauriCommands {
// Identity management
get_identities(): Promise<IdentityInfo[]>;
create_identity(name: string): Promise<IdentityInfo>;
delete_identity(id: string): Promise<void>;
// Bookmark management
get_bookmarks(): Promise<BookmarkInfo[]>;
create_bookmark(name: string, address: string, port: number,
nickname?: string): Promise<BookmarkInfo>;
delete_bookmark(id: string): Promise<void>;
// Connection
connect(address: string, port: number, nickname: string,
password?: string): Promise<void>;
disconnect(): Promise<void>;
// Messaging
send_message(target: string, message: string): Promise<void>;
get_messages(server_address: string, limit: number,
offset: number): Promise<MessageInfo[]>;
}
interface IdentityInfo {
id: string;
name: string;
counter: number;
max_counter: number;
}
interface BookmarkInfo {
id: string;
name: string;
address: string;
port: number;
nickname: string | null;
auto_connect: boolean;
last_connected: string | null;
}
interface MessageInfo {
id: number;
invoker_name: string;
message: string;
timestamp: string;
is_read: boolean;
}
3.2 Internal Rust Interfaces
// Protocol layer
pub trait PacketProcessor {
fn encode(&self, packet: OutPacket) -> Result<Vec<OutUdpPacket>>;
fn decode(&self, data: &[u8]) -> Result<InPacket>;
}
// Audio layer
pub trait AudioCapture {
async fn start(&mut self) -> AudioResult<()>;
async fn stop(&mut self) -> AudioResult<()>;
async fn capture(&mut self) -> AudioResult<AudioFrame>;
}
pub trait AudioPlayback {
async fn start(&mut self) -> AudioResult<()>;
async fn stop(&mut self) -> AudioResult<()>;
async fn play(&mut self, frame: AudioFrame) -> AudioResult<()>;
}
// Database layer
pub trait IdentityStore {
fn create(&self, name: &str, key: &str) -> DatabaseResult<Identity>;
fn get(&self, id: &str) -> DatabaseResult<Identity>;
fn list(&self) -> DatabaseResult<Vec<Identity>>;
fn update(&self, id: &str, updates: IdentityUpdates) -> DatabaseResult<()>;
fn delete(&self, id: &str) -> DatabaseResult<()>;
}
4. Data Dictionary
4.1 Protocol Fields
| Field | Type | Size | Description |
|---|---|---|---|
| MAC | [u8; 8] | 8 bytes | EAX message authentication code |
| PId | u16 | 2 bytes | Packet sequence ID |
| CId | u16 | 2 bytes | Client ID (C2S only) |
| PT | u8 | 1 byte | Packet type + flags |
| VId | u16 | 2 bytes | Voice packet ID |
| Codec | u8 | 1 byte | Audio codec type |
4.2 Flag Bits
| Bit | Name | Mask | Description |
|---|---|---|---|
| 7 | UE | 0x80 | Unencrypted |
| 6 | CP | 0x40 | Compressed (QuickLZ) |
| 5 | NP | 0x20 | New protocol |
| 4 | FR | 0x10 | Fragmented |
| 3-0 | Type | 0x0F | Packet type (0-8) |
4.3 Error Codes
| Code | Name | Description |
|---|---|---|
| 0x0000 | ok | Success |
| 0x0200 | client_invalid_id | Invalid client ID |
| 0x0201 | client_nickname_inuse | Nickname already in use |
| 0x0208 | client_invalid_password | Wrong password |
| 0x0300 | channel_invalid_id | Invalid channel ID |
| 0x0400 | server_invalid_id | Invalid server ID |
| 0x0403 | server_maxclients_reached | Server full |
| 0x0701 | connection_lost | Connection lost |
5. Test Design
5.1 Test Cases (32 total)
Protocol Tests (14)
test_packet_type_conversion- PacketType enum conversiontest_flags- Flag bit manipulationtest_header_c2s- C2S header parsingtest_header_s2c- S2C header parsingtest_in_packet_parse- Input packet parsingtest_out_packet- Output packet creationtest_command_parse- Command string parsingtest_command_serialize- Command serializationtest_command_builder- CommandBuilder patterntest_escape_sequences- Escape/unescapetest_init_packet_parse- Init packet parsingtest_init_packet_serialize- Init packet serializationtest_ack_packet- Acknowledgement packettest_packet_type_properties- Type property queries
Crypto Tests (12)
test_sha1- SHA-1 hashtest_sha256- SHA-256 hashtest_sha512- SHA-512 hashtest_hash_password- Password hashingtest_create_key_nonce- Key derivationtest_create_encryption_key- Packet-specific keytest_shared_secret_old- Old protocol shared secrettest_shared_secret_new- New protocol shared secrettest_key_cache- Key cachingtest_eax_encrypt_decrypt- EAX encryption/decryptiontest_fake_encrypt_decrypt- Fake encryptiontest_hash_cash_level- Hash Cash computationtest_compute_uid- UID computation
Connection Tests (6)
test_encode_version- Version encodingtest_rsa_puzzle- RSA puzzle solvertest_resend_manager- Retransmission managertest_rtt_estimator- RTT estimationtest_sent_packet_retry- Packet retry logic
6. References
- TS3 Protocol Paper:
refercence/tsdeclarations/ts3protocol.md - Packet Definitions:
refercence/tsdeclarations/Packets.txt - Message Definitions:
refercence/tsdeclarations/Messages.toml - Source Code:
src/(4780 lines, 39 Rust files)