From 61c56bd4a52b2c29c74e0347d3bfd368b99c2a86 Mon Sep 17 00:00:00 2001 From: Wang Date: Sat, 29 Aug 2026 11:02:17 +0800 Subject: [PATCH] feat(runtime-host): discover coordination relays automatically Use a bounded IPFS Amino discovery client to supply public Circuit Relay v2 candidates while the main peer endpoint retains reservation authority. Preserve manual relay precedence and publish only accepted reservations. Generated-by: OpenAI Codex --- .../__tests__/runtime-host-management.test.ts | 17 +- .../runtime-host-ssh-terminal.test.ts | 40 ++ .../src/main/runtime-host-management.ts | 27 +- .../src/main/runtime-host-ssh-terminal.ts | 7 + apps/desktop/src/preload/bridge-contract.d.ts | 2 + apps/desktop/src/preload/preload.ts | 2 + .../locales/settings-projects-copy.ts | 19 +- .../runtime-host-management-dialog.tsx | 58 +- .../renderer/styles/settings/runtime-host.css | 30 + docs/astryx-surface-file-inventory.md | 2 +- docs/runtime-host-remote-access.md | 11 +- docs/runtime-host-remote-access.zh-CN.md | 8 +- native/runtime-host-peer/Cargo.lock | 61 ++ native/runtime-host-peer/Cargo.toml | 2 + native/runtime-host-peer/src/bindings.rs | 14 +- native/runtime-host-peer/src/engine.rs | 652 +++++++++++++++++- .../src/engine/relay_discovery.rs | 504 ++++++++++++++ .../RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv | 5 + .../RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt | 118 +++- .../runtime-host-service-manager.test.ts | 13 + packages/cli/src/cli-core.ts | 12 + packages/cli/src/runtime-host-cli.ts | 34 +- .../runtime-host-peer-management-command.ts | 16 +- .../cli/src/runtime-host-service-command.ts | 1 + ...runtime-host-service-management-command.ts | 2 + .../cli/src/runtime-host-setup-command.ts | 1 + .../__tests__/peer-management-frame.test.ts | 1 + .../src/__tests__/peer-native.test.ts | 2 + .../runtime-host/src/client/peer-client.ts | 8 +- packages/runtime-host/src/operator/index.ts | 1 + .../src/operator/managed-deployment.ts | 1 + .../src/operator/peer-management-frame.ts | 1 + .../src/operator/service-management-frame.ts | 2 + packages/runtime-host/src/peer-mesh/owner.ts | 4 + .../runtime-host/src/server/peer-listener.ts | 1 + .../runtime-host/src/transport/peer-native.ts | 9 + .../generate-runtime-host-peer-notices.mjs | 6 +- 37 files changed, 1635 insertions(+), 59 deletions(-) create mode 100644 native/runtime-host-peer/src/engine/relay_discovery.rs diff --git a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts index e7d234745f..5e3ee971dd 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts @@ -20,7 +20,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { - RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY, + RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY, runtimeHostAccessCredentialFingerprint, type RuntimeHostServiceManagementFrame, } from '@maka/runtime-host/operator'; @@ -937,11 +937,11 @@ test('keeps the SSH profile while adding and removing its managed Direct peer', assert.equal(input.action, 'status'); assert.equal( input.capabilityRequest, - RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY, + RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY, ); return { ...serviceResult('status'), - operatorCapabilities: [RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY], + operatorCapabilities: [RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY], }; }, runAccessManagement: async () => assert.fail('access management is not expected'), @@ -971,9 +971,9 @@ test('keeps the SSH profile while adding and removing its managed Direct peer', const configure = handlers.get('runtime-host-management:configure-direct-peer'); assert.ok(configure); - const enabled = await configure({}, profile.id, true, []); + const enabled = await configure({}, profile.id, true, [], true); assert.equal((enabled as { profilePresent: boolean }).profilePresent, true); - const disabled = await configure({}, profile.id, false, []); + const disabled = await configure({}, profile.id, false, [], true); assert.equal((disabled as { profilePresent: boolean }).profilePresent, false); assert.deepEqual(actions, ['enable', 'disable']); }); @@ -1005,7 +1005,7 @@ test('disables a newly enabled listener when its Desktop profile cannot be commi directPeerClientAvailable: true, runServiceManagement: async () => ({ ...serviceResult('status'), - operatorCapabilities: [RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY], + operatorCapabilities: [RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY], }), runAccessManagement: async () => assert.fail('access management is not expected'), runPeerManagement: async (input) => { @@ -1037,7 +1037,7 @@ test('disables a newly enabled listener when its Desktop profile cannot be commi const configure = handlers.get('runtime-host-management:configure-direct-peer'); assert.ok(configure); await assert.rejects( - configure({}, 'office', true, []) as Promise, + configure({}, 'office', true, [], true) as Promise, failure === 'descriptor' ? /usable direct-peer descriptor/u : /profile store failed/u, ); assert.deepEqual(actions, ['enable', 'disable']); @@ -1077,13 +1077,14 @@ test('does not invoke peer management when the remote operator lacks its capabil state: 'unsupported', routeHints: [], coordinationRelays: [], + automaticRelayDiscovery: false, profilePresent: false, profileEnabled: false, clientAvailable: true, managementAvailable: false, }); await assert.rejects( - configure({}, 'office', true, []) as Promise, + configure({}, 'office', true, [], true) as Promise, /Update this Runtime Host/u, ); }); diff --git a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts index a08f663a00..614c6db12c 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts @@ -27,6 +27,7 @@ import { type RuntimeHostSshProcessFactory } from '@maka/runtime-host/client'; import { encodeRuntimeHostActivationFrame, encodeRuntimeHostAccessManagementFrame, + encodeRuntimeHostPeerManagementFrame, encodeRuntimeHostServiceManagementFrame, encodeRuntimeHostSetupFrame, encodeRuntimeHostPeerMeshManagementFrame, @@ -638,6 +639,45 @@ test('keeps a prepared access credential out of the SSH terminal projection', as await harness.terminal.close(); }); +test('requests relay-discovery status only on the peer-management frame', async () => { + const harness = createHarness('pending'); + const management = harness.terminal.runPeerManagement({ + destination: 'operator@example.com', + operatorPath: '/home/operator/.local/share/maka/operator', + action: 'status', + expectedTarget: { + serviceId: 'b'.repeat(64), + rootPath: '/srv/maka', + rootId: 'a'.repeat(64), + deploymentId: '00000000-0000-4000-8000-000000000001', + }, + }); + await waitFor(() => harness.pty.hasDataListener()); + const command = harness.launchArgs.at(-1)?.at(-1) ?? ''; + assert.match(command, /peer.*status.*--framed.*--relay-discovery-status/u); + + harness.pty.emitData( + encodeRuntimeHostPeerManagementFrame({ + kind: 'result', + action: 'status', + status: { + state: 'enabled', + serviceState: 'running', + peerId: '12D3KooWpeer', + rootId: 'a'.repeat(64), + routeHints: ['/ip4/192.0.2.1/udp/41000/quic-v1'], + coordinationRelays: [], + automaticRelayDiscovery: true, + }, + }), + ); + harness.pty.exit(0); + + const result = await management; + assert.equal(result.kind === 'result' && result.status.automaticRelayDiscovery, true); + await harness.terminal.close(); +}); + test('sends a Mesh invitation only after the authenticated remote operator requests it', async () => { const harness = createHarness('pending'); const invitation = JSON.stringify({ secret: 'one-time-mesh-secret' }); diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts index c1e82c737f..8b0099619e 100644 --- a/apps/desktop/src/main/runtime-host-management.ts +++ b/apps/desktop/src/main/runtime-host-management.ts @@ -20,7 +20,7 @@ import type { IpcMain } from 'electron'; import { RUNTIME_HOST_OPERATOR_ACCESS_MANAGEMENT_CAPABILITY, - RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY, + RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY, isProductReleaseVersion, runtimeHostAccessCredentialFingerprint, type RuntimeHostManagedUpdatePolicy, @@ -317,6 +317,7 @@ export function createDesktopRuntimeHostManagement(input: { ...(status.peerId ? { peerId: status.peerId } : {}), routeHints: status.routeHints, coordinationRelays: status.coordinationRelays, + automaticRelayDiscovery: status.automaticRelayDiscovery ?? false, profilePresent: profile.exists, profileEnabled: profile.enabled, clientAvailable: input.directPeerClientAvailable, @@ -334,7 +335,7 @@ export function createDesktopRuntimeHostManagement(input: { operatorPath: target.managed.control.operatorPath, action: 'status', expectedTarget: target.expectedTarget, - capabilityRequest: RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY, + capabilityRequest: RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY, }); if (capability.kind === 'error') throw new Error(capability.error.message); if (capability.action !== 'status') { @@ -343,7 +344,7 @@ export function createDesktopRuntimeHostManagement(input: { return { ...target, available: capability.operatorCapabilities?.includes( - RUNTIME_HOST_OPERATOR_PEER_MANAGEMENT_CAPABILITY, + RUNTIME_HOST_OPERATOR_PEER_RELAY_DISCOVERY_CAPABILITY, ) === true, }; }; @@ -356,6 +357,7 @@ export function createDesktopRuntimeHostManagement(input: { state: 'unsupported', routeHints: [], coordinationRelays: [], + automaticRelayDiscovery: false, profilePresent: profile.exists, profileEnabled: profile.enabled, clientAvailable: input.directPeerClientAvailable, @@ -390,11 +392,15 @@ export function createDesktopRuntimeHostManagement(input: { profileIdValue: unknown, enabledValue: unknown, coordinationRelaysValue: unknown, + automaticRelayDiscoveryValue: unknown, ): Promise => { if (typeof enabledValue !== 'boolean') { throw new Error('Runtime Host direct-peer state is invalid'); } const coordinationRelays = requireCoordinationRelays(coordinationRelaysValue); + if (typeof automaticRelayDiscoveryValue !== 'boolean') { + throw new Error('Runtime Host relay discovery state is invalid'); + } const { profileId, managed, transport, expectedTarget, available } = await peerManagementTarget(profileIdValue); if (!available) { @@ -410,6 +416,7 @@ export function createDesktopRuntimeHostManagement(input: { operatorPath: managed.control.operatorPath, action: enabledValue ? 'enable' : 'disable', ...(enabledValue ? { coordinationRelays } : {}), + ...(enabledValue ? { automaticRelayDiscovery: automaticRelayDiscoveryValue } : {}), expectedTarget, }); if (response.kind !== 'result') { @@ -829,8 +836,18 @@ export function createDesktopRuntimeHostManagement(input: { getDirectPeer(profileId)); input.ipcMain.handle( channels.configureDirectPeer, - (_event, profileId: unknown, enabled: unknown, coordinationRelays: unknown) => - configureDirectPeer(profileId, enabled, coordinationRelays), + ( + _event, + profileId: unknown, + enabled: unknown, + coordinationRelays: unknown, + automaticRelayDiscovery: unknown, + ) => configureDirectPeer( + profileId, + enabled, + coordinationRelays, + automaticRelayDiscovery, + ), ); return { diff --git a/apps/desktop/src/main/runtime-host-ssh-terminal.ts b/apps/desktop/src/main/runtime-host-ssh-terminal.ts index daa5b28754..ed1c216868 100644 --- a/apps/desktop/src/main/runtime-host-ssh-terminal.ts +++ b/apps/desktop/src/main/runtime-host-ssh-terminal.ts @@ -174,6 +174,7 @@ export interface DesktopRuntimeHostSshPeerManagementInput { readonly operatorPath: string; readonly action: Extract; readonly coordinationRelays?: readonly string[]; + readonly automaticRelayDiscovery?: boolean; readonly expectedTarget: DesktopRuntimeHostSshManagementInput['expectedTarget']; readonly signal?: AbortSignal; } @@ -1343,11 +1344,17 @@ function runtimeHostPeerManagementRemoteCommand( 'peer', input.action, '--framed', + '--relay-discovery-status', ...(input.action === 'enable' && input.coordinationRelays ? input.coordinationRelays.length === 0 ? ['--clear-coordination-relays'] : input.coordinationRelays.flatMap((relay) => ['--coordination-relay', relay]) : []), + ...(input.action === 'enable' && input.automaticRelayDiscovery !== undefined + ? [input.automaticRelayDiscovery + ? '--automatic-relay-discovery' + : '--no-automatic-relay-discovery'] + : []), ...managedServiceTargetArgs(input.expectedTarget), ].map(quotePosix).join(' '); return `exec "\${SHELL:-/bin/sh}" -lic ${quotePosix(`exec ${command}`)}`; diff --git a/apps/desktop/src/preload/bridge-contract.d.ts b/apps/desktop/src/preload/bridge-contract.d.ts index e5f9f1e5f1..85c431907e 100644 --- a/apps/desktop/src/preload/bridge-contract.d.ts +++ b/apps/desktop/src/preload/bridge-contract.d.ts @@ -514,6 +514,7 @@ export interface DesktopRuntimeHostDirectPeerSnapshot { readonly peerId?: string; readonly routeHints: readonly string[]; readonly coordinationRelays: readonly string[]; + readonly automaticRelayDiscovery: boolean; readonly profilePresent: boolean; readonly profileEnabled: boolean; readonly clientAvailable: boolean; @@ -731,6 +732,7 @@ export interface MakaBridge { profileId: string, enabled: boolean, coordinationRelays: readonly string[], + automaticRelayDiscovery: boolean, ): Promise; listCredentials(profileId: string): Promise; rotateCredential(profileId: string): Promise; diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts index 63c7344366..c21d120bff 100644 --- a/apps/desktop/src/preload/preload.ts +++ b/apps/desktop/src/preload/preload.ts @@ -1349,12 +1349,14 @@ const makaBridge = { profileId: string, enabled: boolean, coordinationRelays: readonly string[], + automaticRelayDiscovery: boolean, ) { return ipcRenderer.invoke( 'runtime-host-management:configure-direct-peer', profileId, enabled, coordinationRelays, + automaticRelayDiscovery, ); }, listCredentials(profileId: string): Promise { diff --git a/apps/desktop/src/renderer/locales/settings-projects-copy.ts b/apps/desktop/src/renderer/locales/settings-projects-copy.ts index 84ed7af676..93b2e00435 100644 --- a/apps/desktop/src/renderer/locales/settings-projects-copy.ts +++ b/apps/desktop/src/renderer/locales/settings-projects-copy.ts @@ -134,6 +134,9 @@ export type SettingsProjectsCopy = { directPeerRoutes: string; directPeerCoordinationRelays: string; directPeerCoordinationRelaysPlaceholder: string; + directPeerAdvancedCoordination: string; + directPeerAutomaticRelayDiscovery: string; + directPeerAutomaticRelayDiscoveryHelp: string; directPeerEnable: string; directPeerDisable: string; directPeerAddProfile: string; @@ -298,7 +301,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { useConnectionCode: '使用连接码', configureManually: '手动配置', thisComputerRemoteAccess: '远程访问', - thisComputerRemoteAccessHelp: '让其他 Maka Desktop 通过实验性 Direct peer 连接此 Host', + thisComputerRemoteAccessHelp: '通过实验性端到端直连访问此 Host;可自动发现公共协调节点来辅助打洞', remoteAccessOn: '已开启', remoteAccessOff: '未开启', enableRemoteAccess: '开启', @@ -407,7 +410,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { failed: '启动失败', }, directPeer: 'Direct peer(实验性)', - directPeerDescription: '创建独立的实验性 Direct profile。受限 NAT 或被阻止的 UDP 可能使其不可达,且不会自动回退;保留 SSH profile 用于手动恢复。', + directPeerDescription: '创建独立的实验性 Direct profile。可自动发现或手动指定协调节点来辅助打洞;受限 NAT 或被阻止的 UDP 仍可能使其不可达,且不会回退到中继传输。保留 SSH profile 用于手动恢复。', directPeerState: { unsupported: '需要更新', not_configured: '未配置', @@ -423,6 +426,10 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { directPeerRoutes: '可用路径', directPeerCoordinationRelays: '连接协调节点(可选)', directPeerCoordinationRelaysPlaceholder: '多个地址用逗号分隔', + directPeerAdvancedCoordination: '手动设置协调节点', + directPeerAutomaticRelayDiscovery: '自动发现协调节点', + directPeerAutomaticRelayDiscoveryHelp: + '协调节点使用 Circuit Relay v2 协议,仅帮助建立端到端直连,不承载应用流量。Maka 会通过公共 IPFS 网络尽力发现可用节点;手动设置的节点优先。', directPeerEnable: '启用并添加', directPeerDisable: '停用', directPeerAddProfile: '添加到 Desktop', @@ -587,7 +594,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { useConnectionCode: 'Use connection code', configureManually: 'Configure manually', thisComputerRemoteAccess: 'Remote access', - thisComputerRemoteAccessHelp: 'Let another Maka Desktop reach this Host through experimental Direct peer', + thisComputerRemoteAccessHelp: 'Reach this Host through experimental end-to-end direct connections, with automatic public coordination discovery', remoteAccessOn: 'On', remoteAccessOff: 'Off', enableRemoteAccess: 'Enable', @@ -696,7 +703,7 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { failed: 'Failed', }, directPeer: 'Direct peer (experimental)', - directPeerDescription: 'Create an independent experimental Direct profile. Restrictive NAT or blocked UDP may make it unreachable, and it does not fall back automatically; keep the SSH profile for manual recovery.', + directPeerDescription: 'Create an independent experimental Direct profile. Discover coordination peers automatically or provide them manually to assist hole punching; restrictive NAT or blocked UDP may still make it unreachable, and traffic does not fall back to a relay. Keep the SSH profile for manual recovery.', directPeerState: { unsupported: 'Update required', not_configured: 'Not configured', @@ -712,6 +719,10 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = { directPeerRoutes: 'Routes', directPeerCoordinationRelays: 'Connection coordination peers (optional)', directPeerCoordinationRelaysPlaceholder: 'Separate multiple addresses with commas', + directPeerAdvancedCoordination: 'Set coordination peers manually', + directPeerAutomaticRelayDiscovery: 'Discover coordination peers automatically', + directPeerAutomaticRelayDiscoveryHelp: + 'Coordination peers use Circuit Relay v2 only to establish an end-to-end direct connection; they never carry application traffic. Maka discovers candidates through the public IPFS network on a best-effort basis, while manually configured peers remain preferred.', directPeerEnable: 'Enable and add', directPeerDisable: 'Disable', directPeerAddProfile: 'Add to Desktop', diff --git a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx index 951d391bea..ffa18efe57 100644 --- a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx +++ b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx @@ -21,10 +21,13 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'; import { Layout, LayoutContent, LayoutFooter } from '@astryxdesign/core/Layout'; import { Text } from '@astryxdesign/core/Text'; +import { Switch } from '@astryxdesign/core/Switch'; +import { Tooltip } from '@astryxdesign/core/Tooltip'; import { Badge, Banner, Button, + IconButton, MoreMenu, Selector, Spinner, @@ -32,6 +35,7 @@ import { useToast, useUiLocale, } from '@maka/ui'; +import { HelpCircle, ICON_SIZE } from '@maka/ui/icons'; import { uiLocaleToIntlLocale, type UiLocale } from '@maka/core/ui-locale'; import type { RemoteRuntimeHostProfile } from '@maka/runtime-host/client'; import type { @@ -101,6 +105,7 @@ export function RuntimeHostManagementDialog(props: { const [directPeer, setDirectPeer] = useState(); const [directPeerError, setDirectPeerError] = useState(); const [coordinationRelays, setCoordinationRelays] = useState(''); + const [automaticRelayDiscovery, setAutomaticRelayDiscovery] = useState(true); const nextDirectoryRootId = useRef(1); const logsRef = useRef(null); @@ -213,6 +218,7 @@ export function RuntimeHostManagementDialog(props: { function applyDirectPeer(snapshot: DesktopRuntimeHostDirectPeerSnapshot): void { setDirectPeer(snapshot); setCoordinationRelays(snapshot.coordinationRelays.join(', ')); + setAutomaticRelayDiscovery(snapshot.automaticRelayDiscovery); setDirectPeerError(undefined); } @@ -243,6 +249,7 @@ export function RuntimeHostManagementDialog(props: { profile.id, enabled, relays, + automaticRelayDiscovery, ), ); } catch (failure) { @@ -730,17 +737,46 @@ export function RuntimeHostManagementDialog(props: { /> ) : null} - +
+
+ + {copy.directPeerAutomaticRelayDiscovery} + + + +
+ +
+
+ {copy.directPeerAdvancedCoordination} + +