@decentnetwork/peer 0.1.103 → 0.1.105
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/dist/compat/filetransfer.js +14 -5
- package/dist/peer.js +49 -3
- package/package.json +1 -1
|
@@ -239,17 +239,23 @@ export class FileTransferManager {
|
|
|
239
239
|
}
|
|
240
240
|
return inner;
|
|
241
241
|
}
|
|
242
|
-
#freeNumber(friendId) {
|
|
242
|
+
#freeNumber(friendId, preferred = 0) {
|
|
243
243
|
const inner = this.#map(this.#sending, friendId);
|
|
244
|
-
|
|
244
|
+
// Start at a content-derived slot. Older receivers retain an incomplete
|
|
245
|
+
// receive slot in memory until it finishes or is killed. After the sender
|
|
246
|
+
// restarts, always reusing slot 0 makes a DIFFERENT new file look like a
|
|
247
|
+
// retransmitted offer for that stale slot, so the receiver silently ACKs
|
|
248
|
+
// the old file and the new UI stays at 0%. The same content keeps the same
|
|
249
|
+
// preferred slot (resume), while different content naturally avoids stale
|
|
250
|
+
// collisions; circular search still supports concurrent transfers.
|
|
251
|
+
for (let n = 0; n < MAX_CONCURRENT_FILE_PIPES; n++) {
|
|
252
|
+
const i = (preferred + n) % MAX_CONCURRENT_FILE_PIPES;
|
|
245
253
|
if (!inner.has(i))
|
|
246
254
|
return i;
|
|
255
|
+
}
|
|
247
256
|
return -1;
|
|
248
257
|
}
|
|
249
258
|
sendFile(friendId, data, opts) {
|
|
250
|
-
const fileNumber = this.#freeNumber(friendId);
|
|
251
|
-
if (fileNumber < 0)
|
|
252
|
-
return null;
|
|
253
259
|
// Content-addressed fileId (sha256): re-sending the SAME file after a
|
|
254
260
|
// restart or a dropped peer yields the SAME id, so the receiver can match
|
|
255
261
|
// it against its persisted partial and resume from where it left off — the
|
|
@@ -257,6 +263,9 @@ export class FileTransferManager {
|
|
|
257
263
|
const fileId = data.length > 0
|
|
258
264
|
? new Uint8Array(createHash("sha256").update(data).digest()).slice(0, FILE_ID_LENGTH)
|
|
259
265
|
: randomBytes(FILE_ID_LENGTH);
|
|
266
|
+
const fileNumber = this.#freeNumber(friendId, fileId[0] ?? 0);
|
|
267
|
+
if (fileNumber < 0)
|
|
268
|
+
return null;
|
|
260
269
|
const req = encodeFileSendRequest(fileNumber, opts.kind ?? FILEKIND.DATA, BigInt(data.length), fileId, opts.name);
|
|
261
270
|
const lanPath = this.isLanPath(friendId);
|
|
262
271
|
const st = {
|
package/dist/peer.js
CHANGED
|
@@ -210,8 +210,22 @@ const RECV_REQUEST_MIN_INTERVAL_MS = 200;
|
|
|
210
210
|
export class Peer {
|
|
211
211
|
#opts;
|
|
212
212
|
#events = new EventEmitter();
|
|
213
|
+
/** While negotiating a non-LAN file, temporarily stop sending this friend
|
|
214
|
+
* UDP control traffic. Peer <=0.1.94 routes FILE_CONTROL UDP-only whenever
|
|
215
|
+
* it has seen recent inbound UDP, even when that public/NAT path is one-way.
|
|
216
|
+
* Repeated relay-only offers let that 4s freshness expire, after which the
|
|
217
|
+
* old peer returns ACCEPT/ACK over its reliable TCP relay. */
|
|
218
|
+
#fileRelayNegotiationUntil = new Map();
|
|
213
219
|
// Toxcore-standard file transfer (wire-compatible with native Carrier/toxcore).
|
|
214
|
-
#fileTransfer = new FileTransferManager((friendId, packetId, payload) =>
|
|
220
|
+
#fileTransfer = new FileTransferManager((friendId, packetId, payload) => {
|
|
221
|
+
if (packetId === PACKET_ID_FILE_SENDREQUEST) {
|
|
222
|
+
const session = this.#friendSessions.get(friendId);
|
|
223
|
+
const lan = !!session?.lanRemoteHost && session.remote?.host === session.lanRemoteHost;
|
|
224
|
+
if (!lan)
|
|
225
|
+
this.#fileRelayNegotiationUntil.set(friendId, Date.now() + 6_000);
|
|
226
|
+
}
|
|
227
|
+
return this.#sendMessengerPacket(friendId, packetId, payload);
|
|
228
|
+
}, (event, payload) => { this.#events.emit(event, payload); }, (friendId) => {
|
|
215
229
|
const session = this.#friendSessions.get(friendId);
|
|
216
230
|
return !!session?.lanRemoteHost && session.remote?.host === session.lanRemoteHost;
|
|
217
231
|
});
|
|
@@ -604,6 +618,7 @@ export class Peer {
|
|
|
604
618
|
clearTimeout(timer);
|
|
605
619
|
this.#profileRetryTimers.clear();
|
|
606
620
|
this.#profileRetryAttempts.clear();
|
|
621
|
+
this.#fileRelayNegotiationUntil.clear();
|
|
607
622
|
if (this.#expressPollTimer) {
|
|
608
623
|
clearInterval(this.#expressPollTimer);
|
|
609
624
|
this.#expressPollTimer = undefined;
|
|
@@ -980,6 +995,7 @@ export class Peer {
|
|
|
980
995
|
}
|
|
981
996
|
const existed = this.#friends.delete(friendId);
|
|
982
997
|
this.#friendSessions.delete(friendId);
|
|
998
|
+
this.#fileRelayNegotiationUntil.delete(friendId);
|
|
983
999
|
this.#pendingFriendRequests.delete(friendId);
|
|
984
1000
|
this.#cookieRetryCount.delete(friendId);
|
|
985
1001
|
this.#lastCookieSentKey.delete(friendId);
|
|
@@ -2147,6 +2163,21 @@ export class Peer {
|
|
|
2147
2163
|
// syscall (that was the CCTV CPU regression) — safe at CCTV packet
|
|
2148
2164
|
// rates.
|
|
2149
2165
|
this.#adoptRemote(state, remote.address, remote.port);
|
|
2166
|
+
// A successfully decrypted packet arriving from a private address in
|
|
2167
|
+
// one of our physical subnets is stronger LAN proof than an endpoint
|
|
2168
|
+
// hint: it demonstrates that this exact host:port already reaches us.
|
|
2169
|
+
// Lock it immediately. Without this, duplicate public/relay copies
|
|
2170
|
+
// arriving a moment later overwrite `remote`, so file DATA travels on
|
|
2171
|
+
// the LAN but ACKs hairpin through the public Internet (observed as
|
|
2172
|
+
// ~38KB/s between mac-dev and WSL on the same 10.0.0.0/24).
|
|
2173
|
+
if (!state.lanRemoteHost &&
|
|
2174
|
+
isPrivateAddress(remote.address) &&
|
|
2175
|
+
!isCgnatAddress(remote.address) &&
|
|
2176
|
+
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(remote.address, s))) {
|
|
2177
|
+
state.lanRemoteHost = remote.address;
|
|
2178
|
+
this.#rememberEndpointCandidate(state, remote.address, remote.port);
|
|
2179
|
+
this.#debugLog(`lan_lock_confirmed friend=${friendId} host=${remote.address}:${remote.port} (decrypted UDP source)`);
|
|
2180
|
+
}
|
|
2150
2181
|
if (!state.lastUdpRecvMs) {
|
|
2151
2182
|
this.#debugLog(`udp_confirmed friend=${friendId} via=${remote.address}:${remote.port} (direct UDP path live)`);
|
|
2152
2183
|
}
|
|
@@ -3345,6 +3376,11 @@ export class Peer {
|
|
|
3345
3376
|
* buffered packets through the exact same dispatch. `kind` is the first
|
|
3346
3377
|
* payload byte (never undefined here — the caller defaults it). */
|
|
3347
3378
|
#deliverLosslessPayload(friendId, state, kind, inner, remote, packetNumber = 0) {
|
|
3379
|
+
// Any file response proves the receiver can hear this session. Release the
|
|
3380
|
+
// old-peer relay-only negotiation guard before the data pump chooses its
|
|
3381
|
+
// normal LAN/reliable-public path.
|
|
3382
|
+
if (kind === PACKET_ID_FILE_CONTROL)
|
|
3383
|
+
this.#fileRelayNegotiationUntil.delete(friendId);
|
|
3348
3384
|
// Toxcore file transfer (FILE_SENDREQUEST/CONTROL/DATA = 80/81/82).
|
|
3349
3385
|
if (this.#fileTransfer.handlePacket(friendId, kind, inner))
|
|
3350
3386
|
return;
|
|
@@ -3964,6 +4000,8 @@ export class Peer {
|
|
|
3964
4000
|
const udpFresh = !!realUdpRemote &&
|
|
3965
4001
|
s?.lastUdpRecvMs !== undefined &&
|
|
3966
4002
|
Date.now() - s.lastUdpRecvMs < 4_000;
|
|
4003
|
+
const forceFileRelay = (this.#fileRelayNegotiationUntil.get(friendId) ?? 0) > Date.now() &&
|
|
4004
|
+
!(s?.lanRemoteHost && s.remote?.host === s.lanRemoteHost);
|
|
3967
4005
|
// A freshly-received UDP packet proves only the peer -> us direction.
|
|
3968
4006
|
// That is enough for lossy IP/video bulk, whose inner TCP stream can
|
|
3969
4007
|
// recover a dropped packet, but not for reliable file data: returning
|
|
@@ -3980,7 +4018,8 @@ export class Peer {
|
|
|
3980
4018
|
// lastUdpRecvMs). This converts a multi-second blackout into a few
|
|
3981
4019
|
// seconds of higher relay latency. Control packets always probe UDP
|
|
3982
4020
|
// (to keep the NAT mapping warm) so we still try UDP for them.
|
|
3983
|
-
const tryUdp =
|
|
4021
|
+
const tryUdp = !forceFileRelay &&
|
|
4022
|
+
realUdpRemote &&
|
|
3984
4023
|
(isBulkData ? udpFresh && fileUdpOnlyConfirmed : true);
|
|
3985
4024
|
if (tryUdp && s?.remote) {
|
|
3986
4025
|
try {
|
|
@@ -4535,10 +4574,17 @@ export class Peer {
|
|
|
4535
4574
|
if (payload.length >= 18) {
|
|
4536
4575
|
const lanHost = `${payload[12]}.${payload[13]}.${payload[14]}.${payload[15]}`;
|
|
4537
4576
|
const lanPort = ((payload[16] << 8) | payload[17]) >>> 0;
|
|
4577
|
+
// WSL sees only its 172.x virtual NIC, not the Windows host's physical
|
|
4578
|
+
// 10.x subnet. In that topology a peer on the same real LAN is still
|
|
4579
|
+
// safely identifiable because its server-reflexive address equals ours.
|
|
4580
|
+
// Permit the advertised private candidate when both peers share the same
|
|
4581
|
+
// public NAT; if it is a CGNAT/private collision and never validates, the
|
|
4582
|
+
// normal 12s stale-LAN-lock breaker restores the public/relay path.
|
|
4583
|
+
const samePublicNat = !!this.#srflxCache?.addr.host && this.#srflxCache.addr.host === host;
|
|
4538
4584
|
const sameLan = lanPort !== 0 &&
|
|
4539
4585
|
isPrivateAddress(lanHost) &&
|
|
4540
4586
|
!isCgnatAddress(lanHost) &&
|
|
4541
|
-
getPhysicalLanSubnets().some((s) => isInIpv4Subnet(lanHost, s)) &&
|
|
4587
|
+
(getPhysicalLanSubnets().some((s) => isInIpv4Subnet(lanHost, s)) || samePublicNat) &&
|
|
4542
4588
|
!(getPhysicalLanAddresses().includes(lanHost) && this.#udp.localPort() === lanPort);
|
|
4543
4589
|
if (sameLan) {
|
|
4544
4590
|
this.#rememberEndpointCandidate(session, lanHost, lanPort);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.105",
|
|
4
4
|
"description": "Pure TypeScript port of Elastos Carrier (toxcore-derived) P2P messaging. DHT, onion routing, TCP relay, FlatBuffers app payloads, Express offline relay. Wire-compatible with iOS Beagle and the Carrier C SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|