feat(protocol): A.1 — fix hostname resolution on Android and iOS
Resolves the Beta-blocking issue surfaced during Android v0.2.0-beta.1
verification: hostnames could not be used, only literal IPs.
Root cause:
tsclientlib's built-in resolver uses hickory-resolver, which reads
/etc/resolv.conf. That file does not exist on Android or iOS, so
any connect by hostname exited the connection task before
signalling ready and surfaced the cryptic error
BridgeError.connection(field0: protocol backend:
connection task exited before signalling ready)
Fix:
crates/chanora_protocol/src/resolver.rs (new):
Resolves hostnames via tokio::net::lookup_host, which uses the
platform's getaddrinfo. Works on every platform Chanora targets.
Tiny in-process positive-result cache (5 min TTL) keeps
reconnects cheap. IPv4 sorted ahead of IPv6 in the returned list
to favour the more reliable path on dual-stack networks.
crates/chanora_protocol/src/adapter.rs:
connection_task now resolves the hostname itself and passes the
resulting SocketAddr (not the hostname String) to
tsclientlib::Connection::build. tsclientlib's ServerAddress enum
accepts SocketAddr via its From impl, so the upstream resolver
is skipped entirely.
crates/chanora_protocol/src/lib.rs:
New typed error arm ProtocolError::DnsFailed { host, reason }
so the UI can distinguish 'server not found' from 'server
refused our packets'.
crates/chanora_bridge/src/lib.rs:
Matching BridgeError::DnsFailed { host, reason } DTO surfaced
to Dart, with explicit From<CoreError::Protocol(DnsFailed)>
mapping so the UI gets the structured fields rather than a
stringified mess.
Tests added (crates/chanora_protocol/src/resolver.rs::tests):
- rejects_empty
- literal_ipv4_short_circuits
- literal_ipv4_default_port_path
- unresolvable_returns_dns_failed
- resolves_known_hostname (#[ignore], --ignored to run; hits net)
Empirical verification (2026-05-14):
Workspace: cargo check + cargo test --workspace clean.
Live resolver test: cn.teamspeak.app → 175.178.125.23:9987 (passes).
cargo test -p chanora_core --test alpha_smoke -- --ignored:
server='Vigorous Pro' channels=42 clients=20 (passes by hostname).
flutter test: alpha_e2e_test + beta_e2e_test both green.
Physical Moto G Stylus 5G (Android 14 arm64-v8a):
APK rebuilt (48.9 MB). adb install + launch.
Connect form left at default 'cn.teamspeak.app'.
logcat shows:
chanora_protocol: dns resolved input=cn.teamspeak.app
resolved=175.178.125.23:9987
tsclientlib: starting connection to 175.178.125.23:9987
tsproto::resend: Connecting → Connected
chanora_protocol: initial state snapshot received
UI shows 'Connected to Vigorous Pro' / '42 channels • 20 online'.
This is the first item in Category A (post-Beta polish bundle).
Pause point: review before A.6 (full reconnect).
This commit is contained in:
@@ -165,7 +165,32 @@ async fn connection_task(
|
||||
voice_in_tx: mpsc::Sender<InboundVoice>,
|
||||
ready_tx: oneshot::Sender<Result<(), ProtocolError>>,
|
||||
) {
|
||||
let mut builder = Connection::build(cfg.address.clone()).name(cfg.nickname.clone());
|
||||
// Resolve the hostname OURSELVES using the platform resolver.
|
||||
// tsclientlib's built-in hickory-resolver reads /etc/resolv.conf,
|
||||
// which does not exist on Android or iOS — by side-stepping it
|
||||
// here we get hostname connects working on every platform.
|
||||
let addrs = match crate::resolver::resolve(&cfg.address).await {
|
||||
Ok(a) => a,
|
||||
Err(e) => {
|
||||
let _ = ready_tx.send(Err(e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Pick the first address (IPv4 preferred by the resolver's
|
||||
// ordering). Future retry logic could fall back to subsequent
|
||||
// addresses; one is enough for the Beta connect flow.
|
||||
let resolved = addrs[0];
|
||||
info!(
|
||||
target: "chanora_protocol",
|
||||
input = %cfg.address,
|
||||
resolved = %resolved,
|
||||
"dns resolved"
|
||||
);
|
||||
|
||||
// Pass the resolved SocketAddr directly to tsclientlib so it
|
||||
// skips its own resolver entirely (tsclientlib accepts
|
||||
// SocketAddr via the From<SocketAddr> for ServerAddress impl).
|
||||
let mut builder = Connection::build(resolved).name(cfg.nickname.clone());
|
||||
|
||||
let identity = match cfg.identity.as_deref() {
|
||||
Some(s) => match Identity::new_from_str(s) {
|
||||
|
||||
Reference in New Issue
Block a user