代码
- 内容分发块级寻址:新增 src/net/relay/content/{chunker,store,runtime,source,peer,crypto}.ts
- 组密钥(C 档)确定性加密:AES-256-GCM,块 id β′ = sha256(密文) 前 32 hex;双 epoch 过渡窗口
- 实例生命周期:三处 teardown() 不再杀实例(local/remote/leased-spawner);启动认领 + TCP 探活判孤儿
- 骨干选路:jitter 选路 + endpoint-target;relay client/server/wire/identity/directory/rendezvous/switcher 调整
- 工作台 src/web/server.ts、src/worker/relay-tunnel.ts 装配与候选链观测
脚本与测试
- scripts/overlay-{probe,keyring,jitter}.cjs 更新
- 探针新增 OBS-21(每连接候选数)/ OBS-22(teardown 静态守卫 + 认领面)/ OBS-23(组密钥加密)
- 新增 test/{orchestrator-teardown,orchestrator-rehydrate,overlay-content,overlay-jitter}.test.mjs;relay 两例更新
文档
- 新增交接单:覆盖网络-序24-内容分发块级寻址 / 序25-实例逐步拉起 / 序26-骨干稳定选路与加密
- INDEX.md、交接单/README.md、skills/dsh-auto-handoff-chain/SKILL.md 同步
验收(零回归,2026-09-18 08:0x 复核)
- npm test 201 tests / 200 pass / 0 fail / 1 skipped
- overlay-failover-drill --scene all --table 12 PASS / 0 SKIP / 0 FAIL
- overlay-probe --table 23 PASS / 0 SKIP / 0 FAIL (rc=0)
158 lines
6.7 KiB
TypeScript
158 lines
6.7 KiB
TypeScript
/**
|
||
* 覆盖网络中继(relay)—— **自研、零新增依赖、零新增公网口**(传输方案 §10 定案)。
|
||
*
|
||
* ## 一段话说完它是什么
|
||
* worker 侧 `RelayClient` **只拨出**一条 wss;`RelayServer` **只绑回环**,为每个注册端口在
|
||
* `127.0.0.1` 上开一条监听;Manager 连那条回环口 ⇒ 字节经多路复用跑回 worker 本地端口。
|
||
* 对 Manager 而言地址形态与 sshd 版**完全同形**(`host:port`),所以换它是 env 级动作。
|
||
*
|
||
* ## 三条硬约束(来自实测,改动前先读)
|
||
* 1. **零新增公网口**:47 无本机防火墙(`nft INPUT policy accept`)⇒ 绑 `0.0.0.0` 即公网可达。
|
||
* 2. **worker 只拨出**:任何"在 worker 上开监听"的方案都让暴露面从 O(1) 变 O(N)。
|
||
* 3. **精确 ACK**:注册与开流都必须有确认帧,未确认即断并计数 —— 治的是 SSH 版"静默失败"的病根。
|
||
*
|
||
* @module dshs/net/relay
|
||
*/
|
||
|
||
export { RelayServer, RELAY_PATH, DEFAULT_RELAY_PORT, DEFAULT_AUTH_DEADLINE_MS, DEFAULT_AUTH_WINDOW_MS, DEFAULT_IDLE_TIMEOUT_MS, DEFAULT_HB_SEC, DEFAULT_MAX_STREAMS_PER_PORT, DEFAULT_QUEUE_MAX_BYTES, DEFAULT_PRESENCE_GRACE_MS, DEFAULT_PRESENCE_OFFLINE_DEBOUNCE_MS, DEFAULT_PRESENCE_BATCH_MS, DEFAULT_PRESENCE_TTL_FACTOR, DEFAULT_PRESENCE_SUB_MAX } from './server.js'
|
||
export type { RelayServerOptions, RelayStatus, PresenceEntry } from './server.js'
|
||
export { RelayClient, describeClientStatus, gracefulBurstMsDefault, openedChannelFailedTerminally, waitUpOnStatus } from './client.js'
|
||
export type { RelayClientOptions, RelayClientStatus, RelayClientState, WebSocketLike, WebSocketCtor, WaitUpStatusOptions, RelayPresenceEntry, RelayPresenceStatus, RelayPresenceState } from './client.js'
|
||
export { RelayRendezvous } from './rendezvous.js'
|
||
export type { RelayRendezvousOptions } from './rendezvous.js'
|
||
export { RelayDialer } from './dialer.js'
|
||
export type { RelayDialerOptions } from './dialer.js'
|
||
export { RelayFailoverSupervisor, relayFailoverThresholds } from './switcher.js'
|
||
export type {
|
||
RelayChannelHandle,
|
||
RelayFailoverDeps,
|
||
RelayFailoverStats,
|
||
RelayFailoverThresholds,
|
||
} from './switcher.js'
|
||
export { MuxDuplex } from './duplex.js'
|
||
export type { MuxDuplexOptions } from './duplex.js'
|
||
export { MUX, WS_CLOSE, acceptWebSocket, encodeMux, decodeMux, encodeJsonFrame, parseJsonPayload, WsConnection } from './wire.js'
|
||
export type { MuxFrame, MuxType, WsServerOptions } from './wire.js'
|
||
export { OPS_NETWORK, NAME_SEP, assertNetworkId, assertSameNetwork, describeDialers, isHostId, isNetworkId, logicalName, normalizeDialers, parseLogicalName, sameNetwork } from './network.js'
|
||
export { DIRECTORY_PATH, DIRECTORY_PAYLOAD_TAG, DIRECTORY_VERSION, DEFAULT_OVERLAY_SEED, buildDirectoryDocument, directoryPayload, directoryUrlFor, overlayEnvSeeds, overlayEnvTrustedKeys, parseDirectory, publicKeyFrom, publicRelayEntries, readCachedDirectory, resolveOverlayRelay, listOverlayRelayCandidates, signDirectory, toRelayUrl, verifyDirectory, writeCachedDirectory } from './directory.js'
|
||
export type { CachedDirectory, DirectoryVerdict, OverlayAddressSource, OverlayDirectory, OverlayRelayCandidates, OverlayRelayResolution, ResolveOverlayRelayOptions } from './directory.js'
|
||
export { keyEntryOf, loadKeysFile, parseKeysInline, normalizeKeyRecord, lookupKey, describeKeyEntry, assertKey } from './keys.js'
|
||
export type { RelayKeyEntry, RelayKeyMap } from './keys.js'
|
||
export {
|
||
IDENTITY_VERSION,
|
||
NODE_GRANT_TAG,
|
||
REVOCATION_TAG,
|
||
SIGNER_SET_TAG,
|
||
PROOF_TAG,
|
||
DEFAULT_NODE_KEY_FILE,
|
||
generateAuthorityKey,
|
||
generateNodeKey,
|
||
identityEnvRequire,
|
||
identityEnvTrustedRoots,
|
||
identityEnvTrustedSigners,
|
||
loadNodeGrant,
|
||
loadOrCreateNodeKey,
|
||
loadRevocations,
|
||
loadTrustedSigners,
|
||
nodeGrantPayload,
|
||
nodeKeyFingerprint,
|
||
normalizePublicKey,
|
||
parseNodeGrant,
|
||
parseRevocationList,
|
||
parseSignerSet,
|
||
proofPayload,
|
||
publicKeyOfPrivate,
|
||
readSignedFile,
|
||
revocationPayload,
|
||
signNodeGrant,
|
||
signProof,
|
||
signRevocations,
|
||
signSignerSet,
|
||
signerSetPayload,
|
||
verifyNodeGrant,
|
||
verifyPeerGrant,
|
||
verifyProof,
|
||
verifyRevocations,
|
||
verifySignerSet,
|
||
writeSignedFile,
|
||
} from './identity.js'
|
||
export type {
|
||
IdentityReason,
|
||
IdentityVerdict,
|
||
NodeGrant,
|
||
PeerVerifyContext,
|
||
RevocationList,
|
||
SignerSet,
|
||
} from './identity.js'
|
||
export { chooseNode, describeDecision, rankCandidates, scoreCandidate } from './placement.js'
|
||
export type { ChooseOptions, NodeCandidate, PlacementDecision, PlacementScore, PlacementWeights } from './placement.js'
|
||
export { relayEndpointTarget, hostNameIndex } from './endpoint-target.js'
|
||
export type { RelayEndpointDecision, RelayEndpointTargetInput } from './endpoint-target.js'
|
||
|
||
// ── 序㉔ 内容分发(块级内容寻址 · 同网段 peer 优先)────────────────────────────
|
||
// ⛔ 本段**只做导出**(交接单 §3.1:`index.ts` 改动仅限导出)。
|
||
// 模块职责:`chunker`(切分+哈希)→ `store`(内容寻址存储)→ `source`(源优先级链)
|
||
// → `peer`(同网段 peer 与分组隔离)。
|
||
export {
|
||
DEFAULT_BLOCK_SIZE,
|
||
BLOCK_ID_HEX_LEN,
|
||
blockIdOf,
|
||
contentIdOf,
|
||
isBlockId,
|
||
chunkify,
|
||
planOf,
|
||
reassemble,
|
||
} from './content/chunker.js'
|
||
export type { Chunk, ChunkedContent } from './content/chunker.js'
|
||
export { ContentStore, DEFAULT_MAX_BYTES } from './content/store.js'
|
||
export type { ContentStoreCounters, ContentStoreOptions } from './content/store.js'
|
||
export { ContentSourceChain, SOURCE_TIERS, DEFAULT_TIER_ORDER, emptySourceCounters } from './content/source.js'
|
||
export type {
|
||
SourceTier,
|
||
SourceHitCounters,
|
||
SourceFetchOutcome,
|
||
TierFetchResult,
|
||
TierFetcher,
|
||
ContentSourceChainOptions,
|
||
} from './content/source.js'
|
||
export { ContentPeerGroup, groupKeyOf, sameGroup, PEER_COUNTER_KEYS } from './content/peer.js'
|
||
export type {
|
||
PeerDeclaration,
|
||
PeerCounters,
|
||
PeerCounterKey,
|
||
ContentPeerGroupOptions,
|
||
} from './content/peer.js'
|
||
export { ContentRuntime } from './content/runtime.js'
|
||
export type { ContentRuntimeOptions, ContentSnapshot } from './content/runtime.js'
|
||
// 🆕 序㉘ · 单 B:组密钥加密(缺省不启用)—— 走**既有**验签链,⛔ 不新根、不新签名链。
|
||
export {
|
||
ContentCipher,
|
||
openContentCipher,
|
||
loadGroupKeyFile,
|
||
describeGroupKey,
|
||
keyIdOf,
|
||
verifyGroupKeyCredential,
|
||
parseGroupKeyCredential,
|
||
groupKeyCredentialPayload,
|
||
CONTENT_CRYPTO_COUNTER_KEYS,
|
||
DEFAULT_GROUP_KEY_FILE,
|
||
DEFAULT_EPOCH_GRACE_MS,
|
||
CONTENT_CIPHER_VERSION,
|
||
GROUP_KEY_TAG,
|
||
IV_LEN,
|
||
TAG_LEN,
|
||
KEY_LEN,
|
||
MIN_BLOB_LEN,
|
||
} from './content/crypto.js'
|
||
export type {
|
||
ContentCryptoCounters,
|
||
ContentCryptoCounterKey,
|
||
ContentCipherOptions,
|
||
GroupKeyFile,
|
||
GroupKeyEpochEntry,
|
||
GroupKeyCredential,
|
||
GroupKeyLoadReason,
|
||
GroupKeyLoadResult,
|
||
LoadGroupKeyOptions,
|
||
} from './content/crypto.js'
|