fix(clients): pin mDNS discovery to IPv4 on every client
Host machines' OS mDNS responders answer AAAA for hostname.local even though the punktfunk stack binds IPv4 sockets exclusively, so a v6-resolved address rendered a host card whose connect always failed. Apple's Network.framework prefers IPv6 (RFC 6724) — force the resolve connection to v4; pf-client-core (Linux/Windows/Deck shells) now picks get_addresses_v4() instead of an arbitrary first address. Android already did exactly this, for the same reason. Lift all three when the stack actually speaks IPv6 (per-family shard sizing is the gating item — the IPv6 header costs 20 bytes of the MTU budget shard_payload is maximal against). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -127,7 +127,16 @@ public final class HostDiscovery: ObservableObject {
|
|||||||
.map { $0.trimmingCharacters(in: .whitespaces) }
|
.map { $0.trimmingCharacters(in: .whitespaces) }
|
||||||
.filter { !$0.isEmpty }
|
.filter { !$0.isEmpty }
|
||||||
}
|
}
|
||||||
let conn = NWConnection(to: result.endpoint, using: .udp)
|
// Resolve over IPv4 only: Network.framework prefers IPv6 (RFC 6724), and the host's OS
|
||||||
|
// mDNS responder often answers AAAA for its hostname even though the punktfunk host stack
|
||||||
|
// (control QUIC + data UDP) binds IPv4 sockets exclusively — a v6-resolved address would
|
||||||
|
// produce a host card whose connect always fails in the Rust core (`host:port` parse).
|
||||||
|
// Same policy as the Android/desktop clients; lift when the stack speaks IPv6.
|
||||||
|
let params = NWParameters.udp
|
||||||
|
if let ip = params.defaultProtocolStack.internetProtocol as? NWProtocolIP.Options {
|
||||||
|
ip.version = .v4
|
||||||
|
}
|
||||||
|
let conn = NWConnection(to: result.endpoint, using: params)
|
||||||
connections[key] = conn
|
connections[key] = conn
|
||||||
conn.stateUpdateHandler = { state in
|
conn.stateUpdateHandler = { state in
|
||||||
MainActor.assumeIsolated { [weak self] in
|
MainActor.assumeIsolated { [weak self] in
|
||||||
|
|||||||
@@ -61,7 +61,14 @@ pub fn browse() -> async_channel::Receiver<DiscoveryEvent> {
|
|||||||
ServiceEvent::ServiceResolved(info) => {
|
ServiceEvent::ServiceResolved(info) => {
|
||||||
let props = info.get_properties();
|
let props = info.get_properties();
|
||||||
let val = |k: &str| props.get_property_val_str(k).unwrap_or("").to_string();
|
let val = |k: &str| props.get_property_val_str(k).unwrap_or("").to_string();
|
||||||
let Some(addr) = info.get_addresses().iter().next().map(|a| a.to_string())
|
// IPv4 only, on purpose (same policy as the Android client): the core
|
||||||
|
// dials `format!("{host}:{port}").parse::<SocketAddr>()` over IPv4-bound
|
||||||
|
// sockets, so a v6 pick from this unordered address set (the host's OS
|
||||||
|
// responder often answers AAAA for its hostname) would render a host card
|
||||||
|
// that fails on every click. A v6-only advert is dropped — the honest
|
||||||
|
// "not found" — until the stack actually speaks IPv6.
|
||||||
|
let Some(addr) =
|
||||||
|
info.get_addresses_v4().iter().next().map(|a| a.to_string())
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user