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:
EdisonJwa
2026-05-15 00:17:13 +08:00
parent 1324f478fe
commit bc0da50cdb
6 changed files with 266 additions and 1 deletions
+24
View File
@@ -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),