@steve02081504/fount-p2p 0.0.12 → 0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -10
- package/core/composite_key.mjs +5 -5
- package/crypto/crypto.mjs +3 -3
- package/crypto/key.mjs +12 -12
- package/dag/storage.mjs +36 -36
- package/discovery/advert_peer_hints.mjs +9 -3
- package/discovery/adverts.mjs +115 -0
- package/discovery/bt/index.mjs +153 -112
- package/discovery/bt/probe_child.mjs +2 -1
- package/discovery/bt/runtime.mjs +2 -9
- package/discovery/index.mjs +267 -62
- package/discovery/internal/signal_crypto.mjs +109 -0
- package/discovery/lan.mjs +279 -0
- package/discovery/lan_interfaces.mjs +78 -0
- package/discovery/lan_peer_hints.mjs +22 -5
- package/discovery/nostr.mjs +474 -65
- package/federation/chunk_fetch_pending.mjs +10 -13
- package/federation/manifest_fetch_pending.mjs +1 -3
- package/files/assemble.mjs +14 -14
- package/files/assemble_stream.mjs +6 -6
- package/files/chunk_responder.mjs +29 -20
- package/files/evfs.mjs +29 -28
- package/files/manifest_fetch.mjs +16 -3
- package/files/public_manifest.mjs +11 -11
- package/files/transfer_key_registry.mjs +2 -2
- package/index.mjs +86 -11
- package/infra/cli.mjs +62 -0
- package/infra/debug_log.mjs +56 -0
- package/infra/default_node_dir.mjs +22 -0
- package/infra/priority.mjs +56 -0
- package/infra/service.mjs +140 -0
- package/infra/tunables.json +5 -0
- package/link/frame.mjs +9 -16
- package/link/handshake.mjs +25 -18
- package/link/pipe.mjs +8 -8
- package/link/providers/ble_gatt.mjs +6 -6
- package/link/providers/lan_tcp.mjs +62 -33
- package/link/providers/webrtc.mjs +3 -1
- package/link/rtc.mjs +3 -3
- package/mailbox/consumer_registry.mjs +2 -2
- package/mailbox/deliver_or_store.mjs +5 -5
- package/mailbox/wire.mjs +28 -24
- package/node/entity_store.mjs +6 -6
- package/node/instance.mjs +46 -27
- package/node/local_data_revision.mjs +26 -0
- package/node/log.mjs +56 -0
- package/node/network.mjs +37 -5
- package/node/reputation_store.mjs +4 -4
- package/node/reputation_sync.mjs +318 -0
- package/node/routing_profile.mjs +21 -0
- package/node/signaling_config.mjs +30 -11
- package/overlay/index.mjs +22 -1
- package/package.json +13 -2
- package/rooms/scoped_link.mjs +17 -166
- package/transport/advert_ingest.mjs +11 -14
- package/transport/group_link_set.mjs +149 -99
- package/transport/link_registry.mjs +211 -60
- package/transport/mesh_keepalive.mjs +217 -0
- package/transport/node_scope.mjs +289 -0
- package/transport/offer_answer.mjs +45 -48
- package/transport/peer_pool.mjs +116 -14
- package/transport/remote_user_room.mjs +6 -9
- package/transport/{rtc_mdns_filter.mjs → rtc_ice_local_hostname.mjs} +13 -13
- package/transport/runtime_bootstrap.mjs +170 -108
- package/transport/tunables.json +11 -0
- package/transport/tunables.mjs +18 -0
- package/transport/user_room.mjs +83 -158
- package/trust_graph/build.mjs +0 -2
- package/trust_graph/cache.mjs +12 -3
- package/trust_graph/registry.mjs +6 -6
- package/trust_graph/send.mjs +0 -3
- package/utils/async_mutex.mjs +4 -4
- package/utils/emit_safe.mjs +3 -3
- package/utils/json_io.mjs +12 -12
- package/utils/map_pool.mjs +5 -5
- package/wire/part_ingress.mjs +32 -28
- package/wire/part_query.mjs +26 -20
- package/discovery/mdns.mjs +0 -197
- package/transport/signal_crypto.mjs +0 -104
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
|
+
import { getReputationTable } from '../node/reputation_sync.mjs'
|
|
3
|
+
import { pickNodeScoreFromReputation } from '../reputation/pick_score.mjs'
|
|
4
|
+
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/** @type {{ useLocalReputation: boolean }} */
|
|
8
|
+
let priorityConfig = { useLocalReputation: false }
|
|
9
|
+
|
|
10
|
+
const PRIORITY_BOOST = 1000
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 配置 infra 路由加权(是否用本地 reputation)。
|
|
14
|
+
* @param {{ useLocalReputation?: boolean }} config - 是否用本地 reputation 加权路由
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
export function setInfraPriority(config = {}) {
|
|
18
|
+
priorityConfig = {
|
|
19
|
+
useLocalReputation: Boolean(config.useLocalReputation),
|
|
20
|
+
}
|
|
21
|
+
applyPriorityToRegistry()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @returns {{ useLocalReputation: boolean }} 当前 priority 配置副本
|
|
26
|
+
*/
|
|
27
|
+
export function getInfraPriority() {
|
|
28
|
+
return { ...priorityConfig }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @returns {void}
|
|
33
|
+
*/
|
|
34
|
+
export function applyPriorityToRegistry() {
|
|
35
|
+
const registry = getLinkRegistry()
|
|
36
|
+
if (!priorityConfig.useLocalReputation) {
|
|
37
|
+
registry.setPriorityWeightFunction(null)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
registry.setPriorityWeightFunction(nodeHash => {
|
|
41
|
+
const score = pickNodeScoreFromReputation(
|
|
42
|
+
getReputationTable(),
|
|
43
|
+
normalizeHex64(nodeHash) || nodeHash,
|
|
44
|
+
)
|
|
45
|
+
return Math.floor(score * PRIORITY_BOOST)
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* stopInfra:卸 weight,并重置 priority 配置,避免再次 startInfra 幽灵恢复加权。
|
|
51
|
+
* @returns {void}
|
|
52
|
+
*/
|
|
53
|
+
export function clearInfraPriorityFromRegistry() {
|
|
54
|
+
priorityConfig = { useLocalReputation: false }
|
|
55
|
+
getLinkRegistry().setPriorityWeightFunction(null)
|
|
56
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
|
|
2
|
+
import { isNodeInitialized } from '../node/instance.mjs'
|
|
3
|
+
import { setOverlayRateGate, clearOverlayRateGate } from '../overlay/index.mjs'
|
|
4
|
+
import { getLinkRegistry } from '../transport/link_registry.mjs'
|
|
5
|
+
import { attachNodeScopeMailbox } from '../transport/node_scope.mjs'
|
|
6
|
+
|
|
7
|
+
import { attachInfraDebugLog, detachInfraDebugLog } from './debug_log.mjs'
|
|
8
|
+
import {
|
|
9
|
+
applyPriorityToRegistry,
|
|
10
|
+
clearInfraPriorityFromRegistry,
|
|
11
|
+
getInfraPriority,
|
|
12
|
+
setInfraPriority,
|
|
13
|
+
} from './priority.mjs'
|
|
14
|
+
import infraTunables from './tunables.json' with { type: 'json' }
|
|
15
|
+
|
|
16
|
+
/** @type {Map<string, { tokens: number, updatedAt: number }>} */
|
|
17
|
+
const overlayRateBuckets = new Map()
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Token bucket:桶容量 = burst,补充速率 = perMin/min。
|
|
21
|
+
* @param {Map<string, { tokens: number, updatedAt: number }>} buckets - 每 sender 桶状态
|
|
22
|
+
* @param {string} sender - 发送方 nodeHash
|
|
23
|
+
* @param {number} now - 当前时间戳(ms)
|
|
24
|
+
* @param {{ perMin: number, burst: number }} limits - 限速参数
|
|
25
|
+
* @returns {boolean} 是否允许本次 overlay 动作
|
|
26
|
+
*/
|
|
27
|
+
export function consumeOverlayRateToken(buckets, sender, now, limits) {
|
|
28
|
+
const perMin = Math.max(1, limits.perMin)
|
|
29
|
+
const burst = Math.max(1, limits.burst)
|
|
30
|
+
const refillPerMs = perMin / 60_000
|
|
31
|
+
let bucket = buckets.get(sender)
|
|
32
|
+
if (!bucket) bucket = { tokens: burst, updatedAt: now }
|
|
33
|
+
const elapsed = Math.max(0, now - bucket.updatedAt)
|
|
34
|
+
bucket.tokens = Math.min(burst, bucket.tokens + elapsed * refillPerMs)
|
|
35
|
+
bucket.updatedAt = now
|
|
36
|
+
if (bucket.tokens < 1) {
|
|
37
|
+
buckets.set(sender, bucket)
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
bucket.tokens -= 1
|
|
41
|
+
buckets.set(sender, bucket)
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @returns {void}
|
|
47
|
+
*/
|
|
48
|
+
function installOverlayRateLimit() {
|
|
49
|
+
const limits = {
|
|
50
|
+
perMin: Math.max(1, Number(infraTunables.overlayRatePerMin) || 120),
|
|
51
|
+
burst: Math.max(1, Number(infraTunables.overlayRateBurst) || 30),
|
|
52
|
+
}
|
|
53
|
+
setOverlayRateGate((sender, action) => {
|
|
54
|
+
if (action !== 'route_req' && action !== 'relay') return true
|
|
55
|
+
return consumeOverlayRateToken(overlayRateBuckets, sender, Date.now(), limits)
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @returns {void}
|
|
61
|
+
*/
|
|
62
|
+
function removeOverlayRateLimit() {
|
|
63
|
+
clearOverlayRateGate()
|
|
64
|
+
overlayRateBuckets.clear()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** @type {boolean} */
|
|
68
|
+
let infraRunning = false
|
|
69
|
+
|
|
70
|
+
/** @type {(() => void) | null} */
|
|
71
|
+
let mailboxDispose = null
|
|
72
|
+
|
|
73
|
+
/** @type {(() => void) | null} */
|
|
74
|
+
let debugDispose = null
|
|
75
|
+
|
|
76
|
+
/** @type {number | null} */
|
|
77
|
+
let savedMaxActive = null
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @returns {boolean} infra relay 是否在运行
|
|
81
|
+
*/
|
|
82
|
+
export function isInfraRunning() {
|
|
83
|
+
return infraRunning
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 启动 public-good infra:overlay、mailbox、rate limit、priority、debug log。
|
|
88
|
+
* @param {{ maxActive?: number, logger?: { info?: Function, warn?: Function, error?: Function, log?: Function } | null }} [options] - maxActive 与 debug logger
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
export async function startInfra(options = {}) {
|
|
92
|
+
if (!isNodeInitialized()) throw new Error('p2p: startInfra requires initNode')
|
|
93
|
+
if (infraRunning) {
|
|
94
|
+
if (options.maxActive != null) await getLinkRegistry().setMaxActive(options.maxActive)
|
|
95
|
+
if (options.logger !== undefined) {
|
|
96
|
+
detachInfraDebugLog()
|
|
97
|
+
debugDispose = attachInfraDebugLog(options.logger ?? null)
|
|
98
|
+
}
|
|
99
|
+
applyPriorityToRegistry()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
const registry = getLinkRegistry()
|
|
103
|
+
await registry.ensureRuntime()
|
|
104
|
+
registry.ensureOverlayRouter()
|
|
105
|
+
installOverlayRateLimit()
|
|
106
|
+
savedMaxActive = registry.getMaxActive()
|
|
107
|
+
if (options.maxActive != null)
|
|
108
|
+
await registry.setMaxActive(options.maxActive)
|
|
109
|
+
else
|
|
110
|
+
await registry.setMaxActive(infraTunables.defaultMaxActive ?? savedMaxActive)
|
|
111
|
+
if (!mailboxDispose) mailboxDispose = attachNodeScopeMailbox()
|
|
112
|
+
const logger = options.logger === undefined ? console : options.logger ?? null
|
|
113
|
+
debugDispose = attachInfraDebugLog(logger)
|
|
114
|
+
applyPriorityToRegistry()
|
|
115
|
+
infraRunning = true
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 卸掉 infra 自己挂的面(mailbox / rate / debug / priority / maxActive 恢复)。
|
|
120
|
+
* 不碰用户另行 attach 的 wires,不绑信誉同步。
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
export async function stopInfra() {
|
|
124
|
+
if (!infraRunning) return
|
|
125
|
+
debugDispose?.()
|
|
126
|
+
debugDispose = null
|
|
127
|
+
detachInfraDebugLog()
|
|
128
|
+
clearInfraPriorityFromRegistry()
|
|
129
|
+
removeOverlayRateLimit()
|
|
130
|
+
mailboxDispose?.()
|
|
131
|
+
mailboxDispose = null
|
|
132
|
+
const registry = getLinkRegistry()
|
|
133
|
+
if (savedMaxActive != null)
|
|
134
|
+
await registry.setMaxActive(savedMaxActive)
|
|
135
|
+
savedMaxActive = null
|
|
136
|
+
infraRunning = false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 再导出:infra 路由加权配置(见 `priority.mjs`)。 */
|
|
140
|
+
export { getInfraPriority, setInfraPriority }
|
package/link/frame.mjs
CHANGED
|
@@ -2,12 +2,10 @@ import { randomBytes } from 'node:crypto'
|
|
|
2
2
|
|
|
3
3
|
import { bytesToHex, hexToBytes, toBytes } from '../core/bytes_codec.mjs'
|
|
4
4
|
|
|
5
|
-
/** 二进制帧协议版本号。 */
|
|
6
|
-
export const FRAME_VERSION = 1
|
|
7
5
|
/** frameId 字段字节长度(128 位)。 */
|
|
8
6
|
export const FRAME_ID_BYTES = 16
|
|
9
|
-
/** 帧头:
|
|
10
|
-
export const FRAME_HEADER_BYTES =
|
|
7
|
+
/** 帧头:frameId(16) + seq(4) + total(4)。 */
|
|
8
|
+
export const FRAME_HEADER_BYTES = FRAME_ID_BYTES + 4 + 4
|
|
11
9
|
/** 默认单帧最大 chunk 大小(15 KiB)。 */
|
|
12
10
|
export const DEFAULT_MAX_FRAME_CHUNK_BYTES = 15 * 1024
|
|
13
11
|
/** 重组后消息最大字节数(8 MiB)。 */
|
|
@@ -62,11 +60,10 @@ export function encodeFrames(frameId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_C
|
|
|
62
60
|
const end = Math.min(body.byteLength, start + chunkBytes)
|
|
63
61
|
const chunk = body.subarray(start, end)
|
|
64
62
|
const frame = new Uint8Array(FRAME_HEADER_BYTES + chunk.byteLength)
|
|
65
|
-
frame
|
|
66
|
-
frame.set(idBytes, 1)
|
|
63
|
+
frame.set(idBytes, 0)
|
|
67
64
|
const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength)
|
|
68
|
-
view.setUint32(
|
|
69
|
-
view.setUint32(
|
|
65
|
+
view.setUint32(FRAME_ID_BYTES, seq, false)
|
|
66
|
+
view.setUint32(FRAME_ID_BYTES + 4, total, false)
|
|
70
67
|
frame.set(chunk, FRAME_HEADER_BYTES)
|
|
71
68
|
frames.push(frame)
|
|
72
69
|
}
|
|
@@ -76,23 +73,19 @@ export function encodeFrames(frameId, bytes, maxChunkBytes = DEFAULT_MAX_FRAME_C
|
|
|
76
73
|
/**
|
|
77
74
|
* 解析单帧头与 chunk。
|
|
78
75
|
* @param {Uint8Array | ArrayBuffer | ArrayBufferView} frame 原始帧
|
|
79
|
-
* @returns {{
|
|
76
|
+
* @returns {{ frameId: string, seq: number, total: number, chunk: Uint8Array }} 帧字段
|
|
80
77
|
*/
|
|
81
78
|
export function decodeFrame(frame) {
|
|
82
79
|
const bytes = toBytes(frame)
|
|
83
80
|
if (bytes.byteLength < FRAME_HEADER_BYTES)
|
|
84
81
|
throw new Error('p2p: frame too short')
|
|
85
|
-
const
|
|
86
|
-
if (version !== FRAME_VERSION)
|
|
87
|
-
throw new Error(`p2p: unsupported frame version ${version}`)
|
|
88
|
-
const frameId = bytesToHex(bytes.subarray(1, 1 + FRAME_ID_BYTES))
|
|
82
|
+
const frameId = bytesToHex(bytes.subarray(0, FRAME_ID_BYTES))
|
|
89
83
|
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
90
|
-
const seq = view.getUint32(
|
|
91
|
-
const total = view.getUint32(
|
|
84
|
+
const seq = view.getUint32(FRAME_ID_BYTES, false)
|
|
85
|
+
const total = view.getUint32(FRAME_ID_BYTES + 4, false)
|
|
92
86
|
if (!total || seq >= total)
|
|
93
87
|
throw new Error('p2p: invalid frame sequence')
|
|
94
88
|
return {
|
|
95
|
-
version,
|
|
96
89
|
frameId,
|
|
97
90
|
seq,
|
|
98
91
|
total,
|
package/link/handshake.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer'
|
|
|
2
2
|
import { randomBytes } from 'node:crypto'
|
|
3
3
|
|
|
4
4
|
import { isHex64, normalizeHex64 } from '../core/hexIds.mjs'
|
|
5
|
+
import { normalizeLanHosts } from '../discovery/lan_interfaces.mjs'
|
|
5
6
|
import { normalizeTcpPort } from '../core/tcp_port.mjs'
|
|
6
7
|
import { keyPairFromSeed, pubKeyHash, sign, verify } from '../crypto/crypto.mjs'
|
|
7
8
|
import { ensureNodeSeed, getNodeHash } from '../node/identity.mjs'
|
|
@@ -51,7 +52,7 @@ export function buildAuthMessage(peerNonce, localBinding, localNodeHash) {
|
|
|
51
52
|
/**
|
|
52
53
|
* 构造 link hello 握手包。
|
|
53
54
|
* @param {{ nodeHash?: string, nodePubKey?: string, nonce?: string }} [options] 可选身份字段,省略则从本地节点种子推导
|
|
54
|
-
* @returns {{
|
|
55
|
+
* @returns {{ nodeHash: string, nodePubKey: string, nonce: string }} hello 对象
|
|
55
56
|
*/
|
|
56
57
|
export function buildHello(options = {}) {
|
|
57
58
|
let publicKey = null
|
|
@@ -66,7 +67,7 @@ export function buildHello(options = {}) {
|
|
|
66
67
|
throw new Error('p2p: invalid hello fields')
|
|
67
68
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash)
|
|
68
69
|
throw new Error('p2p: hello nodePubKey does not match nodeHash')
|
|
69
|
-
return {
|
|
70
|
+
return { nodeHash, nodePubKey, nonce }
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
@@ -92,13 +93,12 @@ export async function buildAuth(peerNonce, localBinding, options = {}) {
|
|
|
92
93
|
/**
|
|
93
94
|
* 解析并校验 hello 对象,无效时返回 null。
|
|
94
95
|
* @param {unknown} hello 原始 hello 载荷
|
|
95
|
-
* @returns {{
|
|
96
|
+
* @returns {{ nodeHash: string, nodePubKey: string, nonce: string } | null} 规范化 hello 或 null
|
|
96
97
|
*/
|
|
97
98
|
export function parseHello(hello) {
|
|
98
99
|
const nodeHash = normalizeHex64(hello?.nodeHash)
|
|
99
100
|
const nodePubKey = normalizeHex64(hello?.nodePubKey)
|
|
100
101
|
const nonce = normalizeHex64(hello?.nonce)
|
|
101
|
-
if (Number(hello?.v) !== 1) return null
|
|
102
102
|
if (!isHex64(nodeHash) || !isHex64(nodePubKey) || !isHex64(nonce)) return null
|
|
103
103
|
try {
|
|
104
104
|
if (pubKeyHash(Buffer.from(nodePubKey, 'hex')) !== nodeHash) return null
|
|
@@ -106,7 +106,7 @@ export function parseHello(hello) {
|
|
|
106
106
|
catch {
|
|
107
107
|
return null
|
|
108
108
|
}
|
|
109
|
-
return {
|
|
109
|
+
return { nodeHash, nodePubKey, nonce }
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
@@ -136,26 +136,30 @@ export async function verifyAuth(hello, auth, expectedNonce, remoteBinding) {
|
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
138
|
* 构造 discovery advert 待签名字节串。
|
|
139
|
-
* @param {string}
|
|
139
|
+
* @param {string} rendezvousKey discovery 内部汇合键
|
|
140
140
|
* @param {number} ts 时间戳(毫秒)
|
|
141
141
|
* @param {string} nodeHash 节点 nodeHash
|
|
142
142
|
* @param {number | null} [tcpPort=null] 可选 LAN TCP 监听端口(签入消息)
|
|
143
|
+
* @param {unknown} [lanHosts=null] 可选 LAN IPv4 列表(签入消息)
|
|
143
144
|
* @returns {Uint8Array} 待签名消息字节
|
|
144
145
|
*/
|
|
145
|
-
export function buildAdvertMessage(
|
|
146
|
-
const base = `fount-advert\0${String(
|
|
146
|
+
export function buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort = null, lanHosts = null) {
|
|
147
|
+
const base = `fount-advert\0${String(rendezvousKey)}\0${String(ts)}\0${normalizeHex64(nodeHash)}`
|
|
147
148
|
const port = normalizeTcpPort(tcpPort)
|
|
148
|
-
|
|
149
|
+
let message = port ? `${base}\0${port}` : base
|
|
150
|
+
const hosts = normalizeLanHosts(lanHosts)
|
|
151
|
+
if (hosts.length) message += `\0${hosts.join(',')}`
|
|
152
|
+
return Buffer.from(message, 'utf8')
|
|
149
153
|
}
|
|
150
154
|
|
|
151
155
|
/**
|
|
152
156
|
* 构造带签名的 discovery advert。
|
|
153
|
-
* @param {string}
|
|
157
|
+
* @param {string} rendezvousKey discovery 内部汇合键
|
|
154
158
|
* @param {number} [ts=Date.now()] 时间戳(毫秒)
|
|
155
|
-
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number } | null} [options] 签名身份与可选 tcpPort
|
|
156
|
-
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number }>} 签名 advert
|
|
159
|
+
* @param {{ secretKey?: Uint8Array, nodeHash?: string, nodePubKey?: string, tcpPort?: number, lanHosts?: unknown }} | null} [options] 签名身份与可选 tcpPort / lanHosts
|
|
160
|
+
* @returns {Promise<{ nodeHash: string, nodePubKey: string, ts: number, sig: string, tcpPort?: number, lanHosts?: string[] }>} 签名 advert
|
|
157
161
|
*/
|
|
158
|
-
export async function buildSignedAdvert(
|
|
162
|
+
export async function buildSignedAdvert(rendezvousKey, ts = Date.now(), options = null) {
|
|
159
163
|
const seed = options?.secretKey
|
|
160
164
|
? Buffer.from(options.secretKey)
|
|
161
165
|
: Buffer.from(ensureNodeSeed(), 'hex')
|
|
@@ -167,7 +171,8 @@ export async function buildSignedAdvert(topic, ts = Date.now(), options = null)
|
|
|
167
171
|
const tcpPort = normalizeTcpPort(options?.tcpPort)
|
|
168
172
|
if (options?.tcpPort && !tcpPort)
|
|
169
173
|
throw new Error('p2p: advert tcpPort invalid')
|
|
170
|
-
const
|
|
174
|
+
const lanHosts = normalizeLanHosts(options?.lanHosts)
|
|
175
|
+
const message = buildAdvertMessage(rendezvousKey, ts, nodeHash, tcpPort, lanHosts)
|
|
171
176
|
const sig = await sign(message, secretKey)
|
|
172
177
|
const advert = {
|
|
173
178
|
nodeHash,
|
|
@@ -176,19 +181,20 @@ export async function buildSignedAdvert(topic, ts = Date.now(), options = null)
|
|
|
176
181
|
sig: Buffer.from(sig).toString('hex'),
|
|
177
182
|
}
|
|
178
183
|
if (tcpPort) advert.tcpPort = tcpPort
|
|
184
|
+
if (lanHosts.length) advert.lanHosts = lanHosts
|
|
179
185
|
return advert
|
|
180
186
|
}
|
|
181
187
|
|
|
182
188
|
/**
|
|
183
189
|
* 验证 discovery advert 签名与时间戳,成功返回发布者 nodeHash。
|
|
184
|
-
* @param {string}
|
|
190
|
+
* @param {string} rendezvousKey 期望的汇合键
|
|
185
191
|
* @param {unknown} advert 原始 advert 载荷
|
|
186
192
|
* @param {number} [now=Date.now()] 当前时间(毫秒)
|
|
187
193
|
* @param {number} [maxSkewMs=10 * 60_000] 允许的最大时钟偏差(毫秒)
|
|
188
194
|
* @returns {Promise<string | null>} 验证通过的 nodeHash,失败返回 null
|
|
189
195
|
*/
|
|
190
|
-
export async function verifySignedAdvert(
|
|
191
|
-
const parsedHello = parseHello({
|
|
196
|
+
export async function verifySignedAdvert(rendezvousKey, advert, now = Date.now(), maxSkewMs = 10 * 60_000) {
|
|
197
|
+
const parsedHello = parseHello({ nodeHash: advert?.nodeHash, nodePubKey: advert?.nodePubKey, nonce: '0'.repeat(64) })
|
|
192
198
|
if (!parsedHello) return null
|
|
193
199
|
const ts = Number(advert?.ts)
|
|
194
200
|
const sig = String(advert?.sig ?? '').trim().toLowerCase()
|
|
@@ -196,7 +202,8 @@ export async function verifySignedAdvert(topic, advert, now = Date.now(), maxSke
|
|
|
196
202
|
const hasTcpPortField = !!advert?.tcpPort
|
|
197
203
|
const tcpPort = normalizeTcpPort(advert?.tcpPort)
|
|
198
204
|
if (hasTcpPortField && !tcpPort) return null
|
|
199
|
-
const
|
|
205
|
+
const lanHosts = normalizeLanHosts(advert?.lanHosts)
|
|
206
|
+
const message = buildAdvertMessage(rendezvousKey, ts, parsedHello.nodeHash, tcpPort, lanHosts)
|
|
200
207
|
const ok = await verify(Buffer.from(sig, 'hex'), message, Buffer.from(parsedHello.nodePubKey, 'hex'))
|
|
201
208
|
return ok ? parsedHello.nodeHash : null
|
|
202
209
|
}
|
package/link/pipe.mjs
CHANGED
|
@@ -28,25 +28,25 @@ export function asLinkHandle(pipe, extras = {}) {
|
|
|
28
28
|
/** @returns {number} 提供者 level */
|
|
29
29
|
get level() { return pipe.level },
|
|
30
30
|
/**
|
|
31
|
-
* @param {...any}
|
|
31
|
+
* @param {...any} callArguments 透传 send
|
|
32
32
|
* @returns {ReturnType<typeof pipe.send>} 是否发送成功
|
|
33
33
|
*/
|
|
34
|
-
send: (...
|
|
34
|
+
send: (...callArguments) => pipe.send(...callArguments),
|
|
35
35
|
/**
|
|
36
|
-
* @param {...any}
|
|
36
|
+
* @param {...any} callArguments 透传 onEnvelope
|
|
37
37
|
* @returns {ReturnType<typeof pipe.onEnvelope>} 取消订阅
|
|
38
38
|
*/
|
|
39
|
-
onEnvelope: (...
|
|
39
|
+
onEnvelope: (...callArguments) => pipe.onEnvelope(...callArguments),
|
|
40
40
|
/**
|
|
41
|
-
* @param {...any}
|
|
41
|
+
* @param {...any} callArguments 透传 onDown
|
|
42
42
|
* @returns {ReturnType<typeof pipe.onDown>} 取消订阅
|
|
43
43
|
*/
|
|
44
|
-
onDown: (...
|
|
44
|
+
onDown: (...callArguments) => pipe.onDown(...callArguments),
|
|
45
45
|
/**
|
|
46
|
-
* @param {...any}
|
|
46
|
+
* @param {...any} callArguments 透传 close
|
|
47
47
|
* @returns {ReturnType<typeof pipe.close>} 关闭完成
|
|
48
48
|
*/
|
|
49
|
-
close: (...
|
|
49
|
+
close: (...callArguments) => pipe.close(...callArguments),
|
|
50
50
|
/** @returns {ReturnType<typeof pipe.stats>} 运行时统计 */
|
|
51
51
|
stats: () => pipe.stats(),
|
|
52
52
|
...extras,
|
|
@@ -207,9 +207,9 @@ export function createBleGattLinkProvider() {
|
|
|
207
207
|
* @returns {void}
|
|
208
208
|
*/
|
|
209
209
|
onWriteRequest(_connection, data, _offset, _withoutResponse, callback) {
|
|
210
|
-
const
|
|
211
|
-
if (sessionInbound) sessionInbound(
|
|
212
|
-
else void acceptPeripheralWrite(
|
|
210
|
+
const buffer = Buffer.from(data)
|
|
211
|
+
if (sessionInbound) sessionInbound(buffer)
|
|
212
|
+
else void acceptPeripheralWrite(buffer).catch(() => { })
|
|
213
213
|
callback(bleno.Characteristic.RESULT_SUCCESS)
|
|
214
214
|
},
|
|
215
215
|
})
|
|
@@ -228,12 +228,12 @@ export function createBleGattLinkProvider() {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
|
-
* @param {Buffer}
|
|
231
|
+
* @param {Buffer} buffer 入站写(首包应为 link-open)
|
|
232
232
|
* @returns {Promise<void>}
|
|
233
233
|
*/
|
|
234
|
-
async function acceptPeripheralWrite(
|
|
234
|
+
async function acceptPeripheralWrite(buffer) {
|
|
235
235
|
if (activeInbound || acceptInflight || !onInbound || !localIdentity) return
|
|
236
|
-
const opened = parseLinkOpen(
|
|
236
|
+
const opened = parseLinkOpen(buffer)
|
|
237
237
|
if (!opened) return
|
|
238
238
|
acceptInflight = true
|
|
239
239
|
try {
|
|
@@ -3,13 +3,50 @@ import { randomBytes } from 'node:crypto'
|
|
|
3
3
|
import net from 'node:net'
|
|
4
4
|
|
|
5
5
|
import { normalizeHex64 } from '../../core/hexIds.mjs'
|
|
6
|
-
import { getLanPeerHint } from '../../discovery/lan_peer_hints.mjs'
|
|
6
|
+
import { getLanPeerHint, listLanPeerHints } from '../../discovery/lan_peer_hints.mjs'
|
|
7
7
|
import { asLinkHandle } from '../pipe.mjs'
|
|
8
8
|
|
|
9
9
|
import { LINK_LEVEL_LAN_TCP } from './levels.mjs'
|
|
10
10
|
import { buildLinkOpen, createLinkIdBoundPipe, parseLinkOpen } from './link_id_pipe.mjs'
|
|
11
11
|
|
|
12
12
|
const MAX_FRAME_BYTES = 1 << 20
|
|
13
|
+
/** LAN TCP 单 endpoint 连接超时。 */
|
|
14
|
+
const LAN_TCP_CONNECT_TIMEOUT_MS = 3_000
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} host 目标 host
|
|
18
|
+
* @param {number} port 目标 port
|
|
19
|
+
* @param {number} [timeoutMs=LAN_TCP_CONNECT_TIMEOUT_MS] 超时
|
|
20
|
+
* @returns {Promise<import('node:net').Socket>} 已连接 socket
|
|
21
|
+
*/
|
|
22
|
+
function connectTcp(host, port, timeoutMs = LAN_TCP_CONNECT_TIMEOUT_MS) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const conn = net.createConnection({ host, port })
|
|
25
|
+
const timer = setTimeout(() => {
|
|
26
|
+
conn.destroy()
|
|
27
|
+
reject(new Error(`p2p: lan_tcp connect timeout ${host}:${port}`))
|
|
28
|
+
}, timeoutMs)
|
|
29
|
+
/**
|
|
30
|
+
* @param {Error} error 连接错误
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
const onError = error => {
|
|
34
|
+
clearTimeout(timer)
|
|
35
|
+
conn.off('connect', onConnect)
|
|
36
|
+
reject(error)
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @returns {void}
|
|
40
|
+
*/
|
|
41
|
+
function onConnect() {
|
|
42
|
+
clearTimeout(timer)
|
|
43
|
+
conn.off('error', onError)
|
|
44
|
+
resolve(conn)
|
|
45
|
+
}
|
|
46
|
+
conn.once('error', onError)
|
|
47
|
+
conn.once('connect', onConnect)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
13
50
|
|
|
14
51
|
/**
|
|
15
52
|
* 在 socket 上挂 length-prefix 编解码(u32be + payload)。
|
|
@@ -168,41 +205,33 @@ async function openTcpPipe(options) {
|
|
|
168
205
|
*/
|
|
169
206
|
async function dialLanTcp(options) {
|
|
170
207
|
const remoteNodeHash = normalizeHex64(options.nodeHash)
|
|
171
|
-
const
|
|
172
|
-
if (!
|
|
208
|
+
const hints = listLanPeerHints(remoteNodeHash)
|
|
209
|
+
if (!hints.length) throw new Error('p2p: lan_tcp no peer hint')
|
|
173
210
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
211
|
+
let lastError = null
|
|
212
|
+
for (const hint of hints) {
|
|
213
|
+
/** @type {import('node:net').Socket | null} */
|
|
214
|
+
let socket = null
|
|
215
|
+
try {
|
|
216
|
+
socket = await connectTcp(hint.host, hint.port)
|
|
217
|
+
const link = await openTcpPipe({
|
|
218
|
+
initiator: true,
|
|
219
|
+
linkId: randomBytes(32).toString('hex'),
|
|
220
|
+
nodeHash: remoteNodeHash,
|
|
221
|
+
localIdentity: options.localIdentity,
|
|
222
|
+
socket,
|
|
223
|
+
host: hint.host,
|
|
224
|
+
port: hint.port,
|
|
225
|
+
})
|
|
226
|
+
await link.ready
|
|
227
|
+
return link
|
|
183
228
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
function onConnect() {
|
|
188
|
-
conn.off('error', onError)
|
|
189
|
-
resolve(conn)
|
|
229
|
+
catch (error) {
|
|
230
|
+
lastError = error
|
|
231
|
+
try { socket?.destroy() } catch { /* ignore */ }
|
|
190
232
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
const link = await openTcpPipe({
|
|
196
|
-
initiator: true,
|
|
197
|
-
linkId: randomBytes(32).toString('hex'),
|
|
198
|
-
nodeHash: remoteNodeHash,
|
|
199
|
-
localIdentity: options.localIdentity,
|
|
200
|
-
socket,
|
|
201
|
-
host: hint.host,
|
|
202
|
-
port: hint.port,
|
|
203
|
-
})
|
|
204
|
-
await link.ready
|
|
205
|
-
return link
|
|
233
|
+
}
|
|
234
|
+
throw lastError || new Error('p2p: lan_tcp dial failed')
|
|
206
235
|
}
|
|
207
236
|
|
|
208
237
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSignalingRuntimeConfig } from '../../node/instance.mjs'
|
|
2
|
+
import { nodeDebug } from '../../node/log.mjs'
|
|
2
3
|
import { ms } from '../../utils/duration.mjs'
|
|
3
4
|
import { createLruMap } from '../../utils/lru.mjs'
|
|
4
5
|
import {
|
|
@@ -43,8 +44,9 @@ export async function canUseWebRtcLink() {
|
|
|
43
44
|
await loadNodeRtcPolyfill()
|
|
44
45
|
cachedAvailable = true
|
|
45
46
|
}
|
|
46
|
-
catch {
|
|
47
|
+
catch (error) {
|
|
47
48
|
cachedAvailable = false
|
|
49
|
+
nodeDebug('p2p:webrtc unavailable', { err: formatErrorReason(error) })
|
|
48
50
|
}
|
|
49
51
|
return cachedAvailable
|
|
50
52
|
}
|
package/link/rtc.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import process from 'node:process'
|
|
|
2
2
|
|
|
3
3
|
import { toBytes } from '../core/bytes_codec.mjs'
|
|
4
4
|
import { getSignalingRuntimeConfig } from '../node/instance.mjs'
|
|
5
|
-
import {
|
|
5
|
+
import { wrapRtcPeerConnectionForIceLocalHostname } from '../transport/rtc_ice_local_hostname.mjs'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 注册进程退出时销毁 libdatachannel 全部原生资源(仅一次)。
|
|
@@ -20,9 +20,9 @@ process.on('exit', () => {
|
|
|
20
20
|
*/
|
|
21
21
|
export async function loadNodeRtcPolyfill() {
|
|
22
22
|
const mod = await import('node-datachannel/polyfill')
|
|
23
|
-
const {
|
|
23
|
+
const { iceLocalHostnamePolicy } = getSignalingRuntimeConfig()
|
|
24
24
|
return {
|
|
25
|
-
RTCPeerConnection:
|
|
25
|
+
RTCPeerConnection: wrapRtcPeerConnectionForIceLocalHostname(mod.RTCPeerConnection, mod.RTCIceCandidate, iceLocalHostnamePolicy),
|
|
26
26
|
RTCIceCandidate: mod.RTCIceCandidate,
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -54,8 +54,8 @@ export async function dispatchMailboxRecordsToConsumers(username, records) {
|
|
|
54
54
|
const ids = await handler(username, scoped)
|
|
55
55
|
for (const id of ids || []) delivered.add(String(id))
|
|
56
56
|
}
|
|
57
|
-
catch (
|
|
58
|
-
console.error('mailbox: consumer batch failed, retry per record',
|
|
57
|
+
catch (error) {
|
|
58
|
+
console.error('mailbox: consumer batch failed, retry per record', error)
|
|
59
59
|
for (const row of scoped)
|
|
60
60
|
try {
|
|
61
61
|
const ids = await handler(username, [row])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { normalizeHex64 } from '../core/hexIds.mjs'
|
|
2
2
|
import { getNodeTransportSettings, getNodeHash } from '../node/identity.mjs'
|
|
3
|
-
import {
|
|
3
|
+
import { activeLinkRoster, deliverToUserRoomPeers } from '../transport/user_room.mjs'
|
|
4
4
|
import { DEFAULT_TRUST_GRAPH_OWNER, requireTrustGraphProvider } from '../trust_graph/registry.mjs'
|
|
5
5
|
|
|
6
6
|
import { allowMailboxRelayForTier } from './importance.mjs'
|
|
@@ -23,9 +23,9 @@ import {
|
|
|
23
23
|
* @returns {Promise<string | null>} 已验证的 remote nodeHash
|
|
24
24
|
*/
|
|
25
25
|
async function resolveRemoteNodeHashForPeer(username, peerId) {
|
|
26
|
+
void username
|
|
26
27
|
if (!peerId) return null
|
|
27
|
-
const
|
|
28
|
-
const entry = slot?.getRoster()?.find(row => row.peerId === peerId)
|
|
28
|
+
const entry = activeLinkRoster().find(row => row.peerId === peerId)
|
|
29
29
|
const remote = entry?.remoteNodeHash?.trim().toLowerCase()
|
|
30
30
|
return remote || null
|
|
31
31
|
}
|
|
@@ -51,9 +51,9 @@ async function resolveRelayHopForIngress(record) {
|
|
|
51
51
|
* @returns {Promise<{ maxHop: number, relayFanoutTrusted: number, relayFanoutNormal: number, wantFanout: number, batterySaver: boolean }>} 按在线 peer 数缩放的路由
|
|
52
52
|
*/
|
|
53
53
|
async function resolveRouting(username) {
|
|
54
|
+
void username
|
|
54
55
|
const { batterySaver, mailbox } = getNodeTransportSettings()
|
|
55
|
-
const
|
|
56
|
-
const peerCount = slot?.getRoster()?.length ?? 0
|
|
56
|
+
const peerCount = activeLinkRoster().length
|
|
57
57
|
return resolveMailboxRoutingForPeerCount(peerCount, mailbox, batterySaver)
|
|
58
58
|
}
|
|
59
59
|
|