You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/08/29 03:14:03 UTC
(maka) 01/01: feat(runtime-host): discover coordination relays automatically
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/runtime-host-peer-relay-discovery
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 55aaf3e204101b937f8dc77949e8d2a8465a5ee8
Author: Wang <m4...@gmail.com>
AuthorDate: Sat Aug 29 11:02:17 2026 +0800
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
---
apps/desktop/src/main/runtime-host-management.ts | 21 +-
apps/desktop/src/main/runtime-host-ssh-terminal.ts | 6 +
apps/desktop/src/preload/bridge-contract.d.ts | 2 +
apps/desktop/src/preload/preload.ts | 2 +
.../src/renderer/locales/settings-projects-copy.ts | 6 +
.../settings/runtime-host-management-dialog.tsx | 25 ++
.../src/renderer/styles/settings/runtime-host.css | 13 +
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 | 466 ++++++++++++++++++++-
.../src/engine/relay_discovery.rs | 337 +++++++++++++++
.../cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv | 5 +
.../cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt | 118 +++++-
.../__tests__/runtime-host-service-manager.test.ts | 2 +
packages/cli/src/cli-core.ts | 8 +
packages/cli/src/runtime-host-cli.ts | 24 +-
.../src/runtime-host-peer-management-command.ts | 5 +
packages/cli/src/runtime-host-service-command.ts | 1 +
packages/cli/src/runtime-host-setup-command.ts | 1 +
.../src/__tests__/peer-management-frame.test.ts | 1 +
.../runtime-host/src/__tests__/peer-native.test.ts | 2 +
packages/runtime-host/src/client/peer-client.ts | 8 +-
.../src/operator/managed-deployment.ts | 1 +
.../src/operator/peer-management-frame.ts | 1 +
packages/runtime-host/src/peer-mesh/owner.ts | 4 +
packages/runtime-host/src/server/peer-listener.ts | 1 +
packages/runtime-host/src/transport/peer-native.ts | 9 +
scripts/generate-runtime-host-peer-notices.mjs | 6 +-
31 files changed, 1145 insertions(+), 26 deletions(-)
diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts
index c1e82c737..5d25ff9d2 100644
--- a/apps/desktop/src/main/runtime-host-management.ts
+++ b/apps/desktop/src/main/runtime-host-management.ts
@@ -317,6 +317,7 @@ export function createDesktopRuntimeHostManagement(input: {
...(status.peerId ? { peerId: status.peerId } : {}),
routeHints: status.routeHints,
coordinationRelays: status.coordinationRelays,
+ automaticRelayDiscovery: status.automaticRelayDiscovery ?? true,
profilePresent: profile.exists,
profileEnabled: profile.enabled,
clientAvailable: input.directPeerClientAvailable,
@@ -356,6 +357,7 @@ export function createDesktopRuntimeHostManagement(input: {
state: 'unsupported',
routeHints: [],
coordinationRelays: [],
+ automaticRelayDiscovery: true,
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<DesktopRuntimeHostDirectPeerSnapshot> => {
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 daa5b2875..ab25d36fe 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<RuntimeHostPeerManagementAction, 'enable' | 'disable' | 'status'>;
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
readonly expectedTarget: DesktopRuntimeHostSshManagementInput['expectedTarget'];
readonly signal?: AbortSignal;
}
@@ -1348,6 +1349,11 @@ function runtimeHostPeerManagementRemoteCommand(
? ['--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 e5f9f1e5f..85c431907 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<DesktopRuntimeHostDirectPeerSnapshot>;
listCredentials(profileId: string): Promise<DesktopRuntimeHostAccessSnapshot>;
rotateCredential(profileId: string): Promise<DesktopRuntimeHostAccessSnapshot>;
diff --git a/apps/desktop/src/preload/preload.ts b/apps/desktop/src/preload/preload.ts
index 63c734436..c21d120bf 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<DesktopRuntimeHostAccessSnapshot> {
diff --git a/apps/desktop/src/renderer/locales/settings-projects-copy.ts b/apps/desktop/src/renderer/locales/settings-projects-copy.ts
index 84ed7af67..cc008179f 100644
--- a/apps/desktop/src/renderer/locales/settings-projects-copy.ts
+++ b/apps/desktop/src/renderer/locales/settings-projects-copy.ts
@@ -134,6 +134,8 @@ export type SettingsProjectsCopy = {
directPeerRoutes: string;
directPeerCoordinationRelays: string;
directPeerCoordinationRelaysPlaceholder: string;
+ directPeerAutomaticRelayDiscovery: string;
+ directPeerAutomaticRelayDiscoveryHelp: string;
directPeerEnable: string;
directPeerDisable: string;
directPeerAddProfile: string;
@@ -423,6 +425,8 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = {
directPeerRoutes: '可用路径',
directPeerCoordinationRelays: '连接协调节点(可选)',
directPeerCoordinationRelaysPlaceholder: '多个地址用逗号分隔',
+ directPeerAutomaticRelayDiscovery: '自动发现协调节点',
+ directPeerAutomaticRelayDiscoveryHelp: '通过公共 IPFS 网络尽力发现可用节点;手动填写的节点仍会优先使用。',
directPeerEnable: '启用并添加',
directPeerDisable: '停用',
directPeerAddProfile: '添加到 Desktop',
@@ -712,6 +716,8 @@ const SETTINGS_PROJECTS_COPY_BY_LOCALE = {
directPeerRoutes: 'Routes',
directPeerCoordinationRelays: 'Connection coordination peers (optional)',
directPeerCoordinationRelaysPlaceholder: 'Separate multiple addresses with commas',
+ directPeerAutomaticRelayDiscovery: 'Discover coordination peers automatically',
+ directPeerAutomaticRelayDiscoveryHelp: 'Best-effort discovery through the public IPFS network; 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 951d391be..df7bc303e 100644
--- a/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
+++ b/apps/desktop/src/renderer/settings/runtime-host-management-dialog.tsx
@@ -21,6 +21,7 @@ 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 {
Badge,
Banner,
@@ -101,6 +102,7 @@ export function RuntimeHostManagementDialog(props: {
const [directPeer, setDirectPeer] = useState<DesktopRuntimeHostDirectPeerSnapshot>();
const [directPeerError, setDirectPeerError] = useState<string>();
const [coordinationRelays, setCoordinationRelays] = useState('');
+ const [automaticRelayDiscovery, setAutomaticRelayDiscovery] = useState(true);
const nextDirectoryRootId = useRef(1);
const logsRef = useRef<HTMLPreElement>(null);
@@ -213,6 +215,7 @@ export function RuntimeHostManagementDialog(props: {
function applyDirectPeer(snapshot: DesktopRuntimeHostDirectPeerSnapshot): void {
setDirectPeer(snapshot);
setCoordinationRelays(snapshot.coordinationRelays.join(', '));
+ setAutomaticRelayDiscovery(snapshot.automaticRelayDiscovery);
setDirectPeerError(undefined);
}
@@ -243,6 +246,7 @@ export function RuntimeHostManagementDialog(props: {
profile.id,
enabled,
relays,
+ automaticRelayDiscovery,
),
);
} catch (failure) {
@@ -741,6 +745,27 @@ export function RuntimeHostManagementDialog(props: {
}
onChange={setCoordinationRelays}
/>
+ <div className="settingsRuntimeHostDirectPeerDiscovery">
+ <div>
+ <Text type="body" weight="semibold">
+ {copy.directPeerAutomaticRelayDiscovery}
+ </Text>
+ <Text type="supporting" color="secondary">
+ {copy.directPeerAutomaticRelayDiscoveryHelp}
+ </Text>
+ </div>
+ <Switch
+ label={copy.directPeerAutomaticRelayDiscovery}
+ isLabelHidden
+ value={automaticRelayDiscovery}
+ isDisabled={
+ loading ||
+ directPeer.profileEnabled ||
+ directPeer.state === 'enabled'
+ }
+ onChange={setAutomaticRelayDiscovery}
+ />
+ </div>
<div className="settingsRuntimeHostUpdatePolicyActions">
<Button
variant="secondary"
diff --git a/apps/desktop/src/renderer/styles/settings/runtime-host.css b/apps/desktop/src/renderer/styles/settings/runtime-host.css
index 4425f94df..66794f89d 100644
--- a/apps/desktop/src/renderer/styles/settings/runtime-host.css
+++ b/apps/desktop/src/renderer/styles/settings/runtime-host.css
@@ -177,6 +177,19 @@
justify-content: flex-end;
}
+.settingsRuntimeHostDirectPeerDiscovery {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: var(--space-3);
+}
+
+.settingsRuntimeHostDirectPeerDiscovery > div {
+ display: grid;
+ gap: var(--space-1);
+ min-width: 0;
+}
+
.settingsRuntimeHostManagementLogs {
overflow-wrap: anywhere;
color: var(--foreground-secondary);
diff --git a/docs/runtime-host-remote-access.md b/docs/runtime-host-remote-access.md
index c98fe30ff..a7648a5d8 100644
--- a/docs/runtime-host-remote-access.md
+++ b/docs/runtime-host-remote-access.md
@@ -131,8 +131,15 @@ service uninstall removes its key while retaining the State Root. Pass
`peer enable --clear-coordination-relays` to remove every configured coordination relay.
This direct-only path is experimental and may fail on restrictive NAT or UDP-blocked networks. It
-does not replace an existing TLS, SSH, or overlay-network fallback and does not use a public relay
-unless one is explicitly configured with `peer enable --coordination-relay`.
+does not replace an existing TLS, SSH, or overlay-network fallback. By default, the Host uses a
+bounded client-only view of the public IPFS DHT to discover Circuit Relay v2 candidates and fills a
+target of two accepted reservations after accounting for manual relays. Manually configured relays
+remain preferred. Disable or restore this
+best-effort discovery with `peer enable --no-automatic-relay-discovery` or
+`peer enable --automatic-relay-discovery`; disabling it leaves manual relays intact. Public peers can
+observe the discovery connection and may refuse or drop reservations. Only accepted reservations
+are advertised to Mesh peers, and Maka still requires the application stream to upgrade to a direct
+connection instead of carrying Session traffic through the relay.
### Direct TLS
diff --git a/docs/runtime-host-remote-access.zh-CN.md b/docs/runtime-host-remote-access.zh-CN.md
index a90aa2149..075dfd0bf 100644
--- a/docs/runtime-host-remote-access.zh-CN.md
+++ b/docs/runtime-host-remote-access.zh-CN.md
@@ -117,8 +117,12 @@ PeerId 和 listener 配置;`peer rotate` 会明确更换 PeerId;卸载 servi
Root。执行 `peer enable --clear-coordination-relays` 可以删除所有已配置的 coordination relay。
Direct-only 路径仍是实验能力,在受限 NAT 或禁用 UDP 的网络中可能失败。它不会替代已有的 TLS、SSH
-或 overlay network fallback;除非用户在 `peer enable` 时明确传入 `--coordination-relay`,否则不会使用
-公共 relay。
+或 overlay network fallback。Host 默认通过公共 IPFS DHT 的有界 client-only 视图发现 Circuit Relay v2
+候选;扣除手动 relay 后,自动池会补足两个已接受 reservation 的目标。手动配置的 relay 始终优先。使用
+`peer enable --no-automatic-relay-discovery` 可以关闭这项尽力而为的发现,使用
+`peer enable --automatic-relay-discovery` 可以重新开启;关闭不会删除手动 relay。公网 Peer 能观察到发现
+连接,也可能拒绝或中止 reservation。只有已接受的 reservation 才会向 Mesh Peer 发布,Maka 仍要求
+application stream 升级为直连,不会通过 relay 传输 Session traffic。
### Direct TLS
diff --git a/native/runtime-host-peer/Cargo.lock b/native/runtime-host-peer/Cargo.lock
index 12bcd90ac..ca3fffa36 100644
--- a/native/runtime-host-peer/Cargo.lock
+++ b/native/runtime-host-peer/Cargo.lock
@@ -88,6 +88,12 @@ dependencies = [
"syn 2.0.119",
]
+[[package]]
+name = "asn1_der"
+version = "0.7.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156"
+
[[package]]
name = "async-io"
version = "2.6.0"
@@ -401,6 +407,12 @@ version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
+[[package]]
+name = "crunchy"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -886,6 +898,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
[[package]]
name = "hickory-proto"
version = "0.25.2"
@@ -1299,6 +1317,7 @@ dependencies = [
"libp2p-dns",
"libp2p-identify",
"libp2p-identity",
+ "libp2p-kad",
"libp2p-mdns",
"libp2p-metrics",
"libp2p-noise",
@@ -1427,18 +1446,47 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9525f3831544f7ae497bde79adf114ef127b0fbbb97edbbf692a80408636421c"
dependencies = [
+ "asn1_der",
"bs58",
"ed25519-dalek",
"hkdf",
"multihash",
"prost",
"rand 0.8.8",
+ "ring",
"sha2",
"thiserror 2.0.20",
"tracing",
"zeroize",
]
+[[package]]
+name = "libp2p-kad"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13d3fd632a5872ec804d37e7413ceea20588f69d027a0fa3c46f82574f4dee60"
+dependencies = [
+ "asynchronous-codec",
+ "bytes",
+ "either",
+ "fnv",
+ "futures",
+ "futures-bounded",
+ "futures-timer",
+ "libp2p-core",
+ "libp2p-identity",
+ "libp2p-swarm",
+ "quick-protobuf",
+ "quick-protobuf-codec",
+ "rand 0.8.8",
+ "sha2",
+ "smallvec",
+ "thiserror 2.0.20",
+ "tracing",
+ "uint",
+ "web-time",
+]
+
[[package]]
name = "libp2p-mdns"
version = "0.48.0"
@@ -1469,6 +1517,7 @@ dependencies = [
"libp2p-dcutr",
"libp2p-identify",
"libp2p-identity",
+ "libp2p-kad",
"libp2p-ping",
"libp2p-relay",
"libp2p-swarm",
@@ -2947,6 +2996,18 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+[[package]]
+name = "uint"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f9227a75a5a540a464c832ad4a4195dbdbecd8787610a56262721fde6f04f90"
+dependencies = [
+ "byteorder",
+ "crunchy",
+ "hex",
+ "static_assertions",
+]
+
[[package]]
name = "unicode-ident"
version = "1.0.24"
diff --git a/native/runtime-host-peer/Cargo.toml b/native/runtime-host-peer/Cargo.toml
index 5b086ce9d..01e367934 100644
--- a/native/runtime-host-peer/Cargo.toml
+++ b/native/runtime-host-peer/Cargo.toml
@@ -32,11 +32,13 @@ libp2p = { version = "0.56", features = [
"dcutr",
"dns",
"identify",
+ "kad",
"macros",
"noise",
"ping",
"quic",
"relay",
+ "rsa",
"tcp",
"tokio",
"yamux",
diff --git a/native/runtime-host-peer/src/bindings.rs b/native/runtime-host-peer/src/bindings.rs
index ced2d6847..9379a8b35 100644
--- a/native/runtime-host-peer/src/bindings.rs
+++ b/native/runtime-host-peer/src/bindings.rs
@@ -19,7 +19,7 @@
use std::{
path::PathBuf,
- sync::{Arc, Mutex},
+ sync::{Arc, Mutex, RwLock},
time::Duration,
};
@@ -39,6 +39,7 @@ pub struct StartPeerEndpointOptions {
pub expected_peer_id: Option<String>,
pub listen_addresses: Option<Vec<String>>,
pub coordination_relays: Option<Vec<String>>,
+ pub automatic_relay_discovery: Option<bool>,
}
#[napi(object)]
@@ -60,6 +61,7 @@ pub struct PeerIdentitySignature {
pub struct PeerEndpoint {
peer_id: String,
listen_addresses: Vec<String>,
+ active_coordination_relays: Arc<RwLock<Vec<Multiaddr>>>,
commands: mpsc::Sender<EngineCommand>,
incoming: Arc<AsyncMutex<mpsc::Receiver<engine::PeerStream>>>,
mesh_incoming: Arc<AsyncMutex<mpsc::Receiver<engine::PeerStream>>>,
@@ -79,6 +81,14 @@ impl PeerEndpoint {
self.listen_addresses.clone()
}
+ #[napi(getter)]
+ pub fn active_coordination_relays(&self) -> Vec<String> {
+ self.active_coordination_relays
+ .read()
+ .map(|addresses| addresses.iter().map(ToString::to_string).collect())
+ .unwrap_or_default()
+ }
+
#[napi]
pub async fn connect(&self, options: ConnectPeerOptions) -> Result<PeerStream> {
connect_peer(self, options, engine::StreamKind::Application).await
@@ -283,6 +293,7 @@ pub fn start_peer_endpoint(options: StartPeerEndpointOptions) -> Result<PeerEndp
options.coordination_relays.unwrap_or_default(),
"coordination relay",
)?,
+ automatic_relay_discovery: options.automatic_relay_discovery.unwrap_or(false),
})
.map_err(peer_error)?;
Ok(PeerEndpoint {
@@ -292,6 +303,7 @@ pub fn start_peer_endpoint(options: StartPeerEndpointOptions) -> Result<PeerEndp
.into_iter()
.map(|address| address.to_string())
.collect(),
+ active_coordination_relays: started.active_coordination_relays,
commands: started.commands,
incoming: Arc::new(AsyncMutex::new(started.incoming)),
mesh_incoming: Arc::new(AsyncMutex::new(started.mesh_incoming)),
diff --git a/native/runtime-host-peer/src/engine.rs b/native/runtime-host-peer/src/engine.rs
index 7b33d11a9..bc52a3608 100644
--- a/native/runtime-host-peer/src/engine.rs
+++ b/native/runtime-host-peer/src/engine.rs
@@ -20,6 +20,7 @@
use std::{
collections::{HashMap, HashSet},
path::PathBuf,
+ sync::{Arc, RwLock},
thread,
time::{Duration, Instant},
};
@@ -43,6 +44,7 @@ mod address;
mod application_stream;
mod identity_store;
mod peer_stream;
+mod relay_discovery;
use address::{
address_with_expected_peer, address_with_peer, coordination_relay_peer_id, is_relayed_address,
@@ -64,7 +66,11 @@ const MAX_ESTABLISHED_CONNECTIONS: u32 = 1024;
const MAX_CONNECTIONS_PER_PEER: u32 = 4;
const LISTENER_ADDRESS_QUIET_PERIOD: Duration = Duration::from_millis(250);
const COORDINATION_RETRY_INTERVAL: Duration = Duration::from_secs(1);
+const AUTOMATIC_RELAY_COOLDOWN: Duration = Duration::from_secs(30);
const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
+const TARGET_COORDINATION_RESERVATIONS: usize = 2;
+const MAX_AUTOMATIC_RELAY_CANDIDATES: usize = 8;
+const MAX_PUBLISHED_ADDRESSES_PER_RELAY: usize = 4;
#[derive(Clone)]
pub struct StartOptions {
@@ -72,11 +78,13 @@ pub struct StartOptions {
pub expected_peer_id: Option<PeerId>,
pub listen_addresses: Vec<Multiaddr>,
pub coordination_relays: Vec<Multiaddr>,
+ pub automatic_relay_discovery: bool,
}
pub struct StartedEndpoint {
pub peer_id: PeerId,
pub listen_addresses: Vec<Multiaddr>,
+ pub active_coordination_relays: Arc<RwLock<Vec<Multiaddr>>>,
pub commands: mpsc::Sender<EngineCommand>,
pub incoming: mpsc::Receiver<PeerStream>,
pub mesh_incoming: mpsc::Receiver<PeerStream>,
@@ -178,11 +186,14 @@ struct DirectConnectState {
struct CoordinationRelay {
addresses: Vec<Multiaddr>,
+ reservation_addresses: Vec<Multiaddr>,
connections: HashSet<ConnectionId>,
pending_connection: Option<ConnectionId>,
identify_received: bool,
identify_sent: bool,
reserve: bool,
+ automatic: bool,
+ reservation_accepted: bool,
client_references: usize,
reservation_listener: Option<ListenerId>,
next_connection_attempt: Instant,
@@ -194,11 +205,14 @@ impl Default for CoordinationRelay {
let now = Instant::now();
Self {
addresses: Vec::new(),
+ reservation_addresses: Vec::new(),
connections: HashSet::new(),
pending_connection: None,
identify_received: false,
identify_sent: false,
reserve: false,
+ automatic: false,
+ reservation_accepted: false,
client_references: 0,
reservation_listener: None,
next_connection_attempt: now,
@@ -216,7 +230,14 @@ impl CoordinationRelay {
self.identify_received = false;
self.identify_sent = false;
self.next_connection_attempt = now;
- self.next_reservation_attempt = now + COORDINATION_RETRY_INTERVAL;
+ self.next_reservation_attempt = now
+ + if self.automatic {
+ AUTOMATIC_RELAY_COOLDOWN
+ } else {
+ COORDINATION_RETRY_INTERVAL
+ };
+ self.reservation_accepted = false;
+ self.reservation_addresses.clear();
self.reservation_listener.take()
}
@@ -225,7 +246,14 @@ impl CoordinationRelay {
return false;
}
self.reservation_listener = None;
- self.next_reservation_attempt = now + COORDINATION_RETRY_INTERVAL;
+ self.reservation_accepted = false;
+ self.reservation_addresses.clear();
+ self.next_reservation_attempt = now
+ + if self.automatic {
+ AUTOMATIC_RELAY_COOLDOWN
+ } else {
+ COORDINATION_RETRY_INTERVAL
+ };
true
}
}
@@ -291,6 +319,8 @@ pub fn start(options: StartOptions) -> Result<StartedEndpoint, PeerError> {
let (incoming_tx, incoming_rx) = mpsc::channel(INCOMING_STREAM_CAPACITY);
let (mesh_incoming_tx, mesh_incoming_rx) = mpsc::channel(MESH_INCOMING_STREAM_CAPACITY);
let (terminal_tx, terminal_rx) = mpsc::channel(1);
+ let active_coordination_relays = Arc::new(RwLock::new(Vec::new()));
+ let active_coordination_relays_for_thread = Arc::clone(&active_coordination_relays);
let thread = thread::Builder::new()
.name("maka-runtime-host-peer".to_owned())
.spawn(move || {
@@ -300,6 +330,7 @@ pub fn start(options: StartOptions) -> Result<StartedEndpoint, PeerError> {
incoming_tx,
mesh_incoming_tx,
ready_tx.clone(),
+ active_coordination_relays_for_thread,
);
if let Err(error) = result {
let _ = ready_tx.send(Err(error.clone()));
@@ -313,6 +344,7 @@ pub fn start(options: StartOptions) -> Result<StartedEndpoint, PeerError> {
Ok(StartedEndpoint {
peer_id: ready.0,
listen_addresses: ready.1,
+ active_coordination_relays,
commands: command_tx,
incoming: incoming_rx,
mesh_incoming: mesh_incoming_rx,
@@ -327,6 +359,7 @@ fn run_endpoint(
incoming_tx: mpsc::Sender<PeerStream>,
mesh_incoming_tx: mpsc::Sender<PeerStream>,
ready_tx: std::sync::mpsc::SyncSender<Result<(PeerId, Vec<Multiaddr>), PeerError>>,
+ active_coordination_relays: Arc<RwLock<Vec<Multiaddr>>>,
) -> Result<(), PeerError> {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
@@ -339,6 +372,7 @@ fn run_endpoint(
incoming_tx,
mesh_incoming_tx,
ready_tx,
+ active_coordination_relays,
))
}
@@ -348,6 +382,7 @@ async fn run_endpoint_async(
incoming_tx: mpsc::Sender<PeerStream>,
mesh_incoming_tx: mpsc::Sender<PeerStream>,
ready_tx: std::sync::mpsc::SyncSender<Result<(PeerId, Vec<Multiaddr>), PeerError>>,
+ active_coordination_relays: Arc<RwLock<Vec<Multiaddr>>>,
) -> Result<(), PeerError> {
let key = match options.expected_peer_id {
Some(expected) => {
@@ -388,7 +423,14 @@ async fn run_endpoint_async(
coordination_relay_peer_id(relay)?;
}
for relay in &options.coordination_relays {
- register_coordination_relay(&mut coordination_relays, relay, local_peer_id, true, false)?;
+ register_coordination_relay(
+ &mut coordination_relays,
+ relay,
+ local_peer_id,
+ true,
+ false,
+ false,
+ )?;
}
maintain_coordination_relays(&mut swarm, &mut coordination_relays, false, Instant::now());
@@ -439,6 +481,9 @@ async fn run_endpoint_async(
let mut external_candidate_ready = startup_external_candidate_ready;
let mut deadline_tick = tokio::time::interval(Duration::from_millis(100));
deadline_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
+ let mut discovered_relays = options
+ .automatic_relay_discovery
+ .then(relay_discovery::spawn);
loop {
tokio::select! {
@@ -549,6 +594,34 @@ async fn run_endpoint_async(
// Dropping the stream applies bounded backpressure to Mesh control callers.
}
}
+ Some(candidate) = async {
+ match &mut discovered_relays {
+ Some(receiver) => receiver.recv().await,
+ None => std::future::pending().await,
+ }
+ } => {
+ discovery_debug(format_args!("candidate {}", candidate.peer_id));
+ register_automatic_relay_candidate(
+ &mut coordination_relays,
+ candidate,
+ local_peer_id,
+ );
+ rebalance_automatic_relays(
+ &mut swarm,
+ &mut coordination_relays,
+ Instant::now(),
+ );
+ publish_active_coordination_relays(
+ &coordination_relays,
+ &active_coordination_relays,
+ );
+ maintain_coordination_relays(
+ &mut swarm,
+ &mut coordination_relays,
+ external_candidate_ready,
+ Instant::now(),
+ );
+ }
Some(completed) = stream_completed_rx.recv() => {
match completed.kind {
StreamCompletion::Application(connection_id) => {
@@ -644,6 +717,16 @@ async fn run_endpoint_async(
&mut coordination_relays,
&mut direct,
&mut external_candidate_ready,
+ &active_coordination_relays,
+ );
+ rebalance_automatic_relays(
+ &mut swarm,
+ &mut coordination_relays,
+ Instant::now(),
+ );
+ publish_active_coordination_relays(
+ &coordination_relays,
+ &active_coordination_relays,
);
maintain_coordination_relays(
&mut swarm,
@@ -665,6 +748,11 @@ async fn run_endpoint_async(
}
_ = deadline_tick.tick() => {
let now = Instant::now();
+ rebalance_automatic_relays(&mut swarm, &mut coordination_relays, now);
+ publish_active_coordination_relays(
+ &coordination_relays,
+ &active_coordination_relays,
+ );
maintain_coordination_relays(
&mut swarm,
&mut coordination_relays,
@@ -831,6 +919,7 @@ fn start_connect(
local_peer_id,
false,
referenced.insert(relay_peer),
+ false,
)?;
}
maintain_coordination_relays(swarm, coordination_relays, false, Instant::now());
@@ -883,6 +972,7 @@ fn handle_swarm_event(
coordination_relays: &mut HashMap<PeerId, CoordinationRelay>,
direct: &mut DirectConnectState,
external_candidate_ready: &mut bool,
+ active_coordination_relays: &Arc<RwLock<Vec<Multiaddr>>>,
) {
match event {
SwarmEvent::ConnectionEstablished {
@@ -926,12 +1016,27 @@ fn handle_swarm_event(
if let Some(relay) = coordination_relays.get_mut(&peer_id) {
relay.connections.remove(&connection_id);
}
+ let mut reservation_changed = false;
+ let mut discard_automatic = false;
if !swarm.is_connected(&peer_id)
&& let Some(relay) = coordination_relays.get_mut(&peer_id)
&& relay.is_active()
- && let Some(listener) = relay.connection_lost(Instant::now())
{
- swarm.remove_listener(listener);
+ let was_accepted = relay.reservation_accepted;
+ discovery_debug(format_args!(
+ "connection to {peer_id} closed; reservation accepted={was_accepted}"
+ ));
+ if let Some(listener) = relay.connection_lost(Instant::now()) {
+ swarm.remove_listener(listener);
+ }
+ reservation_changed = true;
+ discard_automatic = relay.automatic && !was_accepted;
+ }
+ if discard_automatic {
+ discard_automatic_relay_candidate(swarm, coordination_relays, peer_id);
+ }
+ if reservation_changed {
+ publish_active_coordination_relays(coordination_relays, active_coordination_relays);
}
}
SwarmEvent::OutgoingConnectionError { connection_id, .. } => {
@@ -939,12 +1044,19 @@ fn handle_swarm_event(
for connect in direct.pending.values_mut() {
connect.dials.remove(&connection_id);
}
- for relay in coordination_relays.values_mut() {
+ let mut failed_automatic = None;
+ for (peer_id, relay) in coordination_relays.iter_mut() {
if relay.pending_connection == Some(connection_id) {
relay.pending_connection = None;
+ if relay.automatic && !relay.reservation_accepted {
+ failed_automatic = Some(*peer_id);
+ }
break;
}
}
+ if let Some(peer_id) = failed_automatic {
+ discard_automatic_relay_candidate(swarm, coordination_relays, peer_id);
+ }
}
SwarmEvent::Behaviour(BehaviourEvent::Dcutr(dcutr::Event {
remote_peer_id,
@@ -958,6 +1070,31 @@ fn handle_swarm_event(
}
}
SwarmEvent::Behaviour(BehaviourEvent::Dcutr(_)) => {}
+ SwarmEvent::Behaviour(BehaviourEvent::RelayClient(
+ relay::client::Event::ReservationReqAccepted { relay_peer_id, .. },
+ )) => {
+ discovery_debug(format_args!("reservation accepted by {relay_peer_id}"));
+ if let Some(relay) = coordination_relays.get_mut(&relay_peer_id) {
+ relay.reservation_accepted = true;
+ }
+ publish_active_coordination_relays(coordination_relays, active_coordination_relays);
+ }
+ SwarmEvent::NewListenAddr {
+ listener_id,
+ address,
+ } if is_relayed_address(&address) => {
+ if let Some(base_address) = reservation_base_address(address) {
+ for relay in coordination_relays.values_mut() {
+ if relay.reservation_listener == Some(listener_id) {
+ if !relay.reservation_addresses.contains(&base_address) {
+ relay.reservation_addresses.push(base_address);
+ }
+ break;
+ }
+ }
+ publish_active_coordination_relays(coordination_relays, active_coordination_relays);
+ }
+ }
SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received {
peer_id,
..
@@ -999,12 +1136,32 @@ fn handle_swarm_event(
);
}
}
- SwarmEvent::ListenerClosed { listener_id, .. } => {
- for relay in coordination_relays.values_mut() {
+ SwarmEvent::ListenerClosed {
+ listener_id,
+ reason,
+ ..
+ } => {
+ discovery_debug(format_args!(
+ "reservation listener {listener_id:?} closed: {reason:?}"
+ ));
+ let mut changed = false;
+ let mut rejected_automatic = None;
+ for (peer_id, relay) in coordination_relays.iter_mut() {
+ let was_accepted = relay.reservation_accepted;
if relay.listener_closed(listener_id, Instant::now()) {
+ if relay.automatic && !was_accepted {
+ rejected_automatic = Some(*peer_id);
+ }
+ changed = true;
break;
}
}
+ if let Some(peer_id) = rejected_automatic {
+ discard_automatic_relay_candidate(swarm, coordination_relays, peer_id);
+ }
+ if changed {
+ publish_active_coordination_relays(coordination_relays, active_coordination_relays);
+ }
}
_ => {}
}
@@ -1023,6 +1180,7 @@ fn handle_startup_event(
coordination_relays,
&mut DirectConnectState::default(),
external_candidate_ready,
+ &Arc::new(RwLock::new(Vec::new())),
);
}
@@ -1032,6 +1190,7 @@ fn register_coordination_relay(
local_peer_id: PeerId,
reserve: bool,
add_client_reference: bool,
+ automatic: bool,
) -> Result<(), PeerError> {
let relay_peer = coordination_relay_peer_id(address)?;
if relay_peer == local_peer_id {
@@ -1041,6 +1200,11 @@ fn register_coordination_relay(
));
}
let relay = relays.entry(relay_peer).or_default();
+ if automatic && !relay.reserve {
+ relay.automatic = true;
+ } else if reserve {
+ relay.automatic = false;
+ }
relay.reserve |= reserve;
if add_client_reference {
relay.client_references += 1;
@@ -1051,6 +1215,204 @@ fn register_coordination_relay(
Ok(())
}
+fn register_automatic_relay_candidate(
+ relays: &mut HashMap<PeerId, CoordinationRelay>,
+ candidate: relay_discovery::RelayCandidate,
+ local_peer_id: PeerId,
+) {
+ if candidate.peer_id == local_peer_id {
+ return;
+ }
+ if !relays.contains_key(&candidate.peer_id)
+ && relays.values().filter(|relay| relay.automatic).count() >= MAX_AUTOMATIC_RELAY_CANDIDATES
+ {
+ return;
+ }
+ for address in candidate.addresses {
+ let _ = register_coordination_relay(relays, &address, local_peer_id, false, false, true);
+ }
+}
+
+fn rebalance_automatic_relays(
+ swarm: &mut Swarm<Behaviour>,
+ relays: &mut HashMap<PeerId, CoordinationRelay>,
+ now: Instant,
+) {
+ let manual_reservations = relays
+ .values()
+ .filter(|relay| !relay.automatic && relay.reservation_accepted)
+ .count();
+ let desired = TARGET_COORDINATION_RESERVATIONS.saturating_sub(manual_reservations);
+ let mut automatic = relays
+ .iter()
+ .filter(|(_, relay)| relay.automatic)
+ .map(|(peer_id, relay)| (*peer_id, relay.reservation_accepted, relay.reserve))
+ .collect::<Vec<_>>();
+ automatic.sort_unstable_by_key(|(peer_id, accepted, reserved)| {
+ (!*accepted, !*reserved, peer_id.to_string())
+ });
+
+ let mut selected = 0;
+ for (peer_id, accepted, _) in automatic {
+ let relay = relays
+ .get_mut(&peer_id)
+ .expect("automatic relay was collected from the same map");
+ let should_reserve = selected < desired
+ && (accepted
+ || relay.reservation_listener.is_some()
+ || relay.next_reservation_attempt <= now);
+ if should_reserve {
+ relay.reserve = true;
+ selected += 1;
+ continue;
+ }
+ relay.reserve = false;
+ relay.reservation_accepted = false;
+ relay.reservation_addresses.clear();
+ if let Some(listener) = relay.reservation_listener.take() {
+ discovery_debug(format_args!(
+ "removing reservation listener for deselected candidate {peer_id}"
+ ));
+ swarm.remove_listener(listener);
+ }
+ if relay.client_references == 0 {
+ for connection_id in relay.connections.drain() {
+ let _ = swarm.close_connection(connection_id);
+ }
+ }
+ }
+}
+
+fn publish_active_coordination_relays(
+ relays: &HashMap<PeerId, CoordinationRelay>,
+ snapshot: &Arc<RwLock<Vec<Multiaddr>>>,
+) {
+ let mut addresses = relays
+ .values()
+ .filter(|relay| relay.reservation_accepted)
+ .flat_map(|relay| published_reservation_addresses(&relay.reservation_addresses))
+ .collect::<Vec<_>>();
+ addresses.sort_unstable_by_key(ToString::to_string);
+ addresses.dedup();
+ if let Ok(mut current) = snapshot.write() {
+ *current = addresses;
+ }
+}
+
+fn published_reservation_addresses(addresses: &[Multiaddr]) -> Vec<Multiaddr> {
+ let mut addresses = addresses.to_vec();
+ addresses.sort_unstable_by_key(|address| (relay_route_class(address), address.to_string()));
+ let mut classes = HashSet::new();
+ addresses
+ .into_iter()
+ .filter(|address| classes.insert(relay_route_class(address)))
+ .take(MAX_PUBLISHED_ADDRESSES_PER_RELAY)
+ .collect()
+}
+
+fn relay_route_class(address: &Multiaddr) -> (u8, u8) {
+ let mut protocols = address.iter();
+ let host = match protocols.next() {
+ Some(Protocol::Ip4(_)) => 0,
+ Some(Protocol::Ip6(_)) => 1,
+ Some(Protocol::Dns(_) | Protocol::Dns4(_) | Protocol::Dns6(_)) => 2,
+ _ => 3,
+ };
+ let transport = match protocols.next() {
+ Some(Protocol::Udp(_)) => 0,
+ Some(Protocol::Tcp(_)) => 1,
+ _ => 2,
+ };
+ (host, transport)
+}
+
+fn reservation_base_address(mut address: Multiaddr) -> Option<Multiaddr> {
+ if !matches!(address.pop(), Some(Protocol::P2p(_)))
+ || !matches!(address.pop(), Some(Protocol::P2pCircuit))
+ || coordination_relay_peer_id(&address).is_err()
+ || !supported_public_relay_address(&address)
+ {
+ return None;
+ }
+ Some(address)
+}
+
+fn supported_public_relay_address(address: &Multiaddr) -> bool {
+ let mut protocols = address.iter();
+ let host_supported = match protocols.next() {
+ Some(Protocol::Ip4(address)) => public_ipv4(address),
+ Some(Protocol::Ip6(address)) => public_ipv6(address),
+ Some(Protocol::Dns(_) | Protocol::Dns4(_) | Protocol::Dns6(_)) => true,
+ _ => false,
+ };
+ if !host_supported {
+ return false;
+ }
+ match protocols.next() {
+ Some(Protocol::Tcp(_)) => {
+ matches!(protocols.next(), Some(Protocol::P2p(_))) && protocols.next().is_none()
+ }
+ Some(Protocol::Udp(_)) => {
+ matches!(protocols.next(), Some(Protocol::QuicV1))
+ && matches!(protocols.next(), Some(Protocol::P2p(_)))
+ && protocols.next().is_none()
+ }
+ _ => false,
+ }
+}
+
+fn public_ipv4(address: std::net::Ipv4Addr) -> bool {
+ let [first, second, third, _] = address.octets();
+ !(first == 0
+ || first == 10
+ || first == 127
+ || first >= 224
+ || (first == 100 && (64..=127).contains(&second))
+ || (first == 169 && second == 254)
+ || (first == 172 && (16..=31).contains(&second))
+ || (first == 192 && second == 0 && third == 0)
+ || (first == 192 && second == 0 && third == 2)
+ || (first == 192 && second == 168)
+ || (first == 198 && (second == 18 || second == 19))
+ || (first == 198 && second == 51 && third == 100)
+ || (first == 203 && second == 0 && third == 113))
+}
+
+fn public_ipv6(address: std::net::Ipv6Addr) -> bool {
+ if let Some(address) = address.to_ipv4_mapped() {
+ return public_ipv4(address);
+ }
+ let segments = address.segments();
+ !address.is_unspecified()
+ && !address.is_loopback()
+ && !address.is_multicast()
+ && segments[0] & 0xfe00 != 0xfc00
+ && segments[0] & 0xffc0 != 0xfe80
+ && !(segments[0] == 0x2001 && segments[1] == 0x0db8)
+}
+
+fn discard_automatic_relay_candidate(
+ swarm: &mut Swarm<Behaviour>,
+ relays: &mut HashMap<PeerId, CoordinationRelay>,
+ peer_id: PeerId,
+) {
+ let Some(relay) = relays.get(&peer_id) else {
+ return;
+ };
+ if !relay.automatic || relay.reservation_accepted || relay.client_references > 0 {
+ return;
+ }
+ let mut relay = relays
+ .remove(&peer_id)
+ .expect("automatic relay candidate was read from the same map");
+ if let Some(listener) = relay.reservation_listener.take() {
+ swarm.remove_listener(listener);
+ }
+ for connection_id in relay.connections.drain() {
+ let _ = swarm.close_connection(connection_id);
+ }
+}
+
fn release_coordination_relays(
swarm: &mut Swarm<Behaviour>,
relays: &mut HashMap<PeerId, CoordinationRelay>,
@@ -1141,13 +1503,27 @@ fn request_coordination_reservation(
}
relay.next_reservation_attempt = now + COORDINATION_RETRY_INTERVAL;
for address in &relay.addresses {
- if let Ok(listener) = swarm.listen_on(address.clone().with(Protocol::P2pCircuit)) {
- relay.reservation_listener = Some(listener);
- break;
+ match swarm.listen_on(address.clone().with(Protocol::P2pCircuit)) {
+ Ok(listener) => {
+ discovery_debug(format_args!("requesting reservation from {peer_id}"));
+ relay.reservation_listener = Some(listener);
+ break;
+ }
+ Err(error) => {
+ discovery_debug(format_args!(
+ "reservation request for {peer_id} failed: {error}"
+ ));
+ }
}
}
}
+fn discovery_debug(message: std::fmt::Arguments<'_>) {
+ if std::env::var_os("MAKA_PEER_DISCOVERY_DEBUG").is_some() {
+ eprintln!("[peer-relay-pool] {message}");
+ }
+}
+
fn retry_connect_routes(
swarm: &mut Swarm<Behaviour>,
direct: &mut DirectConnectState,
@@ -1366,6 +1742,7 @@ mod tests {
.expect("test listen address"),
],
coordination_relays: Vec::new(),
+ automatic_relay_discovery: false,
}
}
@@ -1448,6 +1825,12 @@ mod tests {
let mut relay = CoordinationRelay {
identify_received: true,
identify_sent: true,
+ reservation_accepted: true,
+ reservation_addresses: vec![
+ "/ip4/192.0.2.1/tcp/4001"
+ .parse()
+ .expect("valid reservation address"),
+ ],
reservation_listener: Some(listener),
next_connection_attempt: now + Duration::from_secs(30),
next_reservation_attempt: now + Duration::from_secs(30),
@@ -1456,6 +1839,8 @@ mod tests {
assert!(relay.listener_closed(listener, now));
assert!(relay.reservation_listener.is_none());
+ assert!(!relay.reservation_accepted);
+ assert!(relay.reservation_addresses.is_empty());
relay.reservation_listener = Some(listener);
assert_eq!(relay.connection_lost(now), Some(listener));
@@ -1464,6 +1849,43 @@ mod tests {
assert_eq!(relay.next_connection_attempt, now);
}
+ #[test]
+ fn active_coordination_routes_only_publish_accepted_reservations() {
+ let accepted_peer = PeerId::random();
+ let pending_peer = PeerId::random();
+ let accepted_address: Multiaddr = format!("/ip4/192.0.2.1/tcp/4001/p2p/{accepted_peer}")
+ .parse()
+ .expect("valid accepted relay address");
+ let pending_address: Multiaddr = format!("/ip4/192.0.2.2/tcp/4001/p2p/{pending_peer}")
+ .parse()
+ .expect("valid pending relay address");
+ let relays = HashMap::from([
+ (
+ accepted_peer,
+ CoordinationRelay {
+ reservation_accepted: true,
+ reservation_addresses: vec![accepted_address.clone()],
+ ..CoordinationRelay::default()
+ },
+ ),
+ (
+ pending_peer,
+ CoordinationRelay {
+ reservation_addresses: vec![pending_address],
+ ..CoordinationRelay::default()
+ },
+ ),
+ ]);
+ let snapshot = Arc::new(RwLock::new(Vec::new()));
+
+ publish_active_coordination_relays(&relays, &snapshot);
+
+ assert_eq!(
+ *snapshot.read().expect("read snapshot"),
+ vec![accepted_address]
+ );
+ }
+
#[test]
fn coordination_relay_requires_one_terminal_peer_identity() {
let relay = PeerId::random();
@@ -1481,4 +1903,26 @@ mod tests {
.expect("valid relayed address");
assert!(coordination_relay_peer_id(&tunneled).is_err());
}
+
+ #[test]
+ fn automatic_relay_routes_only_publish_supported_public_transports() {
+ let relay = PeerId::random();
+ let public_quic: Multiaddr = format!("/ip4/1.1.1.1/udp/4001/quic-v1/p2p/{relay}")
+ .parse()
+ .expect("valid public QUIC address");
+ let private_tcp: Multiaddr = format!("/ip4/192.168.1.2/tcp/4001/p2p/{relay}")
+ .parse()
+ .expect("valid private TCP address");
+ let unsupported_websocket: Multiaddr = format!("/ip4/1.1.1.1/tcp/4001/ws/p2p/{relay}")
+ .parse()
+ .expect("valid WebSocket address");
+ let mapped_private: Multiaddr = format!("/ip6/::ffff:127.0.0.1/tcp/4001/p2p/{relay}")
+ .parse()
+ .expect("valid IPv4-mapped private address");
+
+ assert!(supported_public_relay_address(&public_quic));
+ assert!(!supported_public_relay_address(&private_tcp));
+ assert!(!supported_public_relay_address(&unsupported_websocket));
+ assert!(!supported_public_relay_address(&mapped_private));
+ }
}
diff --git a/native/runtime-host-peer/src/engine/relay_discovery.rs b/native/runtime-host-peer/src/engine/relay_discovery.rs
new file mode 100644
index 000000000..bd6d75e2e
--- /dev/null
+++ b/native/runtime-host-peer/src/engine/relay_discovery.rs
@@ -0,0 +1,337 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use std::{
+ collections::{HashMap, HashSet, VecDeque, hash_map::Entry},
+ time::{Duration, Instant},
+};
+
+use futures::StreamExt;
+use libp2p::{
+ Multiaddr, PeerId, StreamProtocol, Swarm, SwarmBuilder, connection_limits, identify, identity,
+ kad::{self, store::MemoryStore},
+ noise, ping,
+ swarm::{NetworkBehaviour, SwarmEvent, dial_opts::DialOpts},
+ tcp, yamux,
+};
+use tokio::sync::mpsc;
+
+use super::{address::address_with_peer, supported_public_relay_address};
+
+const AMINO_PROTOCOL: &str = "/ipfs/kad/1.0.0";
+const IDENTIFY_PROTOCOL: &str = "/maka/runtime-host/relay-discovery/1";
+const RELAY_HOP_PROTOCOL: &str = "/libp2p/circuit/relay/0.2.0/hop";
+const DISCOVERY_INTERVAL: Duration = Duration::from_secs(30);
+const IDLE_CONNECTION_TIMEOUT: Duration = Duration::from_secs(10);
+const MAX_PENDING_PROBES: usize = 8;
+const MAX_KNOWN_PEERS: usize = 256;
+const PROBE_REVISIT_INTERVAL: Duration = Duration::from_secs(10 * 60);
+
+const AMINO_BOOTSTRAPS: &[&str] = &[
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmNnooDu7bfjPFoTZYxMNLWUQJyrVwtbZg5gBMjTezGAJN",
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmQCU2EcMqAqQPR2i9bChDtGNJchTbq5TbXJJ16u19uLTa",
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmbLHAnMoJPWSCR5Zhtx6BHJX9KiKNN6tpvbUcqanj75Nb",
+ "/dnsaddr/bootstrap.libp2p.io/p2p/QmcZf59bWwK5XFi76CZX8cbJ4BhTzzA3gU1ZjYZcYW3dwt",
+ "/dnsaddr/va1.bootstrap.libp2p.io/p2p/12D3KooWKnDdG3iXw9eTFijk3EWSunZcFi54Zka4wmtqtt6rPxc8",
+ "/ip4/104.131.131.82/tcp/4001/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
+ "/ip4/104.131.131.82/udp/4001/quic-v1/p2p/QmaCpDMGvV2BGHeYERUEnRQAwe3N8SzbUtfsmvsqQLuvuJ",
+];
+
+#[derive(Debug)]
+pub(super) struct RelayCandidate {
+ pub peer_id: PeerId,
+ pub addresses: Vec<Multiaddr>,
+}
+
+#[derive(NetworkBehaviour)]
+struct Behaviour {
+ connection_limits: connection_limits::Behaviour,
+ kad: kad::Behaviour<MemoryStore>,
+ identify: identify::Behaviour,
+ ping: ping::Behaviour,
+}
+
+pub(super) fn spawn() -> mpsc::Receiver<RelayCandidate> {
+ let (sender, receiver) = mpsc::channel(32);
+ tokio::spawn(async move {
+ match build_swarm() {
+ Ok(mut swarm) => run(&mut swarm, sender).await,
+ Err(error) => eprintln!("[peer-relay-discovery] unavailable: {error}"),
+ }
+ });
+ receiver
+}
+
+fn build_swarm() -> Result<Swarm<Behaviour>, Box<dyn std::error::Error + Send + Sync>> {
+ let key = identity::Keypair::generate_ed25519();
+ let local_peer_id = key.public().to_peer_id();
+ let swarm = SwarmBuilder::with_existing_identity(key)
+ .with_tokio()
+ .with_tcp(
+ tcp::Config::default().nodelay(true),
+ noise::Config::new,
+ yamux::Config::default,
+ )?
+ .with_quic()
+ .with_dns()?
+ .with_behaviour(move |key| {
+ let mut config = kad::Config::new(StreamProtocol::new(AMINO_PROTOCOL));
+ config.set_query_timeout(Duration::from_secs(15));
+ let mut kad =
+ kad::Behaviour::with_config(local_peer_id, MemoryStore::new(local_peer_id), config);
+ kad.set_mode(Some(kad::Mode::Client));
+ Behaviour {
+ connection_limits: connection_limits::Behaviour::new(
+ connection_limits::ConnectionLimits::default()
+ .with_max_pending_outgoing(Some(MAX_PENDING_PROBES as u32))
+ .with_max_established_outgoing(Some(32))
+ .with_max_established(Some(32))
+ .with_max_established_per_peer(Some(1)),
+ ),
+ kad,
+ identify: identify::Behaviour::new(identify::Config::new(
+ IDENTIFY_PROTOCOL.to_owned(),
+ key.public(),
+ )),
+ ping: ping::Behaviour::new(ping::Config::new()),
+ }
+ })?
+ .with_swarm_config(|config| config.with_idle_connection_timeout(IDLE_CONNECTION_TIMEOUT))
+ .build();
+ Ok(swarm)
+}
+
+async fn run(swarm: &mut Swarm<Behaviour>, sender: mpsc::Sender<RelayCandidate>) {
+ let mut addresses = HashMap::<PeerId, Vec<Multiaddr>>::new();
+ let mut known = HashMap::<PeerId, Instant>::new();
+ let mut pending = HashSet::<PeerId>::new();
+ let mut queue = VecDeque::<PeerId>::new();
+
+ for value in AMINO_BOOTSTRAPS {
+ let Ok(address) = value.parse::<Multiaddr>() else {
+ continue;
+ };
+ let Some(peer_id) = terminal_peer_id(&address) else {
+ continue;
+ };
+ remember(
+ peer_id,
+ vec![address],
+ &mut addresses,
+ &mut known,
+ &mut queue,
+ Instant::now(),
+ );
+ for address in addresses.get(&peer_id).into_iter().flatten() {
+ swarm
+ .behaviour_mut()
+ .kad
+ .add_address(&peer_id, address.clone());
+ }
+ }
+ let _ = swarm.behaviour_mut().kad.bootstrap();
+
+ let mut walk = tokio::time::interval(DISCOVERY_INTERVAL);
+ walk.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
+ loop {
+ start_probes(swarm, &addresses, &mut queue, &mut pending);
+ tokio::select! {
+ _ = walk.tick() => {
+ expire_known_peers(
+ &mut addresses,
+ &mut known,
+ &pending,
+ Instant::now(),
+ );
+ swarm.behaviour_mut().kad.get_closest_peers(PeerId::random().to_bytes());
+ }
+ event = swarm.select_next_some() => match event {
+ SwarmEvent::Behaviour(BehaviourEvent::Kad(kad::Event::OutboundQueryProgressed {
+ result: kad::QueryResult::GetClosestPeers(result),
+ ..
+ })) => {
+ let peers = match result {
+ Ok(result) => result.peers,
+ Err(kad::GetClosestPeersError::Timeout { peers, .. }) => peers,
+ };
+ debug(format_args!("DHT walk returned {} peers", peers.len()));
+ for peer in peers {
+ remember(
+ peer.peer_id,
+ peer.addrs,
+ &mut addresses,
+ &mut known,
+ &mut queue,
+ Instant::now(),
+ );
+ }
+ }
+ SwarmEvent::Behaviour(BehaviourEvent::Kad(kad::Event::RoutingUpdated {
+ peer,
+ addresses: discovered,
+ ..
+ })) => {
+ remember(
+ peer,
+ discovered.into_vec(),
+ &mut addresses,
+ &mut known,
+ &mut queue,
+ Instant::now(),
+ );
+ }
+ SwarmEvent::Behaviour(BehaviourEvent::Identify(identify::Event::Received {
+ peer_id,
+ info,
+ ..
+ })) => {
+ pending.remove(&peer_id);
+ let mut candidate_addresses = info.listen_addrs;
+ if let Some(discovered) = addresses.get(&peer_id) {
+ candidate_addresses.extend(discovered.iter().cloned());
+ }
+ candidate_addresses = canonical_addresses(peer_id, candidate_addresses)
+ .into_iter()
+ .filter(has_literal_ip_host)
+ .collect();
+ debug(format_args!(
+ "identified {peer_id}; relay={} addresses={}",
+ info.protocols.iter().any(|protocol| protocol.as_ref() == RELAY_HOP_PROTOCOL),
+ candidate_addresses.len(),
+ ));
+ if info.protocols.iter().any(|protocol| protocol.as_ref() == RELAY_HOP_PROTOCOL)
+ && !candidate_addresses.is_empty()
+ && sender.send(RelayCandidate {
+ peer_id,
+ addresses: candidate_addresses,
+ }).await.is_err()
+ {
+ return;
+ }
+ }
+ SwarmEvent::OutgoingConnectionError { peer_id: Some(peer_id), error, .. } => {
+ debug(format_args!("dial to {peer_id} failed: {error}"));
+ pending.remove(&peer_id);
+ }
+ SwarmEvent::ConnectionClosed { peer_id, .. } => {
+ pending.remove(&peer_id);
+ }
+ _ => {}
+ }
+ }
+ }
+}
+
+fn debug(message: std::fmt::Arguments<'_>) {
+ if std::env::var_os("MAKA_PEER_DISCOVERY_DEBUG").is_some() {
+ eprintln!("[peer-relay-discovery] {message}");
+ }
+}
+
+fn remember(
+ peer_id: PeerId,
+ discovered: Vec<Multiaddr>,
+ addresses: &mut HashMap<PeerId, Vec<Multiaddr>>,
+ known: &mut HashMap<PeerId, Instant>,
+ queue: &mut VecDeque<PeerId>,
+ now: Instant,
+) {
+ if !known.contains_key(&peer_id) && known.len() >= MAX_KNOWN_PEERS {
+ return;
+ }
+ let discovered = canonical_addresses(peer_id, discovered);
+ if discovered.is_empty() {
+ return;
+ }
+ let entry = addresses.entry(peer_id).or_default();
+ for address in discovered {
+ if !entry.contains(&address) {
+ entry.push(address);
+ }
+ }
+ if let Entry::Vacant(entry) = known.entry(peer_id) {
+ entry.insert(now);
+ queue.push_back(peer_id);
+ }
+}
+
+fn expire_known_peers(
+ addresses: &mut HashMap<PeerId, Vec<Multiaddr>>,
+ known: &mut HashMap<PeerId, Instant>,
+ pending: &HashSet<PeerId>,
+ now: Instant,
+) {
+ known.retain(|peer_id, first_seen| {
+ pending.contains(peer_id) || now.duration_since(*first_seen) < PROBE_REVISIT_INTERVAL
+ });
+ addresses.retain(|peer_id, _| known.contains_key(peer_id));
+}
+
+fn start_probes(
+ swarm: &mut Swarm<Behaviour>,
+ addresses: &HashMap<PeerId, Vec<Multiaddr>>,
+ queue: &mut VecDeque<PeerId>,
+ pending: &mut HashSet<PeerId>,
+) {
+ while pending.len() < MAX_PENDING_PROBES {
+ let Some(peer_id) = queue.pop_front() else {
+ break;
+ };
+ let Some(addresses) = addresses.get(&peer_id) else {
+ continue;
+ };
+ let options = DialOpts::peer_id(peer_id)
+ .addresses(addresses.clone())
+ .build();
+ if swarm.dial(options).is_ok() {
+ pending.insert(peer_id);
+ }
+ }
+}
+
+fn canonical_addresses(peer_id: PeerId, addresses: Vec<Multiaddr>) -> Vec<Multiaddr> {
+ let mut result = Vec::new();
+ for address in addresses {
+ if address
+ .iter()
+ .any(|protocol| matches!(protocol, libp2p::multiaddr::Protocol::P2pCircuit))
+ {
+ continue;
+ }
+ let address = address_with_peer(address, peer_id);
+ if supported_public_relay_address(&address) && !result.contains(&address) {
+ result.push(address);
+ }
+ }
+ result
+}
+
+fn terminal_peer_id(address: &Multiaddr) -> Option<PeerId> {
+ match address.iter().last()? {
+ libp2p::multiaddr::Protocol::P2p(peer_id) => Some(peer_id),
+ _ => None,
+ }
+}
+
+fn has_literal_ip_host(address: &Multiaddr) -> bool {
+ matches!(
+ address.iter().next(),
+ Some(libp2p::multiaddr::Protocol::Ip4(_) | libp2p::multiaddr::Protocol::Ip6(_))
+ )
+}
diff --git a/packages/cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv b/packages/cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv
index b5b200eea..16be61345 100644
--- a/packages/cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv
+++ b/packages/cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv
@@ -4,6 +4,7 @@ arrayref@0.3.9 X
asn1-rs@0.7.2 X X
asn1-rs-derive@0.6.0 X X
asn1-rs-impl@0.2.0 X X
+asn1_der@0.7.7 X X
async-trait@0.1.92 X X
asynchronous-codec@0.7.0 X
autocfg@1.5.1 X X
@@ -30,6 +31,7 @@ critical-section@1.2.0 X X
crossbeam-channel@0.5.16 X X
crossbeam-epoch@0.9.20 X X
crossbeam-utils@0.8.22 X X
+crunchy@0.2.4 X
crypto-common@0.1.7 X X
ctor@1.0.13 X X
curve25519-dalek@4.1.3 X
@@ -71,6 +73,7 @@ getrandom@0.4.3 X X
hashbrown@0.15.5 X X
hashlink@0.10.0 X X
heck@0.5.0 X X
+hex@0.4.3 X X
hickory-proto@0.25.2 X X
hickory-resolver@0.25.2 X X
hkdf@0.12.4 X X
@@ -101,6 +104,7 @@ libp2p-dcutr@0.14.1 X
libp2p-dns@0.44.0 X
libp2p-identify@0.47.0 X
libp2p-identity@0.2.14 X
+libp2p-kad@0.48.0 X
libp2p-noise@0.46.1 X
libp2p-ping@0.47.0 X
libp2p-quic@0.13.1 X
@@ -221,6 +225,7 @@ tracing@0.1.44 X
tracing-attributes@0.1.31 X
tracing-core@0.1.36 X
typenum@1.20.1 X X
+uint@0.10.1 X X
unicode-ident@1.0.24 X X X
unicode-segmentation@1.13.3 X X
unsigned-varint@0.7.2 X
diff --git a/packages/cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt b/packages/cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt
index b6cab8d19..3af1c6add 100644
--- a/packages/cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt
+++ b/packages/cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt
@@ -5,8 +5,8 @@ Generated by scripts/generate-runtime-host-peer-notices.mjs from the exact
four-target production dependency inventory. Do not edit this file by hand.
Manifest: native/runtime-host-peer/Cargo.toml
-Cargo.lock SHA-256: 07524c9f4eac723344e6fd8746a2f7cfed692feaed1785ecbfd45e0ecbe20981
-Inventory SHA-256: eb4b2941cf0ed87153aff3ac5d32697dde3ba105e388ff96b9584c65882834f1
+Cargo.lock SHA-256: f475d62519bef1d83c2e33b17f1b1eeffe7a03370b041dabea887b7f57aa43ff
+Inventory SHA-256: 4078d1a287187fbfe21e4937305dabf6e81afac6f2f45388b47a04f311cd8223
Packages
--------
@@ -44,6 +44,12 @@ SPDX license: MIT/Apache-2.0
Source: https://github.com/rusticata/asn1-rs.git
License text: 59013a5c8d3a19c26a457579105915a5d51bb0c09d579f8cdedf12e4203c3018 (Apache-2.0.txt)
+asn1_der@0.7.7
+--------------
+SPDX license: BSD-2-Clause OR MIT
+Source: https://github.com/KizzyCode/asn1_der-rust
+License text: b70b1cc58b6d09afc7bfba35c45cc8613d121563acc5b9cbc519ae3b9a10af8d (MIT.txt)
+
async-trait@0.1.92
------------------
SPDX license: MIT OR Apache-2.0
@@ -219,6 +225,12 @@ Source: https://github.com/crossbeam-rs/crossbeam
License text: 769f80b5bcb42ed0af4e4d2fd74e1ac9bf843cb80c5a29219d1ef3544428a6bb (LICENSE-APACHE)
License text: 5a7d13c6710cdec29e74eb3172b80712049c34099ab7259661ada051fc90777b (LICENSE-MIT)
+crunchy@0.2.4
+-------------
+SPDX license: MIT
+Source: https://github.com/eira-fransham/crunchy
+License text: e35e8f8a37b546c01c7eb82d11d7554bb6292a29fc6147c21765d1e9c40b2bae (LICENSE)
+
crypto-common@0.1.7
-------------------
SPDX license: MIT OR Apache-2.0
@@ -497,6 +509,13 @@ Source: https://github.com/withoutboats/heck
License text: 769f80b5bcb42ed0af4e4d2fd74e1ac9bf843cb80c5a29219d1ef3544428a6bb (LICENSE-APACHE)
License text: 033a9383ff21d1e4811d98dd2e0081b0ba0d9c9a31d6c7ba9e63e3488479bf29 (LICENSE-MIT)
+hex@0.4.3
+---------
+SPDX license: MIT OR Apache-2.0
+Source: https://github.com/KokaKiwi/rust-hex
+License text: c5accbbd8546e94c34aed24afe689a617627d18eed5a6c48277e48db57c23851 (LICENSE-APACHE)
+License text: 9dde4a6ad496606322ae0b61b70dc7123fa231be543caa0896d567e34d20fbf8 (LICENSE-MIT)
+
hickory-proto@0.25.2
--------------------
SPDX license: MIT OR Apache-2.0
@@ -689,6 +708,12 @@ SPDX license: MIT
Source: https://github.com/libp2p/rust-libp2p
License text: 80f847ab8b3d1802dcf33aad57b13d3e5cbae4969fb5990a5ff24ac7ae98ddb6 (MIT.txt)
+libp2p-kad@0.48.0
+-----------------
+SPDX license: MIT
+Source: https://github.com/libp2p/rust-libp2p
+License text: 4104e268c110e0948f82580e90eeb8a4106c388c4c0f6a1b7ed2e4dd1360872a (MIT.txt)
+
libp2p-noise@0.46.1
-------------------
SPDX license: MIT
@@ -1483,6 +1508,12 @@ License text: db11fec9946737df39ca3898d9cd8c10ec6f6c3a884a6802b0ad0b81b4e8f23a (
License text: 516b24e051bf5630880ebbd55c40a25ce9552ebaf8970a53e8976eb70e522406 (LICENSE-APACHE)
License text: 87ebb37988efc3285a41635c6a9fd38147e1e581b5f0556a284d5ab8fa5cdd4c (LICENSE-MIT)
+uint@0.10.1
+-----------
+SPDX license: MIT OR Apache-2.0
+Source: https://github.com/paritytech/parity-common
+License text: 59013a5c8d3a19c26a457579105915a5d51bb0c09d579f8cdedf12e4203c3018 (Apache-2.0.txt)
+
unicode-ident@1.0.24
--------------------
SPDX license: (MIT OR Apache-2.0) AND Unicode-3.0
@@ -3831,6 +3862,7 @@ Used by:
- libp2p-core@0.43.2 (MIT.txt)
- libp2p-dns@0.44.0 (MIT.txt)
- libp2p-identify@0.47.0 (MIT.txt)
+- libp2p-kad@0.48.0 (MIT.txt)
- libp2p-noise@0.46.1 (MIT.txt)
- libp2p-ping@0.47.0 (MIT.txt)
- libp2p-quic@0.13.1 (MIT.txt)
@@ -5215,6 +5247,7 @@ Used by:
- if-watch@3.2.2 (Apache-2.0.txt)
- sha2@0.10.9 (LICENSE-APACHE)
- signature@2.2.0 (LICENSE-APACHE)
+- uint@0.10.1 (Apache-2.0.txt)
- yamux@0.12.1 (Apache-2.0.txt)
- yamux@0.13.10 (Apache-2.0.txt)
- yasna@0.5.2 (Apache-2.0.txt)
@@ -9450,6 +9483,32 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+9dde4a6ad496606322ae0b61b70dc7123fa231be543caa0896d567e34d20fbf8
+----------------------------------------------------------------
+Used by:
+- hex@0.4.3 (LICENSE-MIT)
+
+Copyright (c) 2013-2014 The Rust Project Developers.
+Copyright (c) 2015-2020 The rust-hex Developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
9e0027807b0c548ac1df1bd85fe0d72da6665498b06ebf8a64d5cb841f39fcef
----------------------------------------------------------------
Used by:
@@ -9998,6 +10057,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+b70b1cc58b6d09afc7bfba35c45cc8613d121563acc5b9cbc519ae3b9a10af8d
+----------------------------------------------------------------
+Used by:
+- asn1_der@0.7.7 (MIT.txt)
+
+Copyright notice: not included in the published crate
+Cargo authors: KizzyCode Software Labs./Keziah Biermann <de...@kizzycode.de>
+Source: https://github.com/KizzyCode/asn1_der-rust
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
b8777f2b9ad6f8ef48e077876144281fb9683f77b5944b40d720706e01a18d80
----------------------------------------------------------------
Used by:
@@ -10405,6 +10491,7 @@ c5accbbd8546e94c34aed24afe689a617627d18eed5a6c48277e48db57c23851
----------------------------------------------------------------
Used by:
- enum-as-inner@0.6.1 (LICENSE-APACHE)
+- hex@0.4.3 (LICENSE-APACHE)
- resolv-conf@0.7.6 (LICENSE-APACHE)
Apache License
@@ -12319,6 +12406,33 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+e35e8f8a37b546c01c7eb82d11d7554bb6292a29fc6147c21765d1e9c40b2bae
+----------------------------------------------------------------
+Used by:
+- crunchy@0.2.4 (LICENSE)
+
+The MIT License (MIT)
+
+Copyright 2017-2023 Eira Fransham.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
e915669a595b11a200873df8286561881b0e04932f6412a585db6297ba0bc97c
----------------------------------------------------------------
Used by:
diff --git a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
index 2db5fee81..f284ffafc 100644
--- a/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
+++ b/packages/cli/src/__tests__/runtime-host-service-manager.test.ts
@@ -160,6 +160,7 @@ describe('managed Runtime Host service', () => {
'--listen',
'/ip4/0.0.0.0/udp/44001/quic-v1',
'--clear-coordination-relays',
+ '--no-automatic-relay-discovery',
'--allow-interrupt-active-tasks',
'--expected-service-id',
'b'.repeat(64),
@@ -179,6 +180,7 @@ describe('managed Runtime Host service', () => {
framed: true,
listenAddresses: ['/ip4/0.0.0.0/udp/44001/quic-v1'],
coordinationRelays: [],
+ automaticRelayDiscovery: false,
allowInterruptActiveTasks: true,
managedRootId: 'a'.repeat(64),
operatorDeploymentId: '00000000-0000-4000-8000-000000000001',
diff --git a/packages/cli/src/cli-core.ts b/packages/cli/src/cli-core.ts
index f2d9e286e..aec8eb924 100644
--- a/packages/cli/src/cli-core.ts
+++ b/packages/cli/src/cli-core.ts
@@ -213,6 +213,13 @@ function helpText(cliCommand: string): string {
' --websocket-path <path> Persist the upgrade path (default: /runtime-host)',
' --json Emit framed machine-readable progress and result records',
'',
+ 'Managed Runtime Host direct-peer options:',
+ ' --listen <multiaddr> Persist a listener address (repeatable)',
+ ' --coordination-relay <addr> Prefer a Circuit Relay v2 address (repeatable)',
+ ' --clear-coordination-relays Remove every manually configured relay',
+ ' --automatic-relay-discovery Enable best-effort public relay discovery',
+ ' --no-automatic-relay-discovery Disable public discovery and retain manual relays',
+ '',
'Runtime Host access issue options:',
' --root <path> Select the canonical data root',
' --kind <kind> remote-owner or capability-provider',
@@ -317,6 +324,7 @@ export async function runMakaCli(
expectedPeerId: peer.peerId,
listenAddresses: peer.listenAddresses,
coordinationRelays: peer.coordinationRelays,
+ automaticRelayDiscovery: peer.automaticRelayDiscovery,
meshDataRoot: join(config.deploymentRoot, 'peer-mesh'),
},
}
diff --git a/packages/cli/src/runtime-host-cli.ts b/packages/cli/src/runtime-host-cli.ts
index ca9ea69d1..3898aace5 100644
--- a/packages/cli/src/runtime-host-cli.ts
+++ b/packages/cli/src/runtime-host-cli.ts
@@ -165,6 +165,7 @@ export type RuntimeHostCliCommand =
operatorDeploymentId: string;
listenAddresses: string[];
coordinationRelays?: string[];
+ automaticRelayDiscovery?: boolean;
expectedTarget?: RuntimeHostManagedServiceTarget;
allowInterruptActiveTasks?: true;
}
@@ -892,6 +893,7 @@ function parseServicePeerCommand(argv: string[]): RuntimeHostCliCommand {
const listenAddresses: string[] = [];
const coordinationRelays: string[] = [];
let clearCoordinationRelays = false;
+ let automaticRelayDiscovery: boolean | undefined;
let allowInterruptActiveTasks = false;
const options = parseManagedServiceOptions(argv.slice(1), {
allowConfiguration: false,
@@ -905,6 +907,18 @@ function parseServicePeerCommand(argv: string[]): RuntimeHostCliCommand {
if (allowInterruptActiveTasks) return error('Duplicate --allow-interrupt-active-tasks');
allowInterruptActiveTasks = true;
},
+ '--automatic-relay-discovery': () => {
+ if (automaticRelayDiscovery !== undefined) {
+ return error('Relay discovery mode was specified more than once');
+ }
+ automaticRelayDiscovery = true;
+ },
+ '--no-automatic-relay-discovery': () => {
+ if (automaticRelayDiscovery !== undefined) {
+ return error('Relay discovery mode was specified more than once');
+ }
+ automaticRelayDiscovery = false;
+ },
},
valueOptions: {
'--client-data-root': (value) => {
@@ -932,11 +946,12 @@ function parseServicePeerCommand(argv: string[]): RuntimeHostCliCommand {
}
if (
action !== 'enable' &&
- (listenAddresses.length > 0 || coordinationRelays.length > 0 || clearCoordinationRelays)
+ (listenAddresses.length > 0 ||
+ coordinationRelays.length > 0 ||
+ clearCoordinationRelays ||
+ automaticRelayDiscovery !== undefined)
) {
- return error(
- '--listen, --coordination-relay, and --clear-coordination-relays are only valid with peer enable',
- );
+ return error('Peer listener options are only valid with peer enable');
}
if (allowInterruptActiveTasks && action !== 'enable' && action !== 'disable') {
return error('--allow-interrupt-active-tasks is only valid with peer enable or peer disable');
@@ -969,6 +984,7 @@ function parseServicePeerCommand(argv: string[]): RuntimeHostCliCommand {
: coordinationRelays.length > 0
? { coordinationRelays }
: {}),
+ ...(automaticRelayDiscovery === undefined ? {} : { automaticRelayDiscovery }),
...(options.expectedTarget ? { expectedTarget: options.expectedTarget } : {}),
...(allowInterruptActiveTasks ? { allowInterruptActiveTasks: true } : {}),
};
diff --git a/packages/cli/src/runtime-host-peer-management-command.ts b/packages/cli/src/runtime-host-peer-management-command.ts
index d80b0b748..6618eedc0 100644
--- a/packages/cli/src/runtime-host-peer-management-command.ts
+++ b/packages/cli/src/runtime-host-peer-management-command.ts
@@ -73,6 +73,7 @@ export interface RuntimeHostPeerManagementCliOptions {
readonly operatorDeploymentId: string;
readonly listenAddresses: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
readonly expectedTarget?: RuntimeHostManagedServiceTarget;
readonly allowInterruptActiveTasks?: boolean;
}
@@ -312,6 +313,8 @@ async function prepareCanonicalPeer(
coordinationRelays: [
...new Set(options.coordinationRelays ?? current?.coordinationRelays ?? []),
],
+ automaticRelayDiscovery:
+ options.automaticRelayDiscovery ?? current?.automaticRelayDiscovery ?? true,
};
}
@@ -340,6 +343,7 @@ async function readCanonicalPeerStatus(
serviceState: result.service.state,
routeHints: [],
coordinationRelays: [],
+ automaticRelayDiscovery: true,
};
}
return {
@@ -349,6 +353,7 @@ async function readCanonicalPeerStatus(
rootId: config.root.id,
routeHints: expandWildcardListenAddresses(peer.listenAddresses),
coordinationRelays: [...peer.coordinationRelays],
+ automaticRelayDiscovery: peer.automaticRelayDiscovery,
};
}
diff --git a/packages/cli/src/runtime-host-service-command.ts b/packages/cli/src/runtime-host-service-command.ts
index a84e335b8..53059736a 100644
--- a/packages/cli/src/runtime-host-service-command.ts
+++ b/packages/cli/src/runtime-host-service-command.ts
@@ -49,6 +49,7 @@ export interface RuntimeHostServiceCliOptions {
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
readonly meshDataRoot?: string;
};
}
diff --git a/packages/cli/src/runtime-host-setup-command.ts b/packages/cli/src/runtime-host-setup-command.ts
index 55c912172..275213356 100644
--- a/packages/cli/src/runtime-host-setup-command.ts
+++ b/packages/cli/src/runtime-host-setup-command.ts
@@ -651,6 +651,7 @@ async function prepareSupervisedDirectPeer(
coordinationRelays: [
...(options.directPeer?.coordinationRelays ?? current?.coordinationRelays ?? []),
],
+ automaticRelayDiscovery: current?.automaticRelayDiscovery ?? true,
};
}
diff --git a/packages/runtime-host/src/__tests__/peer-management-frame.test.ts b/packages/runtime-host/src/__tests__/peer-management-frame.test.ts
index 2bd158ffb..40ea81a05 100644
--- a/packages/runtime-host/src/__tests__/peer-management-frame.test.ts
+++ b/packages/runtime-host/src/__tests__/peer-management-frame.test.ts
@@ -36,6 +36,7 @@ test('peer management frames preserve a bounded machine-readable descriptor', ()
rootId: 'a'.repeat(64),
routeHints: ['/ip4/192.0.2.1/udp/41000/quic-v1'],
coordinationRelays: [],
+ automaticRelayDiscovery: true,
},
};
assert.deepEqual(
diff --git a/packages/runtime-host/src/__tests__/peer-native.test.ts b/packages/runtime-host/src/__tests__/peer-native.test.ts
index 1568f677b..7dad8c138 100644
--- a/packages/runtime-host/src/__tests__/peer-native.test.ts
+++ b/packages/runtime-host/src/__tests__/peer-native.test.ts
@@ -60,6 +60,7 @@ module.exports = {
return {
peerId: 'client',
listenAddresses: [],
+ activeCoordinationRelays: [],
connect: ({ requestId, peerId, routeHints, coordinationRelays }) => {
stats.requests.push({ requestId, peerId, routeHints, coordinationRelays });
if (peerId === 'ready') return Promise.resolve(stream);
@@ -198,6 +199,7 @@ module.exports = {
startPeerEndpoint: () => ({
peerId: 'peer',
listenAddresses: [],
+ activeCoordinationRelays: [],
connect: async () => stream,
connectMeshControl: async () => stream,
cancelConnect: async () => true,
diff --git a/packages/runtime-host/src/client/peer-client.ts b/packages/runtime-host/src/client/peer-client.ts
index 6464bfdfb..59020d745 100644
--- a/packages/runtime-host/src/client/peer-client.ts
+++ b/packages/runtime-host/src/client/peer-client.ts
@@ -76,6 +76,7 @@ export function createRuntimeHostPeerClientFromEnvironment(
options: {
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
readonly routeResolver?: RuntimeHostPeerRouteResolver;
} = {},
): RuntimeHostPeerClient {
@@ -96,6 +97,7 @@ export function createRuntimeHostPeerClient(input: {
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
readonly routeResolver?: RuntimeHostPeerRouteResolver;
}): RuntimeHostPeerClient {
return new RuntimeHostPeerClientImpl(input);
@@ -107,6 +109,7 @@ class RuntimeHostPeerClientImpl implements RuntimeHostPeerClient {
readonly #expectedPeerId: string | undefined;
readonly #listenAddresses: readonly string[] | undefined;
readonly #coordinationRelays: readonly string[] | undefined;
+ readonly #automaticRelayDiscovery: boolean;
readonly #routeResolver: RuntimeHostPeerRouteResolver | undefined;
#endpoint: RuntimeHostPeerNativeEndpoint | undefined;
#draining: Promise<void> | undefined;
@@ -125,6 +128,7 @@ class RuntimeHostPeerClientImpl implements RuntimeHostPeerClient {
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
readonly routeResolver?: RuntimeHostPeerRouteResolver;
}) {
this.#nativePath = input.nativePath;
@@ -132,6 +136,7 @@ class RuntimeHostPeerClientImpl implements RuntimeHostPeerClient {
this.#expectedPeerId = input.expectedPeerId;
this.#listenAddresses = input.listenAddresses;
this.#coordinationRelays = input.coordinationRelays;
+ this.#automaticRelayDiscovery = input.automaticRelayDiscovery ?? false;
this.#routeResolver = input.routeResolver;
}
@@ -144,7 +149,7 @@ class RuntimeHostPeerClientImpl implements RuntimeHostPeerClient {
return Object.freeze({
peerId: endpoint.peerId,
listenAddresses: Object.freeze([...endpoint.listenAddresses]),
- coordinationRelays: Object.freeze([...(this.#coordinationRelays ?? [])]),
+ coordinationRelays: Object.freeze([...endpoint.activeCoordinationRelays]),
});
}
@@ -328,6 +333,7 @@ class RuntimeHostPeerClientImpl implements RuntimeHostPeerClient {
...(this.#expectedPeerId ? { expectedPeerId: this.#expectedPeerId } : {}),
...(this.#listenAddresses ? { listenAddresses: this.#listenAddresses } : {}),
...(this.#coordinationRelays ? { coordinationRelays: this.#coordinationRelays } : {}),
+ automaticRelayDiscovery: this.#automaticRelayDiscovery,
});
this.#endpoint = endpoint;
this.#draining = this.#drainInbound(endpoint);
diff --git a/packages/runtime-host/src/operator/managed-deployment.ts b/packages/runtime-host/src/operator/managed-deployment.ts
index 9aaa6d297..69de37ee7 100644
--- a/packages/runtime-host/src/operator/managed-deployment.ts
+++ b/packages/runtime-host/src/operator/managed-deployment.ts
@@ -176,6 +176,7 @@ const managedDeploymentConfigSchema = z
peerId: boundedText(256),
listenAddresses: z.array(boundedText(2_048)).min(1).max(16),
coordinationRelays: z.array(boundedText(2_048)).max(16),
+ automaticRelayDiscovery: z.boolean().default(true),
})
.strict()
.optional(),
diff --git a/packages/runtime-host/src/operator/peer-management-frame.ts b/packages/runtime-host/src/operator/peer-management-frame.ts
index d216179ec..6c53370da 100644
--- a/packages/runtime-host/src/operator/peer-management-frame.ts
+++ b/packages/runtime-host/src/operator/peer-management-frame.ts
@@ -37,6 +37,7 @@ const STATUS_SCHEMA = z
.optional(),
routeHints: z.array(ADDRESS_SCHEMA).max(16),
coordinationRelays: z.array(ADDRESS_SCHEMA).max(16),
+ automaticRelayDiscovery: z.boolean().optional(),
})
.strict();
diff --git a/packages/runtime-host/src/peer-mesh/owner.ts b/packages/runtime-host/src/peer-mesh/owner.ts
index 8c62ce623..3f1560154 100644
--- a/packages/runtime-host/src/peer-mesh/owner.ts
+++ b/packages/runtime-host/src/peer-mesh/owner.ts
@@ -41,6 +41,7 @@ export async function openRuntimeHostPeerMeshOwner(input: {
readonly dataRoot: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
}): Promise<RuntimeHostPeerMeshOwner> {
let mesh: PeerMeshNode | undefined;
let resolverMesh: PeerMeshNode | undefined;
@@ -55,6 +56,9 @@ export async function openRuntimeHostPeerMeshOwner(input: {
...(input.expectedPeerId ? { expectedPeerId: input.expectedPeerId } : {}),
...(input.listenAddresses ? { listenAddresses: input.listenAddresses } : {}),
...(input.coordinationRelays ? { coordinationRelays: input.coordinationRelays } : {}),
+ ...(input.automaticRelayDiscovery === undefined
+ ? {}
+ : { automaticRelayDiscovery: input.automaticRelayDiscovery }),
routeResolver: {
resolveRoutes: (peerId) => resolverMesh?.resolveRoutes(peerId),
},
diff --git a/packages/runtime-host/src/server/peer-listener.ts b/packages/runtime-host/src/server/peer-listener.ts
index 5e538bbab..11db805ab 100644
--- a/packages/runtime-host/src/server/peer-listener.ts
+++ b/packages/runtime-host/src/server/peer-listener.ts
@@ -42,6 +42,7 @@ export interface RuntimeHostPeerListenerConfiguration {
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
}
export type RuntimeHostPeerListenerEndpointOptions =
diff --git a/packages/runtime-host/src/transport/peer-native.ts b/packages/runtime-host/src/transport/peer-native.ts
index 0b1106264..c9f362dc9 100644
--- a/packages/runtime-host/src/transport/peer-native.ts
+++ b/packages/runtime-host/src/transport/peer-native.ts
@@ -62,6 +62,7 @@ export interface RuntimeHostPeerIdentityProof {
export interface RuntimeHostPeerNativeEndpoint {
readonly peerId: string;
readonly listenAddresses: readonly string[];
+ readonly activeCoordinationRelays: readonly string[];
connect(options: {
readonly requestId: number;
readonly peerId: string;
@@ -100,6 +101,7 @@ interface RuntimeHostPeerNativeModule {
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
}): unknown;
}
@@ -170,6 +172,7 @@ export function startRuntimeHostPeerEndpoint(input: {
readonly expectedPeerId?: string;
readonly listenAddresses?: readonly string[];
readonly coordinationRelays?: readonly string[];
+ readonly automaticRelayDiscovery?: boolean;
}): RuntimeHostPeerNativeEndpoint {
try {
const endpoint = loadNativeModule(input.nativePath).startPeerEndpoint({
@@ -177,6 +180,9 @@ export function startRuntimeHostPeerEndpoint(input: {
...(input.expectedPeerId ? { expectedPeerId: input.expectedPeerId } : {}),
...(input.listenAddresses ? { listenAddresses: input.listenAddresses } : {}),
...(input.coordinationRelays ? { coordinationRelays: input.coordinationRelays } : {}),
+ ...(input.automaticRelayDiscovery === undefined
+ ? {}
+ : { automaticRelayDiscovery: input.automaticRelayDiscovery }),
});
if (!isPeerNativeEndpoint(endpoint)) {
throw new RuntimeHostPeerError(
@@ -442,6 +448,9 @@ function isPeerNativeEndpoint(value: unknown): value is RuntimeHostPeerNativeEnd
'listenAddresses' in value &&
Array.isArray(value.listenAddresses) &&
value.listenAddresses.every((address) => typeof address === 'string') &&
+ 'activeCoordinationRelays' in value &&
+ Array.isArray(value.activeCoordinationRelays) &&
+ value.activeCoordinationRelays.every((address) => typeof address === 'string') &&
'connect' in value &&
typeof value.connect === 'function' &&
'connectMeshControl' in value &&
diff --git a/scripts/generate-runtime-host-peer-notices.mjs b/scripts/generate-runtime-host-peer-notices.mjs
index ce9a7bb0f..5cf6b719d 100644
--- a/scripts/generate-runtime-host-peer-notices.mjs
+++ b/scripts/generate-runtime-host-peer-notices.mjs
@@ -104,7 +104,11 @@ function declaredLicenseFallback(pkg, source) {
text: apache2LicenseText,
};
}
- if (pkg.license === 'MIT') {
+ if (
+ pkg.license
+ .split(/\s+OR\s+/u)
+ .some((alternative) => alternative.replaceAll(/[()]/gu, '').trim() === 'MIT')
+ ) {
const authors = pkg.authors.length > 0 ? pkg.authors.join(', ') : 'not included';
return {
name: 'MIT.txt',
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org