fix(wsl-host): pair again from the distro when a WSL host refuses the enrolled token (#31745)

* fix(wsl-host): pair again from the distro when a WSL host refuses the enrolled token

A WSL host that lost its device registry, or revoked this desktop, refused the
enrolled token forever, and an update over it read as deferred. The connect now
re-pairs through the managed repair gate SSH uses (one repair at a time, a
capped number per window, only an offer under the pinned host key), reading the
host's published offer from the distro. The incumbent's terminal count uses the
same enrolled-or-live pairing the connect does.

* fix(wsl-host): retire a bridge whose pairing was refused, and let a user's Connect retry a repair

A refused or repaired-then-refused pairing on a reused bridge left its stale
token cached, and the failure status then saved it over the repair. That bridge
now closes first. A user-initiated Connect clears a standing repair failure or a
spent cap, as an explicit SSH connect does.

* fix(wsl-host): retire a refused bridge without cancelling a queued Update or Force

* fix(wsl-host): repair only a stored pairing, dispose the repair's transport directly, and log refusals neutrally

A host still being added has no stored record for the gate to re-pair, so its
live bridge is reused as before. The WSL repair deps are typed with the WSL
transport, and the shared refusal log no longer says SSH for WSL.
This commit is contained in:
Neil
2026-10-21 23:15:59 -07:00
committed by GitHub
parent 8e1ee111e4
commit 1769476014
11 changed files with 399 additions and 58 deletions
@@ -19,7 +19,7 @@ import {
execOrcadRemote,
type OrcadRemoteExecTarget
} from '../install/orcad-remote-runtime-control'
import type { OrcadHostTransport } from '../../ssh/orcad-host-transport'
import type { OrcadHostTarget, OrcadHostTransport } from '../../ssh/orcad-host-transport'
export type OrcadTunnelPortInput = {
environment: KnownRuntimeEnvironment
@@ -52,10 +52,11 @@ async function readManagedOrcadSlotReadiness(
return parsed.state === 'ready' ? parsed.readiness : null
}
/** What the active slot publishes now, read over SSH; null when none is active or ready. */
export async function readActiveManagedOrcadReadiness(
input: Pick<OrcadTunnelPortInput, 'target' | 'connection'>
): Promise<ServeReadiness | null> {
/** What the active slot publishes now, over SSH or WSL; null when none is active or ready. */
export async function readActiveManagedOrcadReadiness(input: {
target: OrcadHostTarget
connection: OrcadHostTransport
}): Promise<ServeReadiness | null> {
const context = await resolveOrcadRemoteContext(input.target, input.connection)
const active = context.activationRecord.active
if (!active) {
@@ -14,9 +14,8 @@ import {
getPreferredPairingOffer,
type KnownRuntimeEnvironment
} from '../../../shared/runtime-environments'
import type { SshTarget } from '../../../shared/ssh-types'
import type { ServeReadiness } from '../../server/serve-readiness'
import type { OrcadHostTransport } from '../../ssh/orcad-host-transport'
import type { OrcadHostTarget, OrcadHostTransport } from '../../ssh/orcad-host-transport'
import { tunneledOrcadPairingCode } from '../../ssh/orcad-tunneled-pairing'
import {
getSshConnectionManager,
@@ -33,16 +32,17 @@ export const PAIRING_REPAIR_WINDOW_MS = 10 * 60_000
// Why remembered briefly: every reconnecting pane would otherwise re-read the host over SSH.
export const PAIRING_REPAIR_FAILURE_HOLD_MS = 15_000
export type ManagedOrcadPairingRepairDeps = {
export type ManagedOrcadHostAccess = { target: OrcadHostTarget; connection: OrcadHostTransport }
/** SSH reaches the host over SSH; WSL through its distro, the trust root of its own pairing. */
export type ManagedOrcadPairingRepairDeps<
Access extends ManagedOrcadHostAccess = ManagedOrcadHostAccess
> = {
resolve: (environmentId: string) => KnownRuntimeEnvironment
connect: (environment: KnownRuntimeEnvironment) => Promise<{
target: SshTarget
connection: OrcadHostTransport
}>
readReadiness: (input: {
target: SshTarget
connection: OrcadHostTransport
}) => Promise<ServeReadiness | null>
connect: (environment: KnownRuntimeEnvironment) => Promise<Access>
readReadiness: (input: Access) => Promise<ServeReadiness | null>
/** The bridge or tunnel ports the pairing names; defaults to the SSH deployment's. */
ports?: (environment: KnownRuntimeEnvironment) => { localPort: number; remotePort: number } | null
refresh: (environmentId: string, pairingCode: string) => KnownRuntimeEnvironment
/** Retires every transport still authenticating with the refused token, once per repair. */
repaired: (environmentId: string) => void | Promise<void>
@@ -95,14 +95,16 @@ type StandingFailure = { error: Error; revision: number; until: number }
* refused pairing. Keyed by the refused revision: a refusal seen under an older pairing than the
* stored one was already answered, so it gets the stored pairing without reaching the host.
*/
export class ManagedOrcadPairingRepairs {
export class ManagedOrcadPairingRepairs<
Access extends ManagedOrcadHostAccess = ManagedOrcadHostAccess
> {
private readonly inFlight = new Map<string, Promise<KnownRuntimeEnvironment>>()
private readonly attemptedAt = new Map<string, number[]>()
private readonly standing = new Map<string, StandingFailure>()
// Why: a repair that was already running when the user asked again must not undo that reset.
private readonly resets = new Map<string, number>()
constructor(private readonly deps: ManagedOrcadPairingRepairDeps) {}
constructor(private readonly deps: ManagedOrcadPairingRepairDeps<Access>) {}
/**
* The failure that stands for this pairing now, or null. A build under it would cost SSH execs,
@@ -166,11 +168,11 @@ export class ManagedOrcadPairingRepairs {
throw fail(capSpentError(recent.length))
}
assertRuntimeEnvironmentNotReconciling(environment)
const deployment = getOrcadSshDeployment(environment)
const deployment = (this.deps.ports ?? getOrcadSshDeployment)(environment)
if (!deployment) {
throw new OrcadManagedPairingRepairFailedError('this server is not managed over SSH')
throw new OrcadManagedPairingRepairFailedError('this server has no managed deployment')
}
// Why counted only once read: losing SSH proves nothing about the pairing.
// Why counted only once read: losing contact with the host proves nothing about the pairing.
const readiness = await this.deps.readReadiness(await this.deps.connect(environment))
// Why re-read before judging the answer: an update or recovery may have stored a newer pairing
// while SSH answered; everything after this is synchronous, so the check holds through the write.
@@ -249,6 +251,12 @@ export class ManagedOrcadPairingRepairs {
}
}
/** What callers of the gate use; independent of how a host is reached. */
export type ManagedOrcadPairingGate = Pick<
ManagedOrcadPairingRepairs,
'standingFailure' | 'repair' | 'recordRefusedAfterRepair' | 'resetEpoch' | 'forgetFailures'
>
function capSpentError(attempts: number): Error {
return new OrcadManagedPairingRepairFailedError(
`it tried ${attempts} times in ${PAIRING_REPAIR_WINDOW_MS / 60_000} minutes ` +
@@ -4,12 +4,12 @@
* foreign server never reaches the repair; it fails the build as a foreign answer instead.
*/
import type { KnownRuntimeEnvironment } from '../../../shared/runtime-environments'
import { pairingRevisionOf, type ManagedOrcadPairingRepairs } from './orcad-managed-pairing-repair'
import { pairingRevisionOf, type ManagedOrcadPairingGate } from './orcad-managed-pairing-repair'
import { OrcadManagedPairingRejectedError } from './orcad-managed-tunnel-identity'
export type ManagedOrcadPairingRecovery = {
resolve: (environmentId: string) => KnownRuntimeEnvironment
repairs: ManagedOrcadPairingRepairs
repairs: ManagedOrcadPairingGate
/** Forwards on the new pairing; the refusal already removed the old forward. */
ensure: (environment: KnownRuntimeEnvironment) => Promise<void>
}
@@ -37,7 +37,7 @@ export async function buildWithPairingRepair(
if (!(error instanceof OrcadManagedPairingRejectedError)) {
throw error
}
console.warn('[ssh] The managed Orca server refused this desktop; pairing again:', error)
console.warn('[managed-orcad] The Orca server refused this desktop; pairing again:', error)
}
// Why the refused revision: a repair another caller already landed is joined, not redone.
const repaired = await recovery.repairs.repair(environmentId, pairingRevisionOf(refused))
+4
View File
@@ -1,4 +1,5 @@
/** The production WSL side of `WslHostService`: one holder, connection manager and supervisor. */
import { createWslOrcadPairingRecovery } from './wsl-orcad-pairing-repair'
import { readConfirmedRunningWslDistros } from '../wsl-background-distro-gate'
import { parsePairingCode } from '../../shared/pairing'
import { pairedRuntimeConnectionFromOffer } from '../../shared/paired-runtime-connection'
@@ -62,8 +63,10 @@ export function createWslHostConnector(options: {
? unguardedHolder.ensureHeld(distro)
: Promise.reject(refuseUnknown(distro))
}
const pairingRecovery = createWslOrcadPairingRecovery(options.userDataPath)
const manager = new WslOrcadConnectionManager({
holder,
pairingRecovery,
getPairingOffer: (environmentId) =>
findWslHostDeployment(options.userDataPath, environmentId)
? resolveEnvironmentPairingOffer(options.userDataPath, environmentId)
@@ -141,6 +144,7 @@ export function createWslHostConnector(options: {
listDistros: () => listWslHostDistros(),
readOrcadPublicKey: (distro) => readWslDistroOrcadPublicKey(transportFor(distro)),
connect: (distro, connectOptions) => liveness.connect(distro, connectOptions),
forgetPairingFailures: (environmentId) => pairingRecovery.repairs.forgetFailures(environmentId),
get: (distro) => manager.get(distro),
isReachable: async (distro, environmentId) =>
options.runtime.isReady?.(environmentId) ||
+3
View File
@@ -48,6 +48,7 @@ function makeConnector(distros: WslHostDistroCandidate[]) {
return opened
}),
get: vi.fn((distro: string) => active.get(distro.toLowerCase()) ?? null),
forgetPairingFailures: vi.fn((_environmentId: string) => {}),
isReachable: vi.fn(async (distro: string) => active.has(distro.toLowerCase())),
disconnect: vi.fn(async (distro: string) => {
active.delete(distro.toLowerCase())
@@ -258,6 +259,8 @@ describe('WslHostService', () => {
expect(connector.connect).toHaveBeenLastCalledWith('Ubuntu', {
intent: 'force'
})
// A user's Connect is asking again, so a standing pairing-repair failure must not answer it.
expect(connector.forgetPairingFailures).toHaveBeenLastCalledWith('wsl:ubuntu')
})
it('reconciles a retained dead connection for user work without booting for passive calls', async () => {
+3
View File
@@ -36,6 +36,8 @@ export type WslHostConnector = {
readOrcadPublicKey(distro: string): Promise<string | null>
/** User-initiated; may boot the distro. An intent retries or forces a skipped update. */
connect(distro: string, options?: { intent?: WslHostConnectIntent }): Promise<WslOrcadConnection>
/** The user asked again: a standing pairing-repair failure or spent cap must not answer it. */
forgetPairingFailures?(environmentId: string): void
get(distro: string): WslOrcadConnection | null
/** Checks the paired server without starting a stopped or unconfirmed distro. */
isReachable(distro: string, environmentId: string): Promise<boolean>
@@ -217,6 +219,7 @@ export class WslHostService {
if (refusal || this.removing.has(environmentId)) {
throw new Error(refusal?.message ?? WSL_HOST_REMOVING_MESSAGE)
}
this.wsl.forgetPairingFailures?.(environmentId)
await this.wsl.connect(record.distro, options)
}
@@ -0,0 +1,19 @@
import type { WslOrcadDeferredUpdate } from './wsl-orcad-deploy'
/** One open bridge to a distro's orcad, as the connection manager publishes it. */
export type WslOrcadConnection = {
distro: string
environmentId: string
/** The loopback port on this machine the pairing offer points at. */
localPort: number
/** orcad's port inside the distro. */
remotePort: number
fullVersion: string
/** orcad's pid inside the distro, from its pid file; null when unreadable. */
pid: number | null
/** orcad's pairing code, with its endpoint rewritten to `ws://127.0.0.1:<localPort>`. */
pairingOffer: string
/** An app update the incumbent `fullVersion` is waiting to take (live terminals). */
deferredUpdate: WslOrcadDeferredUpdate | null
close(): Promise<void>
}
+170
View File
@@ -3,6 +3,15 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest
import { encodePairingOffer, parsePairingCode, PAIRING_OFFER_VERSION } from '../../shared/pairing'
import type { ServeReadiness } from '../server/serve-readiness'
import { OrcadManagedIdentityError } from '../managed-server/lifecycle/orcad-managed-tunnel-identity'
import {
ManagedOrcadPairingRepairs,
type ManagedOrcadPairingRepairDeps
} from '../managed-server/lifecycle/orcad-managed-pairing-repair'
import {
createEnvironmentFromPairingOffer,
getPreferredPairingOffer,
type KnownRuntimeEnvironment
} from '../../shared/runtime-environments'
import { checkOrcadStdioBridge, openOrcadStdioBridgeChannel } from '../ssh/orcad-stdio-bridge'
import { startEchoServer } from '../managed-server/lifecycle/orcad-stdio-bridge-test-channel'
import { fakeWslBridgeChildren, posixOrcadStdioBridge } from './wsl-orcad-bridge-test-child'
@@ -383,3 +392,164 @@ describe('WslOrcadConnectionManager', () => {
expect(wslOrcadHostDistro(connection.environmentId)).toBeNull()
})
})
describe('a WSL host that refuses the enrolled pairing', () => {
function offerCode(deviceToken: string, publicKeyB64 = 'public-key'): string {
return encodePairingOffer({
v: PAIRING_OFFER_VERSION,
endpoint: 'ws://127.0.0.1:41000',
deviceToken,
publicKeyB64
})
}
/** An in-memory pairing store and the managed repair gate over it. */
function repairable(published: ServeReadiness) {
let stored: KnownRuntimeEnvironment = createEnvironmentFromPairingOffer({
id: 'wsl:ubuntu',
name: 'Ubuntu',
now: 1,
offer: parsePairingCode(offerCode('lost-token'))!
})
const refresh = vi.fn((environmentId: string, pairingCode: string) => {
stored = createEnvironmentFromPairingOffer({
id: environmentId,
name: 'Ubuntu',
now: 2,
offer: parsePairingCode(pairingCode)!
})
return stored
})
const deps: ManagedOrcadPairingRepairDeps = {
resolve: () => stored,
connect: async () => {
const target = { kind: 'wsl', id: 'wsl:ubuntu', distro: 'Ubuntu', label: 'Ubuntu' } as const
// Never used: readReadiness answers from the fixture.
const spawn = fakeWslBridgeChildren().spawn
return { target, connection: createWslOrcadHostTransport({ target, spawn }) }
},
readReadiness: async () => published,
ports: () => ({ localPort: 41000, remotePort: 6790 }),
refresh,
repaired: vi.fn(),
now: () => 1_000
}
const recovery = { repairs: new ManagedOrcadPairingRepairs(deps), resolve: deps.resolve }
return { stored: () => stored, refresh, recovery }
}
// The host knows only the token it published after losing its device registry.
const verifyIdentity = async (pairingOffer: string) =>
parsePairingCode(pairingOffer)?.deviceToken === 'fresh-token'
? ({ verdict: 'verified' } as const)
: ({ verdict: 'unpaired', detail: 'access grant refused' } as const)
it('heals on the next connect, and the next update counts terminals with the new pairing', async () => {
const store = repairable(readiness(first.port, 'fresh-token'))
const { manager, deployAndLaunch } = setup({
verifyIdentity,
pairingRecovery: store.recovery,
getPairingOffer: () => getPreferredPairingOffer(store.stored())
})
const connection = await manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })
expect(parsePairingCode(connection.pairingOffer)?.deviceToken).toBe('fresh-token')
expect(store.refresh).toHaveBeenCalledOnce()
await manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })
expect(deployAndLaunch).toHaveBeenLastCalledWith(
'Ubuntu',
expect.objectContaining({
pairedOffer: expect.objectContaining({ deviceToken: 'fresh-token' })
})
)
})
it('retires the bridge when the repaired pairing is refused too, so no failure stores the old token', async () => {
const store = repairable(readiness(first.port, 'fresh-token'))
let hostLostRegistry = false
const onChange = vi.fn()
const { manager } = setup({
// The host accepts the enrolled token until it loses its registry, then accepts nothing.
verifyIdentity: async (pairingOffer: string) =>
!hostLostRegistry && parsePairingCode(pairingOffer)?.deviceToken === 'lost-token'
? ({ verdict: 'verified' } as const)
: ({ verdict: 'unpaired', detail: 'access grant refused' } as const),
pairingRecovery: store.recovery,
getPairingOffer: () => getPreferredPairingOffer(store.stored()),
onChange
})
await manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })
hostLostRegistry = true
await expect(manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })).rejects.toThrow()
expect(manager.get('Ubuntu')).toBeNull()
expect(onChange).toHaveBeenLastCalledWith(
'Ubuntu',
null,
expect.objectContaining({ state: 'failed' })
)
expect(getPreferredPairingOffer(store.stored()).deviceToken).toBe('fresh-token')
})
it('still runs an Update queued behind a reconnect whose pairing failed', async () => {
const store = repairable(readiness(first.port, 'fresh-token'))
let hostLostRegistry = false
const { manager, deployAndLaunch } = setup({
verifyIdentity: async (pairingOffer: string) =>
!hostLostRegistry && parsePairingCode(pairingOffer)?.deviceToken === 'lost-token'
? ({ verdict: 'verified' } as const)
: ({ verdict: 'unpaired', detail: 'access grant refused' } as const),
pairingRecovery: store.recovery,
getPairingOffer: () => getPreferredPairingOffer(store.stored())
})
await manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })
hostLostRegistry = true
const reconnect = manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })
const update = manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu', intent: 'update' })
await reconnect.catch(() => {})
const settled = await update.then(
() => null,
(error: unknown) => error
)
expect(settled).not.toBeInstanceOf(WslOrcadConnectionSupersededError)
expect(deployAndLaunch).toHaveBeenLastCalledWith(
'Ubuntu',
expect.objectContaining({ intent: 'update' })
)
})
it('reuses the bridge of a host still being added, which has no stored pairing to repair', async () => {
const store = repairable(readiness(first.port, 'fresh-token'))
const resolve = vi.fn(() => {
throw new Error('no environment is recorded for this host yet')
})
const { manager } = setup({
pairingRecovery: { repairs: store.recovery.repairs, resolve },
getPairingOffer: () => null
})
const opened = await manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })
await expect(manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })).resolves.toMatchObject({
pairingOffer: opened.pairingOffer
})
expect(resolve).not.toHaveBeenCalled()
})
it('never gives the token to a server under another host key', async () => {
const published = readiness(first.port, 'fresh-token')
if (!published.pairing.available) {
throw new Error('fixture readiness publishes a pairing offer')
}
const store = repairable({
...published,
pairing: { ...published.pairing, url: offerCode('fresh-token', 'another-key') }
})
const { manager } = setup({
verifyIdentity,
pairingRecovery: store.recovery,
getPairingOffer: () => getPreferredPairingOffer(store.stored())
})
await expect(manager.ensure('Ubuntu', { environmentId: 'wsl:ubuntu' })).rejects.toBeInstanceOf(
OrcadManagedIdentityError
)
expect(store.refresh).not.toHaveBeenCalled()
expect(getPreferredPairingOffer(store.stored()).deviceToken).toBe('lost-token')
})
})
+43 -32
View File
@@ -4,6 +4,7 @@
* distro; a relaunch that moved orcad's port moves the bridge with it. In memory only: the
* deployment record (C7) persists what `onChange` reports.
*/
import type { WslOrcadConnection } from './wsl-orcad-connection-types'
import {
WslOrcadConnectionSupersededError,
WslOrcadLaunchDeferredError
@@ -24,35 +25,19 @@ import { wslDistroHolderKey } from './wsl-distro-holder-record'
import {
defaultWslOrcadEnvironmentId,
deployAndLaunchWslOrcad,
type WslOrcadDeferredUpdate,
type WslOrcadDeployOptions,
type WslOrcadLaunch
} from './wsl-orcad-deploy'
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
import { createWslOrcadHostTransport } from './wsl-orcad-host-transport'
import { resolveWslOrcadPairing } from './wsl-orcad-pairing-identity'
import { resolveWslOrcadPairing, type WslOrcadPairingRecovery } from './wsl-orcad-pairing-identity'
import { trackWslOrcadHostEnvironment } from './wsl-orcad-host-environments'
import {
listenWslOrcadStdioBridgeOnPreferredPort,
type WslOrcadStdioBridgeOptions
} from './wsl-orcad-stdio-bridge'
export type WslOrcadConnection = {
distro: string
environmentId: string
/** The loopback port on this machine the pairing offer points at. */
localPort: number
/** orcad's port inside the distro. */
remotePort: number
fullVersion: string
/** orcad's pid inside the distro, from its pid file; null when unreadable. */
pid: number | null
/** orcad's pairing code, with its endpoint rewritten to `ws://127.0.0.1:<localPort>`. */
pairingOffer: string
/** An app update the incumbent `fullVersion` is waiting to take (live terminals). */
deferredUpdate: WslOrcadDeferredUpdate | null
close(): Promise<void>
}
export type { WslOrcadConnection } from './wsl-orcad-connection-types'
export type WslOrcadConnectOptions = {
environmentId?: string
@@ -80,6 +65,8 @@ export type WslOrcadConnectionDependencies = {
options: WslOrcadStdioBridgeOptions
) => Promise<StartedPortForward>
verifyIdentity?: (pairingOffer: string, readiness: ServeReadiness) => Promise<OrcadTunnelIdentity>
/** Re-pairs a refused enrolled token; without it a refusal fails the connect. */
pairingRecovery?: Omit<WslOrcadPairingRecovery, 'environmentId'>
/** A connection (re)opened, its offer changed, or its status moved; null once it closed. */
onChange?: (
distro: string,
@@ -146,6 +133,15 @@ export class WslOrcadConnectionManager {
return tracked
}
/** The persisted enrolled pairing, else the live connection's (a host not yet recorded). */
private enrolledOffer(key: string, environmentId: string): PairingOffer | null {
const current = this.active.get(key)?.connection
return (
this.dependencies.getPairingOffer?.(environmentId) ??
(current?.environmentId === environmentId ? parsePairingCode(current.pairingOffer) : null)
)
}
get(distro: string): WslOrcadConnection | null {
return this.active.get(wslDistroHolderKey(distro))?.connection ?? null
}
@@ -166,10 +162,17 @@ export class WslOrcadConnectionManager {
if (!entry) {
return
}
this.active.delete(key)
await this.drop(key, entry)
this.dependencies.onChange?.(entry.connection.distro, null, { state: 'closed' })
}
/** Retires one bridge; unlike close, a connect queued behind it still runs. */
private async drop(key: string, entry: ActiveWslOrcadConnection): Promise<void> {
if (this.active.get(key) === entry) {
this.active.delete(key)
}
await entry.forward.close()
trackWslOrcadHostEnvironment(entry.connection, null)
this.dependencies.onChange?.(entry.connection.distro, null, { state: 'closed' })
}
async dispose(): Promise<void> {
@@ -201,7 +204,7 @@ export class WslOrcadConnectionManager {
environmentId,
label,
intent: options.intent,
pairedOffer: this.dependencies.getPairingOffer?.(environmentId) ?? null,
pairedOffer: this.enrolledOffer(key, environmentId),
signal: options.signal,
holder: this.dependencies.holder,
createTransport: () => transport
@@ -213,11 +216,14 @@ export class WslOrcadConnectionManager {
throw new WslOrcadConnectionSupersededError(distro)
}
const current = this.active.get(key)
const paired =
this.dependencies.getPairingOffer?.(environmentId) ??
(current?.connection.environmentId === environmentId
? parsePairingCode(current.connection.pairingOffer)
: null)
const persisted = this.dependencies.getPairingOffer?.(environmentId) ?? null
const paired = persisted ?? this.enrolledOffer(key, environmentId)
// Why only a persisted pairing: the gate re-pairs the stored record, which a host still being
// added has not written yet.
const recovery =
persisted && this.dependencies.pairingRecovery
? { ...this.dependencies.pairingRecovery, environmentId }
: undefined
if (
current &&
current.connection.remotePort === launch.port &&
@@ -227,8 +233,14 @@ export class WslOrcadConnectionManager {
launch,
current.connection.localPort,
paired,
this.dependencies.verifyIdentity
)
this.dependencies.verifyIdentity,
recovery
).catch(async (error: unknown) => {
// Why retire: this bridge holds a token the host refused, and a failure published with it
// would store that token over a repair the gate already saved.
await this.drop(key, current)
throw error
})
if (this.generation(key) !== generation) {
throw new WslOrcadConnectionSupersededError(distro)
}
@@ -243,9 +255,7 @@ export class WslOrcadConnectionManager {
// orcad moved ports (or this is the first connect): the old bridge dials a dead port.
const preferredLocalPort = current?.connection.localPort ?? options.localPort ?? 0
if (current) {
this.active.delete(key)
await current.forward.close()
trackWslOrcadHostEnvironment(current.connection, null)
await this.drop(key, current)
this.dependencies.onChange?.(distro, null, status.pending)
}
const forward = await listenWslOrcadStdioBridgeOnPreferredPort(
@@ -266,7 +276,8 @@ export class WslOrcadConnectionManager {
launch,
forward.entry.localPort,
paired,
this.dependencies.verifyIdentity
this.dependencies.verifyIdentity,
recovery
)
if (this.generation(key) !== generation) {
throw new WslOrcadConnectionSupersededError(distro)
+44 -2
View File
@@ -7,14 +7,56 @@ import {
type OrcadTunnelIdentity
} from '../managed-server/lifecycle/orcad-managed-tunnel-identity'
import { tunneledOrcadPairingCode } from '../ssh/orcad-tunneled-pairing'
import { getPreferredPairingOffer } from '../../shared/runtime-environments'
import {
buildWithPairingRepair,
type ManagedOrcadPairingRecovery
} from '../managed-server/lifecycle/orcad-managed-tunnel-pairing-recovery'
import type { WslOrcadLaunch } from './wsl-orcad-deploy'
/** Reconnect with the enrolled grant and host key; startup offers only enroll new clients. */
/** The managed repair gate a refused enrolled token goes through, for one WSL host. */
export type WslOrcadPairingRecovery = Omit<ManagedOrcadPairingRecovery, 'ensure'> & {
environmentId: string
}
/**
* Reconnect with the enrolled grant and host key; startup offers only enroll new clients. A token
* the host refuses re-pairs through the gate once, then must verify under the new pairing.
*/
export async function resolveWslOrcadPairing(
launch: Extract<WslOrcadLaunch, { outcome: 'ready' }>,
localPort: number,
paired: PairingOffer | null,
verifyIdentity = verifyWslOrcadIdentity
verifyIdentity = verifyWslOrcadIdentity,
recovery?: WslOrcadPairingRecovery
): Promise<string> {
const verified = (offer: PairingOffer | null) =>
verifyWslOrcadPairing(launch, localPort, offer, verifyIdentity)
// Why only an enrolled token: a first pairing has nothing to repair.
if (!recovery || !paired) {
return verified(paired)
}
let pairingOffer = ''
await buildWithPairingRepair(
{
...recovery,
ensure: async (environment) => {
pairingOffer = await verified(getPreferredPairingOffer(environment))
}
},
recovery.environmentId,
async () => {
pairingOffer = await verified(paired)
}
)
return pairingOffer
}
async function verifyWslOrcadPairing(
launch: Extract<WslOrcadLaunch, { outcome: 'ready' }>,
localPort: number,
paired: PairingOffer | null,
verifyIdentity: typeof verifyWslOrcadIdentity
): Promise<string> {
const pairingOffer = tunneledOrcadPairingCode(launch.readiness, localPort, paired)
const identity = await verifyIdentity(pairingOffer, launch.readiness)
+80
View File
@@ -0,0 +1,80 @@
/**
* Pairing again with a WSL host whose orcad refused this desktop's enrolled token (a lost device
* registry, a revoked device), through the managed SSH repair gate: one repair at a time, a capped
* number per window, and only an offer under the pinned host key. The distro runs on this machine,
* so its published offer is read from the distro itself, as SSH reads it over SSH.
*/
import { resolveEnvironment } from '../../shared/runtime-environment-store'
import {
findWslHostDeployment,
refreshWslHostPairing
} from '../../shared/runtime-environment-wsl-host-store'
import type { KnownRuntimeEnvironment } from '../../shared/runtime-environments'
import { readActiveManagedOrcadReadiness } from '../managed-server/lifecycle/orcad-managed-bound-port'
import {
ManagedOrcadPairingRepairs,
type ManagedOrcadPairingRepairDeps
} from '../managed-server/lifecycle/orcad-managed-pairing-repair'
import type { WslOrcadHostTransport } from './wsl-orcad-host-transport'
import { OrcadManagedPairingRepairFailedError } from '../managed-server/lifecycle/orcad-managed-pairing-repair-failure'
import { notifyManagedOrcadPairingRepaired } from '../managed-server/lifecycle/orcad-managed-pairing-repaired-listener'
import type { WslOrcadHostTarget } from './wsl-orcad-host-target'
import { createWslOrcadHostTransport } from './wsl-orcad-host-transport'
import type { WslOrcadPairingRecovery } from './wsl-orcad-pairing-identity'
type WslOrcadHostAccess = { target: WslOrcadHostTarget; connection: WslOrcadHostTransport }
export function liveWslOrcadPairingRepairDeps(
userDataPath: string
): ManagedOrcadPairingRepairDeps<WslOrcadHostAccess> {
const record = (environmentId: string) => {
const found = findWslHostDeployment(userDataPath, environmentId)
if (!found) {
throw new OrcadManagedPairingRepairFailedError('this server is not a WSL host')
}
return found
}
return {
resolve: (environmentId) => resolveEnvironment(userDataPath, environmentId),
connect: async (environment: KnownRuntimeEnvironment) => {
const target: WslOrcadHostTarget = {
kind: 'wsl',
id: environment.id,
distro: record(environment.id).distro,
label: environment.name
}
return { target, connection: createWslOrcadHostTransport({ target }) }
},
readReadiness: async (access) => {
try {
return await readActiveManagedOrcadReadiness(access)
} finally {
access.connection.dispose()
}
},
ports: (environment) => {
const found = findWslHostDeployment(userDataPath, environment.id)
return found ? { localPort: found.localPort, remotePort: found.remotePort } : null
},
refresh: (environmentId, pairingCode) => {
const { localPort, remotePort } = record(environmentId)
return refreshWslHostPairing(userDataPath, environmentId, {
pairingCode,
localPort,
remotePort
}).environment
},
repaired: notifyManagedOrcadPairingRepaired,
now: Date.now
}
}
/** One gate per desktop, so the cap and a standing failure hold across every WSL connect. */
export function createWslOrcadPairingRecovery(
userDataPath: string,
deps: ManagedOrcadPairingRepairDeps<WslOrcadHostAccess> = liveWslOrcadPairingRepairDeps(
userDataPath
)
): Omit<WslOrcadPairingRecovery, 'environmentId'> {
return { repairs: new ManagedOrcadPairingRepairs(deps), resolve: deps.resolve }
}