refactor(server): centralize remote address selection

Unify remote address resolution so startup logging, primary remote URL selection, and /api/meta all consume the same server-side policy. Keep all external addresses user-visible, preserve stable interface ordering, and only de-prioritize link-local addresses when choosing the primary recommended remote URL.

On the UI side, introduce a shared helper that keeps the first remote address visible by default and collapses the remaining addresses behind a reveal action, with i18n coverage and targeted tests for the new selection behavior.
This commit is contained in:
VooDisss
2026-04-01 17:03:41 +03:00
parent 984743f3c7
commit b0b0a55e14
15 changed files with 241 additions and 53 deletions

View File

@@ -21,7 +21,7 @@ import { launchInBrowser } from "./launcher"
import { resolveUi } from "./ui/remote-ui"
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_COOKIE_NAME, DEFAULT_AUTH_USERNAME } from "./auth/manager"
import { resolveHttpsOptions } from "./server/tls"
import { isAdvertisableRemoteAddress, resolveNetworkAddresses } from "./server/network-addresses"
import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/network-addresses"
import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
import { SpeechService } from "./speech/service"
@@ -457,13 +457,16 @@ async function main() {
let remoteHost = options.host
if (wantsAll) {
if (options.host === "0.0.0.0") {
remoteAddresses = resolveNetworkAddresses({ host: options.host, protocol: remoteProtocol, port: remoteStart.port })
remoteHost = remoteAddresses.find((addr) => isAdvertisableRemoteAddress(addr))?.ip ?? "localhost"
const resolved = resolveRemoteAddresses({ host: options.host, protocol: remoteProtocol, port: remoteStart.port })
remoteAddresses = resolved.userVisible
remoteUrl = resolved.primaryRemoteUrl ?? `${remoteProtocol}://localhost:${remoteStart.port}`
}
} else {
remoteHost = "localhost"
}
remoteUrl = `${remoteProtocol}://${remoteHost}:${remoteStart.port}`
if (!remoteUrl) {
remoteUrl = `${remoteProtocol}://${remoteHost}:${remoteStart.port}`
}
}
serverMeta.localUrl = localUrl
@@ -485,7 +488,6 @@ async function main() {
if (serverMeta.remoteUrl) {
console.log(`Remote Connection URL : ${serverMeta.remoteUrl}`)
const additionalRemoteUrls = serverMeta.addresses
.filter((addr) => isAdvertisableRemoteAddress(addr))
.map((addr) => addr.remoteUrl)
.filter((url) => url !== serverMeta.remoteUrl)

View File

@@ -2,10 +2,10 @@ import assert from "node:assert/strict"
import os from "node:os"
import { describe, it } from "node:test"
import { isAdvertisableRemoteAddress, resolveNetworkAddresses } from "../network-addresses"
import { resolveNetworkAddresses, resolveRemoteAddresses } from "../network-addresses"
describe("resolveNetworkAddresses", () => {
it("keeps RFC1918 addresses grouped without preferring one private range over another", () => {
it("preserves interface order among external addresses", () => {
const addresses = [
{ address: "172.24.0.1", family: "IPv4", internal: false },
{ address: "192.168.1.128", family: "IPv4", internal: false },
@@ -23,24 +23,24 @@ describe("resolveNetworkAddresses", () => {
)
})
})
})
it("marks link-local addresses as non-advertisable for terminal output", () => {
assert.equal(isAdvertisableRemoteAddress({ ip: "169.254.10.20", scope: "external" }), false)
assert.equal(isAdvertisableRemoteAddress({ ip: "192.168.1.128", scope: "external" }), true)
assert.equal(isAdvertisableRemoteAddress({ ip: "127.0.0.1", scope: "loopback" }), false)
})
it("keeps a usable LAN address advertisable when a link-local address is discovered first", () => {
describe("resolveRemoteAddresses", () => {
it("keeps all external addresses user-visible while preferring non-link-local addresses for the primary URL", () => {
const addresses = [
{ address: "169.254.10.20", family: "IPv4", internal: false },
{ address: "192.168.1.128", family: "IPv4", internal: false },
{ address: "172.24.0.1", family: "IPv4", internal: false },
]
usingMockedNetworkInterfaces(addresses, () => {
const result = resolveNetworkAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
const primaryAdvertisable = result.find((entry) => isAdvertisableRemoteAddress(entry))
const result = resolveRemoteAddresses({ host: "0.0.0.0", protocol: "https", port: 9898 })
assert.equal(primaryAdvertisable?.ip, "192.168.1.128")
assert.deepEqual(
result.userVisible.map((entry) => entry.ip),
["192.168.1.128", "172.24.0.1", "169.254.10.20"],
)
assert.equal(result.primaryRemoteUrl, "https://192.168.1.128:9898")
})
})
})

View File

@@ -1,6 +1,12 @@
import os from "os"
import type { NetworkAddress } from "../api-types"
export interface ResolvedRemoteAddresses {
all: NetworkAddress[]
userVisible: NetworkAddress[]
primaryRemoteUrl?: string
}
export function resolveNetworkAddresses(args: {
host: string
protocol: "http" | "https"
@@ -59,34 +65,30 @@ export function resolveNetworkAddresses(args: {
const scopeDelta = scopeWeight[a.scope] - scopeWeight[b.scope]
if (scopeDelta !== 0) return scopeDelta
const addressDelta = compareAddressPriority(a.ip, b.ip)
if (addressDelta !== 0) return addressDelta
return 0
})
}
export function isAdvertisableRemoteAddress(address: Pick<NetworkAddress, "ip" | "scope">): boolean {
if (address.scope !== "external") return false
return !isLinkLocalIPv4(address.ip)
export function resolveRemoteAddresses(args: {
host: string
protocol: "http" | "https"
port: number
}): ResolvedRemoteAddresses {
const all = resolveNetworkAddresses(args)
const userVisible = sortUserVisibleAddresses(all.filter((address) => address.scope === "external"))
return {
all,
userVisible,
primaryRemoteUrl: userVisible[0]?.remoteUrl,
}
}
function compareAddressPriority(left: string, right: string): number {
return getAddressPriority(left) - getAddressPriority(right)
function sortUserVisibleAddresses(addresses: NetworkAddress[]): NetworkAddress[] {
return [...addresses].sort((left, right) => getUserVisiblePriority(left.ip) - getUserVisiblePriority(right.ip))
}
function getAddressPriority(ip: string): number {
const octets = parseIPv4(ip)
if (!octets) return 100
const [first, second] = octets
if (isLinkLocalIPv4(ip)) return 90
if (first === 172 && second >= 16 && second <= 31) return 10
if (first === 10) return 10
if (first === 192 && second === 168) return 10
return 50
function getUserVisiblePriority(ip: string): number {
return isLinkLocalIPv4(ip) ? 1 : 0
}
function isLinkLocalIPv4(ip: string): boolean {

View File

@@ -1,6 +1,6 @@
import { FastifyInstance } from "fastify"
import { ServerMeta } from "../../api-types"
import { resolveNetworkAddresses } from "../network-addresses"
interface RouteDeps {
serverMeta: ServerMeta
@@ -13,14 +13,12 @@ export function registerMetaRoutes(app: FastifyInstance, deps: RouteDeps) {
function buildMetaResponse(meta: ServerMeta): ServerMeta {
const localPort = resolveLocalPort(meta)
const remote = resolveRemote(meta)
const addresses = remote && remote.port > 0 ? resolveNetworkAddresses({ host: meta.host, protocol: remote.protocol, port: remote.port }) : []
return {
...meta,
localPort,
remotePort: remote?.port,
listeningMode: meta.host === "0.0.0.0" || !isLoopbackHost(meta.host) ? "all" : "local",
addresses,
}
}