From 8acd456af121909061ced7dd60a4a9fdeb89cfe8 Mon Sep 17 00:00:00 2001 From: EdisonJwa Date: Sat, 16 May 2026 12:14:37 +0800 Subject: [PATCH] feat(protocol): per-platform TS3 client_version selection ConnectOptions previously took tsclientlib's default Version. Servers that strictly check the announced client signature could refuse or downgrade those sessions. Add pick_client_version() that selects a stable signed descriptor matching the runtime OS: Windows -> Version::Windows_5_0_0_beta51 Linux -> Version::Linux_5_0_0_beta51 macOS -> Version::macOS_5_0_0_beta51 Android -> Version::Android_3_5_0__7 iOS -> Version::iOS_3_5_6 other -> Linux fallback All five variants are guaranteed to exist in the vendored tsproto-types enum at compile time; build fails loudly if upstream removes one. Wired into Connection::build(...).version(pick_client_version()) on every connect. Emits 'selected TS3 client_version' info log line so the choice is visible in chanora.log. --- crates/chanora_protocol/src/adapter.rs | 67 +++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/crates/chanora_protocol/src/adapter.rs b/crates/chanora_protocol/src/adapter.rs index e8319f6..5512d26 100644 --- a/crates/chanora_protocol/src/adapter.rs +++ b/crates/chanora_protocol/src/adapter.rs @@ -28,13 +28,68 @@ use tsclientlib::data::{self, Channel, Client}; use tsclientlib::prelude::*; use tsclientlib::{ ChannelId as TsChannelId, Connection, DisconnectOptions, Identity, MessageHandle, - OutCommandExt, StreamItem, + OutCommandExt, StreamItem, Version, }; use tsproto_packets::packets::{InAudioBuf, OutPacket}; use crate::dto::{ChannelId, ChannelInfo, ClientId, ClientInfo, ServerSnapshot}; use crate::ProtocolError; +/// Pick the TeamSpeak `client_version`/platform/signature triple +/// (sourced from `ReSpeak/tsdeclarations/Versions.csv`, baked into +/// `tsproto-types` at vendor-time) that best matches the *runtime* +/// platform Chanora is executing on. +/// +/// Per-platform selection (set by the project owner): +/// +/// * Windows / Linux / macOS desktops announce as TeamSpeak **5 +/// beta51** — the latest TS5 desktop signature in the vendored +/// `tsproto-types` enum. TS5 servers expect this shape; TS3 +/// servers are happy to accept any well-formed signed client +/// descriptor and have no codec-version coupling. +/// * Android announces as **3.5.0__7** (the latest Android +/// signature in the vendored enum). +/// * iOS announces as **3.5.6** (latest iOS signature in the +/// vendored enum). +/// +/// All five variants are guaranteed to exist in the generated +/// `Version` enum at compile time; if upstream rotates the CSV the +/// build will fail loudly here rather than silently fall back. +fn pick_client_version() -> Version { + #[cfg(target_os = "windows")] + { + Version::Windows_5_0_0_beta51 + } + #[cfg(target_os = "macos")] + { + Version::macOS_5_0_0_beta51 + } + #[cfg(target_os = "ios")] + { + Version::iOS_3_5_6 + } + #[cfg(target_os = "android")] + { + Version::Android_3_5_0__7 + } + #[cfg(target_os = "linux")] + { + Version::Linux_5_0_0_beta51 + } + #[cfg(not(any( + target_os = "windows", + target_os = "macos", + target_os = "ios", + target_os = "android", + target_os = "linux" + )))] + { + // Last-ditch fallback for unanticipated targets (BSDs, + // Solaris-likes). Linux signature is the closest analogue. + Version::Linux_5_0_0_beta51 + } +} + /// Typed configuration for a connection attempt. #[derive(Debug, Clone)] pub struct ConnectConfig { @@ -331,7 +386,15 @@ async fn connection_task( // Pass the resolved SocketAddr directly to tsclientlib so it // skips its own resolver entirely (tsclientlib accepts // SocketAddr via the From for ServerAddress impl). - let mut builder = Connection::build(resolved).name(cfg.nickname.clone()); + let client_version = pick_client_version(); + info!( + target: "chanora_protocol", + client_version = %client_version, + "selected TS3 client_version for this platform" + ); + let mut builder = Connection::build(resolved) + .name(cfg.nickname.clone()) + .version(client_version); let identity = match cfg.identity.as_deref() { Some(s) => match Identity::new_from_str(s) {