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:
@@ -20,12 +20,25 @@
|
||||
//!
|
||||
//! Promoted from `poc/tsclientlib-connect-spike` on 2026-05-14
|
||||
//! as part of the Alpha build.
|
||||
//!
|
||||
//! ## Hostname resolution (A.1)
|
||||
//!
|
||||
//! Upstream `tsclientlib` uses `hickory-resolver` which reads
|
||||
//! `/etc/resolv.conf`. That file does not exist on Android or iOS,
|
||||
//! and the Beta UI surfaced the resulting cryptic "connection task
|
||||
//! exited before signalling ready" errors. We side-step the issue by
|
||||
//! resolving hostnames ourselves with `tokio::net::lookup_host`,
|
||||
//! which uses platform `getaddrinfo` (works correctly on every
|
||||
//! supported platform), and feeding the resulting `SocketAddr`
|
||||
//! directly to `tsclientlib::Connection::build`. A small in-process
|
||||
//! positive-result cache keeps reconnects fast.
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod adapter;
|
||||
mod dto;
|
||||
mod resolver;
|
||||
|
||||
pub use adapter::{ConnectConfig, InboundVoice, ProtocolClient};
|
||||
pub use dto::{ChannelInfo, ClientInfo, ServerSnapshot};
|
||||
@@ -51,6 +64,17 @@ pub enum ProtocolError {
|
||||
#[error("invalid protocol configuration: {0}")]
|
||||
Invalid(String),
|
||||
|
||||
/// Hostname resolution failed. Distinct from [`Self::Connect`]
|
||||
/// so the UI can show a meaningful "Server not found" message
|
||||
/// instead of a generic connection error.
|
||||
#[error("dns lookup failed for '{host}': {reason}")]
|
||||
DnsFailed {
|
||||
/// The hostname (or `host:port`) the caller submitted.
|
||||
host: String,
|
||||
/// Reason from the platform resolver.
|
||||
reason: String,
|
||||
},
|
||||
|
||||
/// Failed to dial / handshake with the server.
|
||||
#[error("connect failed: {0}")]
|
||||
Connect(String),
|
||||
|
||||
Reference in New Issue
Block a user