@decentnetwork/peer 0.1.98 → 0.1.100
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 +18 -2
- package/dist/peer.js +72 -4
- package/package.json +1 -1
|
@@ -108,6 +108,8 @@ const LAN_SAMPLE_MAX_GAIN = 1.25;
|
|
|
108
108
|
const LAN_STARTUP_MAX_GAIN = 1.15;
|
|
109
109
|
const LAN_STARTUP_DELIVERY_GAIN = 1.5; // never probe above 1.5x recently delivered goodput
|
|
110
110
|
const LAN_STARTUP_QUEUE_MS = 500; // unmistakable raw queue spike; smaller app-level RTT outliers are normal on Windows
|
|
111
|
+
const LAN_STARTUP_HARD_QUEUE_MS = 2000; // one multi-second queue sample is unsafe
|
|
112
|
+
const LAN_STARTUP_QUEUE_ROUNDS = 2; // tolerate one scheduler/session-handover outlier on LAN
|
|
111
113
|
const LAN_STARTUP_KEEPUP = 0.70; // delivered goodput below 70% of pace means the receiver is no longer keeping up
|
|
112
114
|
const LAN_STARTUP_SLOW_ROUNDS = 5; // require sustained saturation across Windows scheduler/ACK jitter
|
|
113
115
|
// FILE ACKs are coalesced and handled by the remote JS event loop. On Windows
|
|
@@ -250,7 +252,7 @@ export class FileTransferManager {
|
|
|
250
252
|
cwnd: CWND_INIT_CHUNKS, stalls: 0, parityHighBlock: -1, lastLossMs: 0, lossEwma: 0,
|
|
251
253
|
minRttMs: Infinity, srttMs: 0, probeOffset: -1, probeSentMs: 0,
|
|
252
254
|
paceRateBps: PACE_INIT_BPS, paceTokens: 0, lastPaceMs: nowMs(),
|
|
253
|
-
btlBwBps: 0, bwSamples: [], fullBwRounds: 0, bbrPhase: 0, roundCount: 0,
|
|
255
|
+
btlBwBps: 0, bwSamples: [], fullBwRounds: 0, startupQueueRounds: 0, bbrPhase: 0, roundCount: 0,
|
|
254
256
|
rateMarkMs: nowMs(), rateMarkAcked: 0, lowRttCount: 0, lowRttMinMs: Infinity, lastCcLogMs: 0,
|
|
255
257
|
lanPath: this.isLanPath(friendId),
|
|
256
258
|
};
|
|
@@ -787,6 +789,7 @@ export class FileTransferManager {
|
|
|
787
789
|
// physical-LAN endpoint. Restart STARTUP from the measured rate.
|
|
788
790
|
st.bbrPhase = 0;
|
|
789
791
|
st.fullBwRounds = 0;
|
|
792
|
+
st.startupQueueRounds = 0;
|
|
790
793
|
}
|
|
791
794
|
st.lanPath = lanNow;
|
|
792
795
|
if (!lanNow) {
|
|
@@ -817,7 +820,19 @@ export class FileTransferManager {
|
|
|
817
820
|
st.fullBwRounds++;
|
|
818
821
|
}
|
|
819
822
|
const rawQueue = st.minRttMs !== Infinity ? acceptedRtt - st.minRttMs : 0;
|
|
820
|
-
|
|
823
|
+
if (rawQueue > LAN_STARTUP_QUEUE_MS) {
|
|
824
|
+
st.startupQueueRounds++;
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
st.startupQueueRounds = 0;
|
|
828
|
+
}
|
|
829
|
+
// File acceptance, an event-loop pause, or a relay-to-LAN
|
|
830
|
+
// handover can produce one 500-1000 ms ACK on Windows/macOS.
|
|
831
|
+
// One such sample is not evidence that a paced LAN is full.
|
|
832
|
+
// Require persistence, while retaining an immediate brake for
|
|
833
|
+
// a genuinely dangerous multi-second queue.
|
|
834
|
+
const startupOvershot = rawQueue > LAN_STARTUP_HARD_QUEUE_MS ||
|
|
835
|
+
st.startupQueueRounds >= LAN_STARTUP_QUEUE_ROUNDS;
|
|
821
836
|
const startupUnderDelivering = st.fullBwRounds >= LAN_STARTUP_SLOW_ROUNDS;
|
|
822
837
|
if (startupUnderDelivering || startupOvershot) {
|
|
823
838
|
st.bbrPhase = 2;
|
|
@@ -828,6 +843,7 @@ export class FileTransferManager {
|
|
|
828
843
|
// the sender continue flooding for ~2 s and collapse cwnd.
|
|
829
844
|
st.bwSamples = measuredRate > 0 ? [measuredRate] : [];
|
|
830
845
|
st.btlBwBps = measuredRate > 0 ? measuredRate : PACE_INIT_BPS;
|
|
846
|
+
st.startupQueueRounds = 0;
|
|
831
847
|
st.paceRateBps = Math.max(PACE_INIT_BPS, Math.min(st.paceRateBps * 0.8, st.btlBwBps * 1.1));
|
|
832
848
|
}
|
|
833
849
|
else if (st.bbrPhase === 0) {
|
package/dist/peer.js
CHANGED
|
@@ -1893,10 +1893,33 @@ export class Peer {
|
|
|
1893
1893
|
// breaker" churned because it accepted new keys with NO such evidence).
|
|
1894
1894
|
const peerReKeyed = state.undecryptableRecvMs !== undefined &&
|
|
1895
1895
|
state.undecryptableRecvMs > (state.lastPingRecvMs ?? 0);
|
|
1896
|
-
|
|
1897
|
-
|
|
1896
|
+
const now = Date.now();
|
|
1897
|
+
const sameRehsKey = state.rehsSessionPublicKey !== undefined &&
|
|
1898
|
+
bytesEqual(state.rehsSessionPublicKey, hs.sessionPublicKey);
|
|
1899
|
+
const rehsWindowExpired = state.rehsLastMs === undefined ||
|
|
1900
|
+
now - state.rehsLastMs > REHS_WINDOW_MS;
|
|
1901
|
+
if (!sameRehsKey || rehsWindowExpired) {
|
|
1902
|
+
state.rehsSessionPublicKey = hs.sessionPublicKey.slice();
|
|
1903
|
+
state.rehsCount = 1;
|
|
1904
|
+
state.rehsFirstMs = now;
|
|
1905
|
+
}
|
|
1906
|
+
else {
|
|
1907
|
+
state.rehsCount = (state.rehsCount ?? 0) + 1;
|
|
1908
|
+
}
|
|
1909
|
+
state.rehsLastMs = now;
|
|
1910
|
+
const rehsElapsed = now - (state.rehsFirstMs ?? now);
|
|
1911
|
+
const persistentRehs = (state.rehsCount ?? 0) >= REHS_ACCEPT_COUNT &&
|
|
1912
|
+
rehsElapsed >= REHS_ACCEPT_MS &&
|
|
1913
|
+
rehsElapsed <= REHS_WINDOW_MS;
|
|
1914
|
+
if (sinceEstablished > 1000 && currentSessionAlive && !peerReKeyed && !persistentRehs) {
|
|
1915
|
+
this.#debugVerboseLog(`hs_recv ignored friend=${friendId} (new session pubkey on live established session, ` +
|
|
1916
|
+
`${sinceEstablished}ms after establish, attempts=${state.rehsCount ?? 0})`);
|
|
1898
1917
|
return;
|
|
1899
1918
|
}
|
|
1919
|
+
if (persistentRehs) {
|
|
1920
|
+
this.#debugLog(`hs_recv accepting persistent re-handshake friend=${friendId} ` +
|
|
1921
|
+
`(attempts=${state.rehsCount}, elapsed=${rehsElapsed}ms)`);
|
|
1922
|
+
}
|
|
1900
1923
|
if (peerReKeyed) {
|
|
1901
1924
|
this.#debugLog(`hs_recv accepting re-handshake friend=${friendId} (peer re-keyed: undecryptable data since last good packet)`);
|
|
1902
1925
|
}
|
|
@@ -1908,6 +1931,10 @@ export class Peer {
|
|
|
1908
1931
|
state.sessionSharedKey = nacl.box.before(hs.sessionPublicKey, state.ourSessionKeyPair.secretKey);
|
|
1909
1932
|
state.established = true;
|
|
1910
1933
|
state.sessionEstablishedAtMs = Date.now();
|
|
1934
|
+
state.rehsCount = undefined;
|
|
1935
|
+
state.rehsFirstMs = undefined;
|
|
1936
|
+
state.rehsLastMs = undefined;
|
|
1937
|
+
state.rehsSessionPublicKey = undefined;
|
|
1911
1938
|
// Fresh session up — reset the higher-pubkey defer window for the
|
|
1912
1939
|
// next outage (see #initiateSession).
|
|
1913
1940
|
this.#initiateDeferSinceMs.delete(friendId);
|
|
@@ -1936,7 +1963,13 @@ export class Peer {
|
|
|
1936
1963
|
state.sendPacketNumber ??= 0;
|
|
1937
1964
|
state.receiveBufferStart ??= 0;
|
|
1938
1965
|
}
|
|
1939
|
-
|
|
1966
|
+
// A handshake authenticates identities and proposes keys, but it does not
|
|
1967
|
+
// prove that CRYPTO_DATA encrypted under those keys works in both
|
|
1968
|
+
// directions. Keep a fresh session "unproven" until the first packet is
|
|
1969
|
+
// actually decrypted; otherwise a half-open session receives the 180s
|
|
1970
|
+
// proven-session blackout grace instead of the 32s recovery deadline.
|
|
1971
|
+
if (isNewSession)
|
|
1972
|
+
state.lastPingRecvMs = undefined;
|
|
1940
1973
|
state.pendingEcho = undefined;
|
|
1941
1974
|
state.pendingCookiePeerDhtPublicKey = undefined;
|
|
1942
1975
|
this.#debugLog(`hs_recv friend=${friendId} initiated=${weInitiated ? 1 : 0} remote=${remote.address}:${remote.port}`);
|
|
@@ -2084,6 +2117,13 @@ export class Peer {
|
|
|
2084
2117
|
// stays unset → udpFresh false → bulk data (files) never ride the LAN
|
|
2085
2118
|
// even when LAN UDP is flowing both ways (the snoopy slow-file bug).
|
|
2086
2119
|
state.lastPingRecvMs = Date.now();
|
|
2120
|
+
// Any successfully decrypted packet proves the current session is
|
|
2121
|
+
// healthy. Forget rejected new-key proposals so late/replayed
|
|
2122
|
+
// handshakes can never accumulate across healthy traffic.
|
|
2123
|
+
state.rehsCount = undefined;
|
|
2124
|
+
state.rehsFirstMs = undefined;
|
|
2125
|
+
state.rehsLastMs = undefined;
|
|
2126
|
+
state.rehsSessionPublicKey = undefined;
|
|
2087
2127
|
if (this.#remoteIsTcp(remote)) {
|
|
2088
2128
|
state.hasTcpRoute = true;
|
|
2089
2129
|
}
|
|
@@ -2932,6 +2972,18 @@ export class Peer {
|
|
|
2932
2972
|
// sessions a long grace so a blackout doesn't churn them via re-handshake;
|
|
2933
2973
|
// unproven sessions still time out fast to self-heal a wedged handshake.
|
|
2934
2974
|
const proven = session.lastPingRecvMs !== undefined;
|
|
2975
|
+
// A proven session normally gets a long blackout grace: if the network
|
|
2976
|
+
// disappears, keeping the known-good keys avoids unnecessary churn.
|
|
2977
|
+
// A restarted peer is different. Once our last good decrypt goes quiet
|
|
2978
|
+
// while fresh packets from that same friend keep failing authentication,
|
|
2979
|
+
// the transport is alive but the keys are not. Waiting the full proven
|
|
2980
|
+
// timeout turns every peer restart into a multi-minute wedge. Require a
|
|
2981
|
+
// sustained good-packet silence plus recent desync evidence so a single
|
|
2982
|
+
// late relay duplicate can never tear down a healthy session.
|
|
2983
|
+
const recentUndecryptable = session.undecryptableRecvMs !== undefined &&
|
|
2984
|
+
session.undecryptableRecvMs > lastAlive &&
|
|
2985
|
+
now - session.undecryptableRecvMs <= REINIT_STUCK_MS;
|
|
2986
|
+
const rekeyStuck = recentUndecryptable && silentFor > REINIT_ON_DESYNC_MS;
|
|
2935
2987
|
// A relay-only session (synthetic tcp: remote, no UDP path) whose
|
|
2936
2988
|
// relay route is gone has NO transport at all — nothing can arrive
|
|
2937
2989
|
// and nothing we send leaves. The proven-session blackout grace
|
|
@@ -2942,7 +2994,23 @@ export class Peer {
|
|
|
2942
2994
|
const relayOnly = !session.remote || session.remote.host.startsWith("tcp:");
|
|
2943
2995
|
const pathless = relayOnly && !session.hasTcpRoute;
|
|
2944
2996
|
const deadline = proven && !pathless ? PROVEN_SESSION_HARD_TIMEOUT_MS : FRIEND_TIMEOUT_MS;
|
|
2945
|
-
if (
|
|
2997
|
+
if (rekeyStuck) {
|
|
2998
|
+
this.#debugLog(`session re-key desync for ${friendId} (no good decrypt in ${silentFor}ms; ` +
|
|
2999
|
+
`undecryptable packets still arriving) — tearing down to re-handshake`);
|
|
3000
|
+
this.#friendSessions.delete(friendId);
|
|
3001
|
+
this.#setFriendOffline(friendId);
|
|
3002
|
+
try {
|
|
3003
|
+
const pk = base58ToBytes(friend.pubkey);
|
|
3004
|
+
if (pk.length === 32)
|
|
3005
|
+
this.#tcpRelays?.requestRoute(pk);
|
|
3006
|
+
const dhtPk = this.#friendDhtKeys.get(friendId);
|
|
3007
|
+
if (dhtPk && dhtPk.length === 32)
|
|
3008
|
+
this.#tcpRelays?.requestRoute(dhtPk);
|
|
3009
|
+
}
|
|
3010
|
+
catch { /* skip malformed */ }
|
|
3011
|
+
continue;
|
|
3012
|
+
}
|
|
3013
|
+
else if (proven && !pathless && silentFor > FRIEND_TIMEOUT_MS && silentFor <= deadline) {
|
|
2946
3014
|
// Blackout grace: keep the proven session and keep probing (the
|
|
2947
3015
|
// keepalive + UDP re-punch + relay-keepalive below all still run). Do
|
|
2948
3016
|
// NOT delete or re-handshake — that's what desyncs and churns. The peer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.100",
|
|
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",
|