feat: add TeamSpeak address resolver

This commit is contained in:
Edison Jwa
2026-05-25 01:12:50 +09:00
parent d7556cd39f
commit eb9014cd81
15 changed files with 2360 additions and 314 deletions
+11
View File
@@ -0,0 +1,11 @@
/target/
# Local editor and OS files.
.DS_Store
*.swp
*.swo
# Local environment overrides.
.env
.env.*
!.env.example
+13
View File
@@ -0,0 +1,13 @@
[package]
name = "chanora_resolver"
version = "0.1.0"
edition = "2021"
build = "build.rs"
[dependencies]
anyhow = "1"
hickory-resolver = { version = "0.26", features = ["tokio", "system-config"] }
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
tokio = { version = "1", features = ["io-util", "macros", "net", "rt-multi-thread", "signal", "time"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["fmt", "env-filter"] }
+104
View File
@@ -0,0 +1,104 @@
# chanora_resolver
The library accepts the same kind of raw server input a client UI receives and returns a final `host:port` address suitable for `tsclientlib::Connection::build` or Chanora's `ConnectConfig.address`.
## Resolution Flow
Use `ChanoraResolver::resolve_client_request` or `resolve_client_address` for app code. The resolver owns the decision tree:
1. Normalize raw client input, including `ts3server://host?port=...`.
2. For dotless names such as `6666` or `wwb`, query the myTeamSpeak server-name endpoint first. If it returns a hostname, restart normal resolution with that hostname. If it returns `ip:port`, use that final address.
3. Start plain A/AAAA DNS as the final fallback address.
4. Prefer `_ts3._udp.<host>` SRV. Its target host and port override the user input port, then the target is resolved to a final IP.
5. Try `_tsdns._tcp.<candidate>` SRV on candidate parent/full hosts and query the returned TSDNS server over TCP.
6. Try direct TSDNS TCP on candidate parent/full hosts at port `41144`.
7. Fall back to the A/AAAA result with the user-supplied port, or default TeamSpeak port `9987`.
The older explicit API (`Args { host, service, protocol }`) is still available for diagnostic example use.
## DNS Setup Guide
This chart is for choosing DNS records when operating a TeamSpeak 3 server. It is not the client lookup order; the library still implements the client-side resolver behavior above.
```mermaid
flowchart TD
A["Which DNS type fits your TeamSpeak 3 server?"]
A --> X["Don't do this!"]
X --> X1["Do not use simple TSDNS<br/>without SRV TSDNS"]
A --> B{"How many servers do you have?"}
B -->|One virtual server| C{"Does your server use<br/>the default port 9987?"}
C -->|Yes| D{"Do all services under this domain<br/>run on the same server<br/>as the TS3 server?"}
D -->|Yes| E1["Use A/AAAA or CNAME"]
D -->|No| E2["Use SRV TS3"]
C -->|No| F{"Are you okay with<br/>entering the port manually?"}
F -->|No| E3["Use SRV TS3"]
F -->|Yes| G{"Do all services under this domain<br/>run on the same server<br/>as the TS3 server?"}
G -->|Yes| E4["Use A/AAAA or CNAME"]
G -->|No| E5["Use SRV TSDNS"]
B -->|More than one virtual server| H{"Are you okay with port numbers?"}
H -->|No| I1["Use subdomains with SRV TS3<br/>for each subdomain"]
H -->|Yes| I{"On how many servers<br/>do you run TS3 servers?"}
I -->|Only one| J{"Do all services under this domain<br/>run on the same server<br/>as the TS3 server?"}
J -->|Yes| K1["Use A/AAAA or CNAME"]
J -->|No| K2["Use SRV TSDNS"]
I -->|More than one| K3["Use subdomains with A/AAAA<br/>or CNAME for each server"]
```
## Library Example
```rust
use chanora_resolver::ChanoraResolver;
let client = ChanoraResolver::new()?;
let address = client.resolve_client_address("voice.teamspeak.com").await?;
// tsclientlib:
// let mut connection = tsclientlib::Connection::build(address).connect()?;
// Chanora:
// let cfg = chanora_core::ConnectConfig {
// address,
// nickname,
// password,
// identity,
// ready_timeout,
// };
// let snapshot = session.connect(cfg).await?;
```
## Run
```bash
cargo run --example library -- voice.teamspeak.com
cargo run --example library -- 6666
cargo run --example library -- wwb
cargo run --example library -- kr.teamspeak.app
cargo run --example library -- 'ts3server://voice.teamspeak.com?port=9987'
cargo run --example cli -- -host voice.teamspeak.com -service ts3
```
`example_log` contains live sample outputs for TS3 SRV, DNS fallback, server-name lookup, and unsuccessful TSDNS attempts that must remain non-fatal.
## Fail-Safes
- Bare numeric names such as `6666` are treated as server names first, avoiding OS DNS coercion into numeric IPv4 addresses.
- SRV targets and TSDNS responses are resolved to final IP addresses before handoff, so Chanora can pass a concrete `SocketAddr`-style `host:port` to `tsclientlib`.
- TSDNS TCP lookups use short timeouts and are non-fatal; DNS fallback remains available when TSDNS is absent or unreachable.
- Android uses an explicit SRV resolver configuration instead of relying on system resolver initialization in a raw binary context.
## Test
```bash
cargo fmt --all -- --check
cargo test
cargo clippy --all-targets --all-features -- -D warnings
```
+25
View File
@@ -0,0 +1,25 @@
use std::{env, process::Command};
fn main() {
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-changed=build.rs");
let version = env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "development".to_string());
let commit = git_output(&["rev-parse", "--short", "HEAD"]).unwrap_or_else(|| "unknown".into());
let date =
git_output(&["show", "-s", "--format=%cI", "HEAD"]).unwrap_or_else(|| "unknown".into());
println!("cargo:rustc-env=CHANORA_RESOLVER_BUILD_VERSION={version}");
println!("cargo:rustc-env=CHANORA_RESOLVER_BUILD_COMMIT={commit}");
println!("cargo:rustc-env=CHANORA_RESOLVER_BUILD_DATE={date}");
}
fn git_output(args: &[&str]) -> Option<String> {
let output = Command::new("git").args(args).output().ok()?;
if !output.status.success() {
return None;
}
let value = String::from_utf8(output.stdout).ok()?;
Some(value.trim().to_owned())
}
+229
View File
@@ -0,0 +1,229 @@
1. -----------
2026/5/22 21:29:16 ClientUI Info Connect to server: teamspeak.app
2026/5/22 21:29:16 ClientUI Info Trying to resolve teamspeak.app
2026/5/22 21:29:16 TSDNS Info A/AAAA DNS resolve successful, "teamspeak.app" =(h: 13.33.183.64 p:0)
2026/5/22 21:29:16 TSDNS Info A/AAAA DNS resolve for possible TSDNS successful, "teamspeak.app" =(h: 13.33.183.4 p:0)
2026/5/22 21:29:16 TSDNS Info SRV DNS resolve unsuccessful, "_tsdns._tcp.teamspeak.app" Domain name not found
2026/5/22 21:29:16 TSDNS Info SRV DNS resolve successful, "_ts3._udp.teamspeak.app" =(h: global.teamspeak.app p:9987)
2026/5/22 21:29:16 TSDNS Info A/AAAA DNS resolve successful, "global.teamspeak.app" =(h: 103.224.172.62 p:0)
2026/5/22 21:29:16 ClientUI Info Lookup finished: ip=103.224.172.62 port=9987 query=teamspeak.app error=0
2026/5/22 21:29:16 ClientUI Info Resolve successful: 103.224.172.62:9987
2026/5/22 21:29:16 ClientUI Info Initiating connection: 103.224.172.62:9987
2026/5/22 21:29:16 ClientUI Info Connect status: Connecting
2026/5/22 21:29:17 PktHandler Info server sent ciphers:1
2026/5/22 21:29:17 PktHandler Info Selected cipher:0
2026/5/22 21:29:17 PktHandler Devel Puzzle solve time: 3
2026/5/22 21:29:18 ClientUI Info Connect status: Connected
2026/5/22 21:29:18 ClientUI Info Connect status: Establishing connection
2026/5/22 21:29:18 TSDNS Info TSDNS queried unsuccessfully 13.33.183.4:41144
2026/5/22 21:29:18 TSDNS Info No TSDNS found
2026/5/22 21:29:18 ClientUI Info Connect status: Connection established
2. ----------
2026/5/22 21:30:12 ClientUI Info Connect to server: kr.teamspeak.app
2026/5/22 21:30:12 ClientUI Info Connect status: Disconnected
2026/5/22 21:30:12 ClientUI Info Disconnected or forced to leave, want autoreconnect = 0
2026/5/22 21:30:12 ClientUI Info Connecting to next server...
2026/5/22 21:30:12 ClientUI Info Connect to server: kr.teamspeak.app
2026/5/22 21:30:12 ClientUI Info Trying to resolve kr.teamspeak.app
2026/5/22 21:30:12 TSDNS Info A/AAAA DNS resolve for possible TSDNS successful, "teamspeak.app" =(h: 13.33.183.64 p:0)
2026/5/22 21:30:12 TSDNS Info SRV DNS resolve unsuccessful, "_tsdns._tcp.teamspeak.app" Domain name not found
2026/5/22 21:30:12 TSDNS Info A/AAAA DNS resolve for possible TSDNS successful, "kr.teamspeak.app" =(h: 140.238.11.238 p:0)
2026/5/22 21:30:12 TSDNS Info A/AAAA DNS resolve successful, "kr.teamspeak.app" =(h: 140.238.11.238 p:0)
2026/5/22 21:30:12 TSDNS Info SRV DNS resolve unsuccessful, "_ts3._udp.kr.teamspeak.app" Domain name not found
2026/5/22 21:30:14 TSDNS Info TSDNS queried unsuccessfully 13.33.183.64:41144
2026/5/22 21:30:14 TSDNS Info No TSDNS found
2026/5/22 21:30:14 TSDNS Info TSDNS queried unsuccessfully 140.238.11.238:41144
2026/5/22 21:30:14 TSDNS Info No TSDNS found
2026/5/22 21:30:14 ClientUI Info Lookup finished: ip=140.238.11.238 port=9987 query=kr.teamspeak.app error=0
2026/5/22 21:30:14 ClientUI Info Resolve successful: 140.238.11.238:9987
2026/5/22 21:30:14 ClientUI Info Initiating connection: 140.238.11.238:9987
2026/5/22 21:30:14 ClientUI Info Connect status: Connecting
2026/5/22 21:30:14 PktHandler Info server sent ciphers:1
2026/5/22 21:30:14 PktHandler Info Selected cipher:0
2026/5/22 21:30:14 PktHandler Devel Puzzle solve time: 3
2026/5/22 21:30:15 ClientUI Info Connect status: Connected
2026/5/22 21:30:15 ClientUI Info Connect status: Establishing connection
2026/5/22 21:30:15 ClientUI Info Connect status: Connection established
3. ---------
2026/5/22 21:40:35 ClientUI Info Connect to server: 6666
2026/5/22 21:40:35 ClientUI Info Trying to resolve 6666
2026/5/22 21:40:35 TSDNS Info Trying to resolve server name: 6666
2026/5/22 21:40:35 Addon Info Addon up to date.
2026/5/22 21:40:35 TSDNS Info Server name successfully resolved
2026/5/22 21:40:35 TSDNS Info Lookup: teamspeak.app, parse result: DNS teamspeak.app 9987
2026/5/22 21:40:35 TSDNS Info Server name resolved to host: teamspeak.app 9987
2026/5/22 21:40:35 Addon Info Addon up to date.
2026/5/22 21:40:35 TSDNS Info A/AAAA DNS resolve successful, "teamspeak.app" =(h: 13.33.183.4 p:0)
2026/5/22 21:40:35 TSDNS Info A/AAAA DNS resolve for possible TSDNS successful, "teamspeak.app" =(h: 13.33.183.4 p:0)
2026/5/22 21:40:35 TSDNS Info SRV DNS resolve successful, "_ts3._udp.teamspeak.app" =(h: global.teamspeak.app p:9987)
2026/5/22 21:40:36 TSDNS Info A/AAAA DNS resolve successful, "global.teamspeak.app" =(h: 103.224.172.62 p:0)
2026/5/22 21:40:36 ClientUI Info Lookup finished: ip=103.224.172.62 port=9987 query=teamspeak.app error=0
2026/5/22 21:40:36 ClientUI Info Resolve successful: 103.224.172.62:9987
2026/5/22 21:40:36 ClientUI Info Initiating connection: 103.224.172.62:9987
2026/5/22 21:40:36 Windows Audio Session Devel DeviceDeleteList::wait_for_deletes - enter - DeviceDeleteList
2026/5/22 21:40:36 Windows Audio Session Devel DeviceDeleteList::wait_for_deletes - leave - DeviceDeleteList
2026/5/22 21:40:36 TSDNS Info SRV DNS resolve unsuccessful, "_tsdns._tcp.teamspeak.app" Domain name not found
2026/5/22 21:40:36 Windows Audio Session Devel DeviceDeleteList::wait_for_deletes - enter - DeviceDeleteList
2026/5/22 21:40:36 Windows Audio Session Devel DeviceDeleteList::wait_for_deletes - leave - DeviceDeleteList
2026/5/22 21:40:36 Direct Sound Warning RenderDeviceContext::int_processData outerLoop proc (3) time: 41 msecs - {E4AE96A7-FDC2-4AF6-8C44-22ADDD8A821B}
2026/5/22 21:40:36 ClientUI Info Connect status: Connecting
2026/5/22 21:40:36 Addon Info Addon up to date.
2026/5/22 21:40:36 PktHandler Info server sent ciphers:1
2026/5/22 21:40:36 PktHandler Info Selected cipher:0
2026/5/22 21:40:36 PktHandler Devel Puzzle solve time: 2
2026/5/22 21:40:36 Addon Info Addon up to date.
2026/5/22 21:40:37 Info connected to push system.
2026/5/22 21:40:37 Addon Info Addon up to date.
2026/5/22 21:40:37 ClientUI Info Connect status: Connected
2026/5/22 21:40:37 ClientUI Info Connect status: Establishing connection
2026/5/22 21:40:37 Addon Info Addon up to date.
2026/5/22 21:40:37 TSDNS Info TSDNS queried unsuccessfully 13.33.183.4:41144
2026/5/22 21:40:37 TSDNS Info No TSDNS found
2026/5/22 21:40:37 ClientUI Info Connect status: Connection established
4. --------
2026/5/22 21:41:46 ClientUI Info Connect to server: voice.teamspeak.com
2026/5/22 21:41:46 ClientUI Info Connect status: Disconnected
2026/5/22 21:41:46 ClientUI Info Disconnected or forced to leave, want autoreconnect = 0
2026/5/22 21:41:46 ClientUI Info Connecting to next server...
2026/5/22 21:41:46 ClientUI Info Connect to server: voice.teamspeak.com
2026/5/22 21:41:46 ClientUI Info Trying to resolve voice.teamspeak.com
2026/5/22 21:41:46 TSDNS Info A/AAAA DNS resolve successful, "voice.teamspeak.com" =(h: 148.113.198.70 p:0)
2026/5/22 21:41:46 TSDNS Info A/AAAA DNS resolve for possible TSDNS successful, "teamspeak.com" =(h: 104.18.4.167 p:0)
2026/5/22 21:41:46 TSDNS Info A/AAAA DNS resolve for possible TSDNS successful, "voice.teamspeak.com" =(h: 148.113.198.70 p:0)
2026/5/22 21:41:46 TSDNS Info SRV DNS resolve unsuccessful, "_tsdns._tcp.teamspeak.com" Domain name not found
2026/5/22 21:41:46 TSDNS Info SRV DNS resolve successful, "_ts3._udp.voice.teamspeak.com" =(h: voice.teamspeak.com p:9987)
2026/5/22 21:41:46 TSDNS Info A/AAAA DNS resolve successful, "voice.teamspeak.com" =(h: 148.113.198.70 p:0)
2026/5/22 21:41:46 ClientUI Info Lookup finished: ip=148.113.198.70 port=9987 query=voice.teamspeak.com error=0
2026/5/22 21:41:46 ClientUI Info Resolve successful: 148.113.198.70:9987
2026/5/22 21:41:46 ClientUI Info Initiating connection: 148.113.198.70:9987
2026/5/22 21:41:46 ClientUI Info Connect status: Connecting
2026/5/22 21:41:46 PktHandler Info server sent ciphers:1
2026/5/22 21:41:46 PktHandler Info server sent ciphers:1
2026/5/22 21:41:46 PktHandler Info server sent ciphers:1
2026/5/22 21:41:47 PktHandler Info server sent ciphers:1
2026/5/22 21:41:47 PktHandler Info Selected cipher:0
2026/5/22 21:41:47 PktHandler Devel Puzzle solve time: 2
2026/5/22 21:41:48 TSDNS Info TSDNS queried unsuccessfully 104.18.4.167:41144
2026/5/22 21:41:48 TSDNS Info No TSDNS found
2026/5/22 21:41:48 TSDNS Info TSDNS queried unsuccessfully 148.113.198.70:41144
2026/5/22 21:41:48 TSDNS Info No TSDNS found
2026/5/22 21:41:48 ClientUI Info Connect status: Connected
2026/5/22 21:41:48 ClientUI Info Connect status: Establishing connection
2026/5/22 21:41:49 ClientUI Info Connect status: Connection established
+102
View File
@@ -0,0 +1,102 @@
use anyhow::{anyhow, bail, Result};
use chanora_resolver::{run, setup_log, Args};
const DEFAULT_SERVICE: &str = "ts3";
#[tokio::main]
async fn main() {
setup_log();
if let Err(err) = execute(std::env::args().skip(1)).await {
tracing::error!(err = %err, "lookup failed");
std::process::exit(1);
}
}
async fn execute<I, S>(args: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let parsed = parse_args(args)?;
run(parsed).await
}
fn parse_args<I, S>(args: I) -> Result<Args>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut host = String::new();
let mut service = DEFAULT_SERVICE.to_string();
let mut protocol = String::new();
let mut iter = args.into_iter().map(Into::into).peekable();
while let Some(arg) = iter.next() {
match arg.as_str() {
"-h" | "--help" => {
print_help();
std::process::exit(0);
}
"-host" | "--host" => {
host = next_value(&mut iter, "host")?;
}
"-service" | "--service" => {
service = next_value(&mut iter, "service")?;
}
"-protocol" | "--protocol" => {
protocol = next_value(&mut iter, "protocol")?;
}
value if value.starts_with('-') => {
bail!("unknown flag: {value}");
}
value => {
bail!("unexpected positional argument: {value}");
}
}
}
Ok(Args {
host,
service,
protocol,
})
}
fn next_value<I>(iter: &mut std::iter::Peekable<I>, flag: &str) -> Result<String>
where
I: Iterator<Item = String>,
{
iter.next()
.ok_or_else(|| anyhow!("missing value for {flag} flag"))
}
fn print_help() {
println!("chanora-resolver -host <host> -service <dns|ts3|tsdns|nick> -protocol <udp|tcp>");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_args_supports_flag_pairs() {
let args = parse_args([
"-host",
"Example.com",
"-service",
"dns",
"-protocol",
"udp",
])
.unwrap();
assert_eq!(
args,
Args {
host: "Example.com".into(),
service: "dns".into(),
protocol: "udp".into(),
}
);
}
}
@@ -0,0 +1,47 @@
use chanora_resolver::{setup_log, ChanoraResolver};
#[tokio::main]
async fn main() {
setup_log();
let input = std::env::args()
.nth(1)
.unwrap_or_else(|| "voice.teamspeak.com".into());
let client = match ChanoraResolver::new() {
Ok(client) => client,
Err(err) => {
tracing::error!(err = %err, "failed to initialize client");
std::process::exit(1);
}
};
match client.resolve_client_request(&input).await {
Ok(resolution) => {
let connect_addr = resolution.address;
tracing::info!(
input = %resolution.input,
method = ?resolution.method,
addr = %connect_addr,
"client connection target"
);
// Direct tsclientlib handoff:
// let mut connection = tsclientlib::Connection::build(connect_addr).connect()?;
//
// Chanora handoff:
// let cfg = chanora_core::ConnectConfig {
// address: connect_addr,
// nickname,
// password,
// identity,
// ready_timeout,
// };
// let snapshot = session.connect(cfg).await?;
}
Err(err) => {
tracing::error!(err = %err, "example lookup failed");
std::process::exit(1);
}
}
}
File diff suppressed because it is too large Load Diff