feat: promote linux native audio path

This commit is contained in:
Edison Jwa
2026-05-25 17:42:06 +09:00
parent c19de3a370
commit a2d686d9d0
73 changed files with 26156 additions and 467 deletions
+776
View File
@@ -0,0 +1,776 @@
use std::collections::HashMap;
use anyhow::{anyhow, bail, Context, Result};
use base64::prelude::*;
use clap::Parser;
use futures::prelude::*;
use time::format_description::well_known::Rfc3339;
use time::{Duration, OffsetDateTime};
use tokio::time as tokio_time;
use tracing::{info, warn};
use tracing_subscriber::EnvFilter;
use tsclientlib::data;
use tsclientlib::messages::s2c::{InClientDbIdFromUidPart, InClientDbInfoPart, InMessage};
use tsclientlib::{
ChannelGroupId, ClientDbId, ClientId, Connection, DisconnectOptions, Identity, OutCommandExt,
ServerGroupId, StreamItem, Uid,
};
use tsproto_packets::packets::{Direction, Flags, OutCommand, PacketType};
#[derive(Parser, Debug)]
#[command(
name = "tsclientlib-query-spike",
about = "Chanora PoC: inspect TeamSpeak client profile + connection info over the full client protocol."
)]
struct Args {
/// Server address (hostname[:port] or TSDNS name).
#[arg(short, long, default_value = "kr.teamspeak.app")]
address: String,
/// Nickname used on the server.
#[arg(short, long, default_value = "ChanoraPoC-Inspector")]
nickname: String,
/// Optional TeamSpeak identity string. If omitted, a fresh identity is generated.
#[arg(long)]
identity: Option<String>,
/// Server password if required.
#[arg(long)]
password: Option<String>,
/// Inspect a specific online/offline nickname.
#[arg(long)]
target_nickname: Option<String>,
/// Inspect a specific TeamSpeak UID.
#[arg(long)]
target_uid: Option<String>,
/// Inspect a specific TeamSpeak database id.
#[arg(long)]
target_dbid: Option<u64>,
/// Inspect a specific online client id.
#[arg(long)]
target_clid: Option<u16>,
/// Print the online client table before detailed inspection.
#[arg(long, default_value_t = true)]
show_online: bool,
}
#[derive(Clone, Debug)]
struct Target {
clid: Option<ClientId>,
dbid: Option<ClientDbId>,
uid_b64: Option<String>,
nickname: Option<String>,
}
#[derive(Clone, Debug)]
struct OnlineTargetSnapshot {
description: String,
country_code: String,
server_groups: Vec<ServerGroupId>,
channel_group: ChannelGroupId,
avatar_hash: String,
uid_b64: Option<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
let filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info,tsproto=warn,tsclientlib=warn"));
tracing_subscriber::fmt().with_env_filter(filter).init();
info!(target: "query-spike", address = %args.address, nickname = %args.nickname, "starting query spike");
let mut builder = Connection::build(args.address.clone()).name(args.nickname.clone());
let identity = match &args.identity {
Some(raw) => Identity::new_from_str(raw).context("parsing --identity")?,
None => Identity::create(),
};
builder = builder.identity(identity);
if let Some(password) = &args.password {
builder = builder.password(password.clone());
}
let mut con = builder.connect().context("dialling server")?;
wait_for_initial_state(&mut con).await?;
if let Ok(state) = con.get_state() {
let _ = state.server.set_subscribed(true).send(&mut con);
}
pump_events(&mut con, std::time::Duration::from_secs(1)).await?;
let (mut target, online_snapshot) = {
let state = con.get_state().context("reading connection state")?;
if args.show_online {
print_online_clients(state);
}
let target = resolve_target(&args, state)?;
let snapshot = target
.clid
.and_then(|clid| state.clients.get(&clid))
.map(|client| OnlineTargetSnapshot {
description: client.description.clone(),
country_code: client.country_code.clone(),
server_groups: client.server_groups.iter().copied().collect(),
channel_group: client.channel_group,
avatar_hash: client.avatar_hash.clone(),
uid_b64: client.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
});
(target, snapshot)
};
let _ = request_server_group_list(&mut con).await;
let _ = request_channel_group_list(&mut con).await;
if target.dbid.is_none() {
if let Some(uid) = target.uid_b64.as_deref() {
if let Ok(lookup) = request_client_dbid_from_uid(&mut con, uid).await {
target.dbid = Some(lookup.client_db_id);
}
}
}
println!();
println!("=== Detailed Target ===");
println!("Target nickname : {}", target.nickname.as_deref().unwrap_or("Unknown"));
println!("Target UID : {}", target.uid_b64.as_deref().unwrap_or("Unknown"));
println!(
"Target DBID : {}",
target
.dbid
.map(|id| id.0.to_string())
.unwrap_or_else(|| "Unknown".to_string())
);
println!(
"Target CLID : {}",
target
.clid
.map(|id| id.0.to_string())
.unwrap_or_else(|| "Offline/Unknown".to_string())
);
if let Some(clid) = target.clid {
if let Err(error) = request_client_variables(&mut con, clid).await {
warn!(target: "query-spike", %error, clid = clid.0, "clientgetvariables failed");
}
if let Err(error) = request_client_connection_info(&mut con, clid).await {
warn!(target: "query-spike", %error, clid = clid.0, "getconnectioninfo failed");
}
}
let db_info = match target.dbid {
Some(dbid) => match request_client_db_info(&mut con, dbid).await {
Ok(info) => Some(info),
Err(error) => {
warn!(target: "query-spike", %error, dbid = dbid.0, "clientdbinfo failed");
None
}
},
None => None,
};
let (optional_data, connection_data, server_group_names, channel_group_names) = {
let state = con.get_state().context("reading updated connection state")?;
let server_group_names = state
.server_groups
.iter()
.map(|(id, group)| (*id, group.name.clone()))
.collect::<HashMap<_, _>>();
let channel_group_names = state
.channel_groups
.iter()
.map(|(id, group)| (*id, group.name.clone()))
.collect::<HashMap<_, _>>();
match target.clid.and_then(|clid| state.clients.get(&clid)) {
Some(client) => (
client.optional_data.clone(),
client.connection_data.clone(),
server_group_names,
channel_group_names,
),
None => (None, None, server_group_names, channel_group_names),
}
};
print_detailed_summary(
target,
optional_data.as_ref(),
connection_data.as_ref(),
db_info.as_ref(),
online_snapshot.as_ref(),
&server_group_names,
&channel_group_names,
)?;
con.disconnect(DisconnectOptions::new()).ok();
con.events().for_each(|_| futures::future::ready(())).await;
Ok(())
}
async fn wait_for_initial_state(con: &mut Connection) -> Result<()> {
loop {
let event = con
.events()
.next()
.await
.ok_or_else(|| anyhow!("event stream ended before initial state"))??;
if matches!(event, StreamItem::BookEvents(_)) {
return Ok(());
}
}
}
async fn pump_events(con: &mut Connection, budget: std::time::Duration) -> Result<()> {
let deadline = tokio_time::Instant::now() + budget;
loop {
let now = tokio_time::Instant::now();
if now >= deadline {
return Ok(());
}
let remaining = deadline - now;
match tokio_time::timeout(remaining, con.events().next()).await {
Ok(Some(Ok(_))) => {}
Ok(Some(Err(error))) => return Err(error).context("pumping connection events"),
Ok(None) => bail!("event stream ended while pumping events"),
Err(_) => return Ok(()),
}
}
}
fn build_command(name: &str, args: &[(&str, String)], flags: &[&str]) -> OutCommand {
let mut command = OutCommand::new(Direction::C2S, Flags::empty(), PacketType::Command, name);
for flag in flags {
command.write_arg(flag, &"");
}
for (key, value) in args {
command.write_arg(key, value);
}
command
}
async fn request_messages(con: &mut Connection, command: OutCommand) -> Result<Vec<InMessage>> {
let handle = command.send_with_result(con).context("sending command")?;
let mut messages = Vec::new();
loop {
let item = con
.events()
.next()
.await
.ok_or_else(|| anyhow!("event stream ended before command response"))??;
match item {
StreamItem::MessageEvent(message) => messages.push(message),
StreamItem::MessageResult(reply, status) if reply == handle => {
status.map_err(|error| anyhow!("command failed: {error}"))?;
return Ok(messages);
}
_ => {}
}
}
}
async fn request_messages_and_quiet(
con: &mut Connection,
command: OutCommand,
quiet_period: std::time::Duration,
) -> Result<Vec<InMessage>> {
let messages = request_messages(con, command).await?;
pump_events(con, quiet_period).await?;
Ok(messages)
}
async fn request_client_variables(con: &mut Connection, clid: ClientId) -> Result<()> {
let command = build_command("clientgetvariables", &[("clid", clid.0.to_string())], &[]);
request_messages_and_quiet(con, command, std::time::Duration::from_millis(350)).await?;
Ok(())
}
async fn request_client_connection_info(con: &mut Connection, clid: ClientId) -> Result<()> {
let command = build_command("getconnectioninfo", &[("clid", clid.0.to_string())], &[]);
request_messages_and_quiet(con, command, std::time::Duration::from_millis(350)).await?;
Ok(())
}
async fn request_client_db_info(con: &mut Connection, dbid: ClientDbId) -> Result<InClientDbInfoPart> {
let command = build_command("clientdbinfo", &[("cldbid", dbid.0.to_string())], &[]);
let messages = request_messages(con, command).await?;
for message in messages {
if let InMessage::ClientDbInfo(info) = message {
if let Some(row) = info.iter().next() {
return Ok(row.clone());
}
}
}
bail!("clientdbinfo returned no row");
}
async fn request_client_dbid_from_uid(con: &mut Connection, uid: &str) -> Result<InClientDbIdFromUidPart> {
let command = build_command("clientgetdbidfromuid", &[("cluid", uid.to_string())], &[]);
let messages = request_messages(con, command).await?;
for message in messages {
if let InMessage::ClientDbIdFromUid(info) = message {
if let Some(row) = info.iter().next() {
return Ok(row.clone());
}
}
}
bail!("clientgetdbidfromuid returned no row");
}
async fn request_server_group_list(con: &mut Connection) -> Result<()> {
request_messages_and_quiet(
con,
build_command("servergrouplist", &[], &[]),
std::time::Duration::from_millis(200),
)
.await?;
Ok(())
}
async fn request_channel_group_list(con: &mut Connection) -> Result<()> {
request_messages_and_quiet(
con,
build_command("channelgrouplist", &[], &[]),
std::time::Duration::from_millis(200),
)
.await?;
Ok(())
}
fn resolve_target(args: &Args, state: &data::Connection) -> Result<Target> {
if let Some(clid) = args.target_clid {
if let Some(row) = state.clients.get(&ClientId(clid)) {
return Ok(Target {
clid: Some(row.id),
dbid: Some(row.database_id),
uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
nickname: Some(row.name.clone()),
});
}
return Ok(Target {
clid: Some(ClientId(clid)),
dbid: None,
uid_b64: None,
nickname: None,
});
}
if let Some(uid) = &args.target_uid {
if let Some(row) = state
.clients
.values()
.find(|row| row.uid.as_ref().is_some_and(|value| uid_to_b64(value.as_ref()) == *uid))
{
return Ok(Target {
clid: Some(row.id),
dbid: Some(row.database_id),
uid_b64: Some(uid.clone()),
nickname: Some(row.name.clone()),
});
}
return Ok(Target {
clid: None,
dbid: args.target_dbid.map(ClientDbId),
uid_b64: Some(uid.clone()),
nickname: args.target_nickname.clone(),
});
}
if let Some(nickname) = &args.target_nickname {
if let Some(row) = state.clients.values().find(|row| row.name == *nickname) {
return Ok(Target {
clid: Some(row.id),
dbid: Some(row.database_id),
uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
nickname: Some(row.name.clone()),
});
}
return Ok(Target {
clid: None,
dbid: args.target_dbid.map(ClientDbId),
uid_b64: None,
nickname: Some(nickname.clone()),
});
}
if let Some(dbid) = args.target_dbid {
if let Some(row) = state.clients.values().find(|row| row.database_id.0 == dbid) {
return Ok(Target {
clid: Some(row.id),
dbid: Some(row.database_id),
uid_b64: row.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
nickname: Some(row.name.clone()),
});
}
return Ok(Target {
clid: None,
dbid: Some(ClientDbId(dbid)),
uid_b64: args.target_uid.clone(),
nickname: args.target_nickname.clone(),
});
}
let own_client_id = state.own_client;
let own_client = state
.clients
.get(&own_client_id)
.ok_or_else(|| anyhow!("own client missing from state"))?;
Ok(Target {
clid: Some(own_client_id),
dbid: Some(own_client.database_id),
uid_b64: own_client.uid.as_ref().map(|uid| uid_to_b64(uid.as_ref())),
nickname: Some(own_client.name.clone()),
})
}
fn print_online_clients(state: &data::Connection) {
println!("=== Online Clients ===");
if !state.server.ips.is_empty() {
let ips = state
.server
.ips
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>()
.join(", ");
println!("Server IPs: {ips}");
}
for row in state.clients.values() {
let uid = row
.uid
.as_ref()
.map(|value| uid_to_b64(value.as_ref()))
.unwrap_or_else(|| "Hidden".to_string());
let country = if row.country_code.is_empty() {
"Unknown".to_string()
} else {
row.country_code.clone()
};
println!(
"- {} | clid={} dbid={} cid={} uid={} country={}",
row.name, row.id.0, row.database_id.0, row.channel.0, uid, country
);
}
}
fn print_detailed_summary(
target: Target,
client_info: Option<&data::OptionalClientData>,
connection_info: Option<&data::ConnectionClientData>,
db_info: Option<&InClientDbInfoPart>,
online_snapshot: Option<&OnlineTargetSnapshot>,
server_group_names: &HashMap<ServerGroupId, String>,
channel_group_names: &HashMap<ChannelGroupId, String>,
) -> Result<()> {
let nickname = db_info
.map(|info| info.name.clone())
.or(target.nickname)
.unwrap_or_else(|| "Unknown".to_string());
let uid = db_info
.map(|info| uid_to_b64(info.uid.as_ref()))
.or(target.uid_b64)
.unwrap_or_else(|| "Unknown".to_string());
let dbid = db_info
.map(|info| info.database_id.0)
.or(target.dbid.map(|value| value.0))
.unwrap_or_default();
println!();
println!("=== Client Profile ===");
println!("Nickname : {} ({})", nickname, dbid);
println!("Unique ID : {}", uid);
println!("Database ID : {}", dbid);
println!(
"Description : {}",
db_info
.map(|info| info.description.as_str())
.or_else(|| online_snapshot.map(|info| info.description.as_str()))
.unwrap_or("")
);
println!(
"Version : {}",
client_info.map(|info| info.version.as_str()).unwrap_or("Unknown")
);
println!(
"Connections : {}",
client_info
.map(|info| info.connections_total)
.or_else(|| db_info.map(|info| info.connections_total))
.unwrap_or_default()
);
println!(
"First Connected : {}",
client_info
.map(|info| format_timestamp(info.created))
.or_else(|| db_info.map(|info| format_timestamp(info.created)))
.unwrap_or_else(|| "Unknown".to_string())
);
println!(
"Last Connected : {}",
client_info
.map(|info| format_timestamp(info.last_connected))
.or_else(|| db_info.map(|info| format_timestamp(info.last_connected)))
.unwrap_or_else(|| "Unknown".to_string())
);
println!(
"Online Since : {}",
connection_info
.and_then(|info| info.connected_time.map(format_duration))
.unwrap_or_else(|| "Offline".to_string())
);
println!(
"Country : {}",
online_snapshot
.map(|info| info.country_code.as_str())
.filter(|value| !value.is_empty())
.unwrap_or("Unknown")
);
if let Some(info) = client_info {
println!("Platform : {}", info.platform);
println!(
"Server Groups : {}",
online_snapshot
.map(|snapshot| format_server_groups(&snapshot.server_groups, &server_group_names))
.unwrap_or_else(|| "Unknown".to_string())
);
println!(
"Channel Group : {}",
online_snapshot
.map(|snapshot| {
channel_group_names
.get(&snapshot.channel_group)
.cloned()
.unwrap_or_else(|| format!("Unknown ({})", snapshot.channel_group.0))
})
.unwrap_or_else(|| "Unknown".to_string())
);
println!(
"Avatar Path : {}",
online_snapshot
.map(|snapshot| {
if snapshot.avatar_hash.is_empty() {
"None".to_string()
} else {
snapshot
.uid_b64
.as_ref()
.map(|uid| format!("/avatar_{}", uid_to_avatar_path(uid)))
.unwrap_or_else(|| "Hidden".to_string())
}
})
.unwrap_or_else(|| "Unknown".to_string())
);
} else if let Some(info) = online_snapshot {
println!(
"Server Groups : {}",
format_server_groups(&info.server_groups, &server_group_names)
);
println!(
"Channel Group : {}",
channel_group_names
.get(&info.channel_group)
.cloned()
.unwrap_or_else(|| format!("Unknown ({})", info.channel_group.0))
);
println!(
"Avatar Path : {}",
if info.avatar_hash.is_empty() {
"None".to_string()
} else {
info.uid_b64
.as_ref()
.map(|uid| format!("/avatar_{}", uid_to_avatar_path(uid)))
.unwrap_or_else(|| "Hidden".to_string())
}
);
} else {
println!("Server Groups : Unknown");
println!("Channel Group : Unknown");
println!("Avatar Path : Unknown");
}
println!();
println!("=== Connection Info ===");
if let Some(connection) = connection_info {
println!(
"Connection Time : {}",
connection
.connected_time
.map(format_duration)
.unwrap_or_else(|| "Unknown".to_string())
);
println!("Idle Time : {}", format_duration(connection.idle_time));
println!(
"Ping : {}",
connection
.ping
.map(format_duration)
.unwrap_or_else(|| "Unknown".to_string())
);
println!(
"Client Address : {}",
connection
.client_address
.map(|address| address.to_string())
.unwrap_or_else(|| "Hidden".to_string())
);
println!(
"Packet Loss C->S : total={} speech={} keepalive={} control={}",
format_loss(connection.client_to_server_packetloss_total),
format_loss(connection.client_to_server_packetloss_speech),
format_loss(connection.client_to_server_packetloss_keepalive),
format_loss(connection.client_to_server_packetloss_control),
);
println!(
"Packet Loss S->C : total={} speech={} keepalive={} control={}",
connection
.server_to_client_packetloss_total
.map(format_loss)
.unwrap_or_else(|| "Hidden".to_string()),
connection
.server_to_client_packetloss_speech
.map(format_loss)
.unwrap_or_else(|| "Hidden".to_string()),
connection
.server_to_client_packetloss_keepalive
.map(format_loss)
.unwrap_or_else(|| "Hidden".to_string()),
connection
.server_to_client_packetloss_control
.map(format_loss)
.unwrap_or_else(|| "Hidden".to_string()),
);
println!(
"Bandwidth Sent : 1s[speech={} keepalive={} control={}] 1m[speech={} keepalive={} control={}]",
option_u64(connection.bandwidth_sent_last_second_speech),
option_u64(connection.bandwidth_sent_last_second_keepalive),
option_u64(connection.bandwidth_sent_last_second_control),
option_u64(connection.bandwidth_sent_last_minute_speech),
option_u64(connection.bandwidth_sent_last_minute_keepalive),
option_u64(connection.bandwidth_sent_last_minute_control),
);
println!(
"Bandwidth Received : 1s[speech={} keepalive={} control={}] 1m[speech={} keepalive={} control={}]",
option_u64(connection.bandwidth_received_last_second_speech),
option_u64(connection.bandwidth_received_last_second_keepalive),
option_u64(connection.bandwidth_received_last_second_control),
option_u64(connection.bandwidth_received_last_minute_speech),
option_u64(connection.bandwidth_received_last_minute_keepalive),
option_u64(connection.bandwidth_received_last_minute_control),
);
println!(
"Filetransfer BW : sent={} recv={}",
option_u64(connection.filetransfer_bandwidth_sent),
option_u64(connection.filetransfer_bandwidth_received),
);
println!(
"Packets Sent : speech={} keepalive={} control={}",
option_u64(connection.packets_sent_speech),
option_u64(connection.packets_sent_keepalive),
option_u64(connection.packets_sent_control),
);
println!(
"Packets Received : speech={} keepalive={} control={}",
option_u64(connection.packets_received_speech),
option_u64(connection.packets_received_keepalive),
option_u64(connection.packets_received_control),
);
println!(
"Bytes Sent : speech={} keepalive={} control={}",
option_u64(connection.bytes_sent_speech),
option_u64(connection.bytes_sent_keepalive),
option_u64(connection.bytes_sent_control),
);
println!(
"Bytes Received : speech={} keepalive={} control={}",
option_u64(connection.bytes_received_speech),
option_u64(connection.bytes_received_keepalive),
option_u64(connection.bytes_received_control),
);
} else {
println!("Connection details unavailable (offline or permission-gated).");
}
println!();
println!("=== Transfer Quota ===");
if let Some(info) = client_info {
println!("Downloaded this month : {}", info.bytes_downloaded_month);
println!("Uploaded this month : {}", info.bytes_uploaded_month);
println!("Downloaded total : {}", info.bytes_downloaded_total);
println!("Uploaded total : {}", info.bytes_uploaded_total);
} else if let Some(info) = db_info {
println!("Downloaded this month : {}", info.bytes_downloaded_month);
println!("Uploaded this month : {}", info.bytes_uploaded_month);
println!("Downloaded total : {}", info.bytes_downloaded_total);
println!("Uploaded total : {}", info.bytes_uploaded_total);
} else {
println!("Quota details unavailable.");
}
Ok(())
}
fn format_server_groups(groups: &[ServerGroupId], names: &HashMap<ServerGroupId, String>) -> String {
let mut rendered = Vec::new();
for group in groups {
rendered.push(
names
.get(group)
.cloned()
.unwrap_or_else(|| format!("Unknown ({})", group.0)),
);
}
rendered.join(", ")
}
fn format_timestamp(value: OffsetDateTime) -> String {
value
.format(&Rfc3339)
.unwrap_or_else(|_| value.unix_timestamp().to_string())
}
fn format_duration(duration: Duration) -> String {
let total_ms = duration.whole_milliseconds();
if total_ms < 1000 {
return format!("{total_ms} ms");
}
let total_seconds = duration.whole_seconds();
let seconds = total_seconds % 60;
let minutes = (total_seconds / 60) % 60;
let hours = total_seconds / 3600;
if hours > 0 {
format!("{hours}h {minutes}m {seconds}s")
} else if minutes > 0 {
format!("{minutes}m {seconds}s")
} else {
format!("{seconds}s")
}
}
fn format_loss(value: f32) -> String {
format!("{value:.4}")
}
fn option_u64(value: Option<u64>) -> String {
value
.map(|number| number.to_string())
.unwrap_or_else(|| "Hidden".to_string())
}
fn uid_to_b64(uid: &Uid) -> String {
BASE64_STANDARD.encode(&uid.0)
}
fn uid_to_avatar_path(uid_b64: &str) -> String {
let decoded = BASE64_STANDARD.decode(uid_b64).unwrap_or_default();
let mut rendered = String::with_capacity(decoded.len() * 2);
for byte in decoded {
rendered.push((b'a' + (byte >> 4)) as char);
rendered.push((b'a' + (byte & 0x0f)) as char);
}
rendered
}