fix(companion): first connect no longer stalls on unresolvable .fips dial hints — 0.5.12
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m59s
All checks were successful
Demo images / Build & push demo images (push) Successful in 2m59s
A pairing QR minted while the node UI was browsed over the mesh carried npub….fips as fhost. Android's system DNS can't resolve .fips, so every direct handshake dial failed and first connect crawled through public- anchor discovery (2m35s observed) instead of one LAN hop. - node UI: resolveServerUrl treats .fips names and IPv6 ULA literals as phone-unreachable and substitutes the node's LAN IP (same guard as tailnet/localhost) - app: QR parser falls back to the url param's host when fhost is .fips - app: peer store never keeps .fips dial addresses - app: peer aliases slugged for the fips host map (spaces dropped "Framework PT"/"Mesh anchor" from name resolution entirely) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
385c950f00
commit
7e3d01f633
@ -11,8 +11,8 @@ android {
|
|||||||
applicationId = "com.archipelago.app"
|
applicationId = "com.archipelago.app"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 31
|
versionCode = 32
|
||||||
versionName = "0.5.11"
|
versionName = "0.5.12"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
|
|||||||
@ -88,8 +88,18 @@ object ServerQrParser {
|
|||||||
|
|
||||||
private fun parseFips(uri: Uri): FipsPairInfo? {
|
private fun parseFips(uri: Uri): FipsPairInfo? {
|
||||||
val npub = uri.getQueryParameter("fnpub")?.trim()
|
val npub = uri.getQueryParameter("fnpub")?.trim()
|
||||||
val host = uri.getQueryParameter("fhost")?.trim()
|
var host = uri.getQueryParameter("fhost")?.trim()
|
||||||
if (npub.isNullOrBlank() || host.isNullOrBlank()) return null
|
if (npub.isNullOrBlank() || host.isNullOrBlank()) return null
|
||||||
|
// A .fips fhost is a dead dial hint: Android's system DNS can't
|
||||||
|
// resolve .fips, so every handshake send fails and first connect
|
||||||
|
// falls back to slow anchor discovery. If the QR's `url` host is a
|
||||||
|
// real address, dial that instead (QRs minted while the node UI was
|
||||||
|
// browsed over the mesh carry npub….fips here).
|
||||||
|
if (host.endsWith(".fips")) {
|
||||||
|
val urlHost = uri.getQueryParameter("url")
|
||||||
|
?.let { runCatching { Uri.parse(it).host }.getOrNull() }
|
||||||
|
if (!urlHost.isNullOrBlank() && !urlHost.endsWith(".fips")) host = urlHost
|
||||||
|
}
|
||||||
return FipsPairInfo(
|
return FipsPairInfo(
|
||||||
npub = npub,
|
npub = npub,
|
||||||
ula = uri.getQueryParameter("fip")?.trim().orEmpty(),
|
ula = uri.getQueryParameter("fip")?.trim().orEmpty(),
|
||||||
|
|||||||
@ -126,7 +126,7 @@ class FipsPreferences(private val context: Context) {
|
|||||||
if (peer.ip.isBlank() || peer.port <= 0) continue
|
if (peer.ip.isBlank() || peer.port <= 0) continue
|
||||||
merged.put(JSONObject().apply {
|
merged.put(JSONObject().apply {
|
||||||
put("npub", peer.npub)
|
put("npub", peer.npub)
|
||||||
put("alias", peer.name.ifBlank { "Party phone" })
|
put("alias", hostSafeAlias(peer.name.ifBlank { "party-phone" }))
|
||||||
put("addresses", JSONArray().put(JSONObject().apply {
|
put("addresses", JSONArray().put(JSONObject().apply {
|
||||||
put("transport", "udp")
|
put("transport", "udp")
|
||||||
put("addr", "${peer.ip}:${peer.port}")
|
put("addr", "${peer.ip}:${peer.port}")
|
||||||
@ -145,7 +145,7 @@ class FipsPreferences(private val context: Context) {
|
|||||||
) {
|
) {
|
||||||
merged.put(JSONObject().apply {
|
merged.put(JSONObject().apply {
|
||||||
put("npub", ARCHY_ANCHOR_NPUB)
|
put("npub", ARCHY_ANCHOR_NPUB)
|
||||||
put("alias", "Archipelago anchor")
|
put("alias", "archipelago-anchor")
|
||||||
put("addresses", JSONArray().put(JSONObject().apply {
|
put("addresses", JSONArray().put(JSONObject().apply {
|
||||||
put("transport", ARCHY_ANCHOR_TRANSPORT)
|
put("transport", ARCHY_ANCHOR_TRANSPORT)
|
||||||
put("addr", ARCHY_ANCHOR_ADDR)
|
put("addr", ARCHY_ANCHOR_ADDR)
|
||||||
@ -200,16 +200,19 @@ class FipsPreferences(private val context: Context) {
|
|||||||
val incoming = mutableListOf<JSONObject>()
|
val incoming = mutableListOf<JSONObject>()
|
||||||
incoming += JSONObject().apply {
|
incoming += JSONObject().apply {
|
||||||
put("npub", info.npub)
|
put("npub", info.npub)
|
||||||
put("alias", alias.ifBlank { "Archipelago" })
|
put("alias", hostSafeAlias(alias.ifBlank { "Archipelago" }))
|
||||||
val addresses = JSONArray()
|
val addresses = JSONArray()
|
||||||
if (info.udpPort > 0) {
|
// .fips hosts are unresolvable on Android (no system .fips DNS):
|
||||||
|
// storing one gives the mesh a dial target that fails every
|
||||||
|
// handshake and stalls first connect on anchor discovery.
|
||||||
|
if (info.udpPort > 0 && !info.host.endsWith(".fips")) {
|
||||||
addresses.put(JSONObject().apply {
|
addresses.put(JSONObject().apply {
|
||||||
put("transport", "udp")
|
put("transport", "udp")
|
||||||
put("addr", "${info.host}:${info.udpPort}")
|
put("addr", "${info.host}:${info.udpPort}")
|
||||||
put("priority", 10)
|
put("priority", 10)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (info.tcpPort > 0) {
|
if (info.tcpPort > 0 && !info.host.endsWith(".fips")) {
|
||||||
addresses.put(JSONObject().apply {
|
addresses.put(JSONObject().apply {
|
||||||
put("transport", "tcp")
|
put("transport", "tcp")
|
||||||
put("addr", "${info.host}:${info.tcpPort}")
|
put("addr", "${info.host}:${info.tcpPort}")
|
||||||
@ -220,9 +223,10 @@ class FipsPreferences(private val context: Context) {
|
|||||||
}
|
}
|
||||||
for (anchor in info.anchors) {
|
for (anchor in info.anchors) {
|
||||||
if (anchor.npub == info.npub) continue
|
if (anchor.npub == info.npub) continue
|
||||||
|
if (anchor.addr.substringBeforeLast(":").endsWith(".fips")) continue
|
||||||
incoming += JSONObject().apply {
|
incoming += JSONObject().apply {
|
||||||
put("npub", anchor.npub)
|
put("npub", anchor.npub)
|
||||||
put("alias", "Mesh anchor")
|
put("alias", "mesh-anchor")
|
||||||
put("addresses", JSONArray().put(JSONObject().apply {
|
put("addresses", JSONArray().put(JSONObject().apply {
|
||||||
put("transport", anchor.transport)
|
put("transport", anchor.transport)
|
||||||
put("addr", anchor.addr)
|
put("addr", anchor.addr)
|
||||||
@ -237,7 +241,7 @@ class FipsPreferences(private val context: Context) {
|
|||||||
) {
|
) {
|
||||||
incoming += JSONObject().apply {
|
incoming += JSONObject().apply {
|
||||||
put("npub", ARCHY_ANCHOR_NPUB)
|
put("npub", ARCHY_ANCHOR_NPUB)
|
||||||
put("alias", "Archipelago anchor")
|
put("alias", "archipelago-anchor")
|
||||||
put("addresses", JSONArray().put(JSONObject().apply {
|
put("addresses", JSONArray().put(JSONObject().apply {
|
||||||
put("transport", ARCHY_ANCHOR_TRANSPORT)
|
put("transport", ARCHY_ANCHOR_TRANSPORT)
|
||||||
put("addr", ARCHY_ANCHOR_ADDR)
|
put("addr", ARCHY_ANCHOR_ADDR)
|
||||||
@ -258,3 +262,14 @@ class FipsPreferences(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Peer aliases feed the fips host map as `<alias>.fips` hostnames; anything
|
||||||
|
* that isn't a valid DNS label ("Framework PT" — the space) gets rejected and
|
||||||
|
* silently drops the peer from name resolution. Slug it instead of losing it.
|
||||||
|
*/
|
||||||
|
internal fun hostSafeAlias(alias: String): String =
|
||||||
|
alias.lowercase()
|
||||||
|
.replace(Regex("[^a-z0-9.-]+"), "-")
|
||||||
|
.trim('-', '.')
|
||||||
|
.ifBlank { "archipelago" }
|
||||||
|
|||||||
Binary file not shown.
@ -267,12 +267,20 @@ function isTailnetIp(host: string): boolean {
|
|||||||
* - a tailnet 100.x address (operator browsing over Tailscale — a scanned
|
* - a tailnet 100.x address (operator browsing over Tailscale — a scanned
|
||||||
* QR carried one of these on 2026-07-22 and the companion sat there
|
* QR carried one of these on 2026-07-22 and the companion sat there
|
||||||
* dialing an IP the phone had no route to)
|
* dialing an IP the phone had no route to)
|
||||||
|
* - a .fips name or mesh ULA (operator browsing over the mesh — a scanned
|
||||||
|
* QR carried npub….fips as fhost on 2026-07-24; Android's system DNS
|
||||||
|
* can't resolve .fips, so the phone's direct dial died and first
|
||||||
|
* connect crawled through anchor discovery instead of the LAN)
|
||||||
*/
|
*/
|
||||||
async function resolveServerUrl(): Promise<string> {
|
async function resolveServerUrl(): Promise<string> {
|
||||||
if (IS_DEMO) return DEMO_SERVER_URL
|
if (IS_DEMO) return DEMO_SERVER_URL
|
||||||
const { hostname, origin } = window.location
|
const { hostname, origin } = window.location
|
||||||
const phoneUnreachable =
|
const phoneUnreachable =
|
||||||
hostname === 'localhost' || hostname === '127.0.0.1' || isTailnetIp(hostname)
|
hostname === 'localhost' ||
|
||||||
|
hostname === '127.0.0.1' ||
|
||||||
|
isTailnetIp(hostname) ||
|
||||||
|
hostname.endsWith('.fips') ||
|
||||||
|
hostname.includes(':') // IPv6 literal — the node's mesh ULA
|
||||||
if (!phoneUnreachable) return origin
|
if (!phoneUnreachable) return origin
|
||||||
try {
|
try {
|
||||||
const res = await rpcClient.call<{ mdns_hostname?: string; lan_ip?: string | null }>({
|
const res = await rpcClient.call<{ mdns_hostname?: string; lan_ip?: string | null }>({
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user