@decentnetwork/peer 0.1.39 → 0.1.41
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/dht-rpc.d.ts +25 -0
- package/dist/compat/dht-rpc.js +100 -0
- package/dist/peer.js +259 -9
- package/dist/types/peer.d.ts +10 -0
- package/package.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { KeyPair } from "../crypto/keypair.js";
|
|
2
|
+
export declare const NET_PACKET_PING_REQUEST = 0;
|
|
3
|
+
export declare const NET_PACKET_PING_RESPONSE = 1;
|
|
4
|
+
export declare const NET_PACKET_GET_NODES = 2;
|
|
5
|
+
export declare const NET_PACKET_SEND_NODES = 4;
|
|
6
|
+
export type DhtRpcPacket = {
|
|
7
|
+
type: number;
|
|
8
|
+
senderPublicKey: Uint8Array;
|
|
9
|
+
plain: Uint8Array;
|
|
10
|
+
};
|
|
11
|
+
export declare function encodeDhtRpc(type: number, sender: KeyPair, recipientPublicKey: Uint8Array, plain: Uint8Array): Uint8Array;
|
|
12
|
+
export declare function decodeDhtRpc(packet: Uint8Array, ourSecretKey: Uint8Array): DhtRpcPacket | undefined;
|
|
13
|
+
export declare function buildPingPlain(type: number, pingId: Uint8Array): Uint8Array;
|
|
14
|
+
export declare function parsePingPlain(plain: Uint8Array): {
|
|
15
|
+
innerType: number;
|
|
16
|
+
pingId: Uint8Array;
|
|
17
|
+
} | undefined;
|
|
18
|
+
export declare function buildGetNodesPlain(targetPublicKey: Uint8Array, pingId: Uint8Array): Uint8Array;
|
|
19
|
+
export declare function parseGetNodesPlain(plain: Uint8Array): {
|
|
20
|
+
target: Uint8Array;
|
|
21
|
+
pingId: Uint8Array;
|
|
22
|
+
} | undefined;
|
|
23
|
+
export declare function buildSendNodesPlain(packedNodes: Uint8Array, count: number, pingId: Uint8Array): Uint8Array;
|
|
24
|
+
export declare function parseSendNodesNodeBytes(plain: Uint8Array): Uint8Array | undefined;
|
|
25
|
+
export declare function packUdpNodeV4(host: string, port: number, pk: Uint8Array): Uint8Array | undefined;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Classic toxcore DHT request/response packets (DHT.c): ping and get_nodes.
|
|
2
|
+
//
|
|
3
|
+
// These are what make a node *findable* and *discoverable* over UDP — the
|
|
4
|
+
// mechanism native C++/iOS/Android Carrier clients use to locate a friend's
|
|
5
|
+
// live UDP ip:port and net_crypto to it directly (fast), instead of falling
|
|
6
|
+
// back to a TCP relay (slow). Without answering these, a JS peer is invisible
|
|
7
|
+
// in the DHT: a native client's get_nodes search for our key finds nothing,
|
|
8
|
+
// so it can never punch UDP to us. See peer.ts #handleDhtRpc / #doDhtMaintenance.
|
|
9
|
+
//
|
|
10
|
+
// Wire format (dht_create_packet): no recipient key on the wire (unlike the
|
|
11
|
+
// 0x20 CRYPTO_PACKET) — the recipient identifies us by the sender key field.
|
|
12
|
+
// [type(1)] [sender DHT public key(32)] [nonce(24)] [crypto_box(plain)+MAC]
|
|
13
|
+
// Encrypted to the recipient's DHT pk with our DHT sk.
|
|
14
|
+
import nacl from "tweetnacl";
|
|
15
|
+
import { concatBytes, randomBytes } from "../utils/bytes.js";
|
|
16
|
+
export const NET_PACKET_PING_REQUEST = 0x00;
|
|
17
|
+
export const NET_PACKET_PING_RESPONSE = 0x01;
|
|
18
|
+
export const NET_PACKET_GET_NODES = 0x02;
|
|
19
|
+
export const NET_PACKET_SEND_NODES = 0x04;
|
|
20
|
+
const KEY_SIZE = 32;
|
|
21
|
+
const NONCE_SIZE = 24;
|
|
22
|
+
const MAC_SIZE = 16;
|
|
23
|
+
function isDhtRpcType(t) {
|
|
24
|
+
return (t === NET_PACKET_PING_REQUEST ||
|
|
25
|
+
t === NET_PACKET_PING_RESPONSE ||
|
|
26
|
+
t === NET_PACKET_GET_NODES ||
|
|
27
|
+
t === NET_PACKET_SEND_NODES);
|
|
28
|
+
}
|
|
29
|
+
export function encodeDhtRpc(type, sender, recipientPublicKey, plain) {
|
|
30
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
31
|
+
const encrypted = nacl.box(plain, nonce, recipientPublicKey, sender.secretKey);
|
|
32
|
+
return concatBytes([Uint8Array.of(type), sender.publicKey, nonce, encrypted]);
|
|
33
|
+
}
|
|
34
|
+
export function decodeDhtRpc(packet, ourSecretKey) {
|
|
35
|
+
if (packet.length < 1 + KEY_SIZE + NONCE_SIZE + MAC_SIZE)
|
|
36
|
+
return undefined;
|
|
37
|
+
const type = packet[0];
|
|
38
|
+
if (!isDhtRpcType(type))
|
|
39
|
+
return undefined;
|
|
40
|
+
const senderPublicKey = packet.slice(1, 1 + KEY_SIZE);
|
|
41
|
+
const nonce = packet.slice(1 + KEY_SIZE, 1 + KEY_SIZE + NONCE_SIZE);
|
|
42
|
+
const encrypted = packet.slice(1 + KEY_SIZE + NONCE_SIZE);
|
|
43
|
+
const plain = nacl.box.open(encrypted, nonce, senderPublicKey, ourSecretKey);
|
|
44
|
+
if (!plain)
|
|
45
|
+
return undefined;
|
|
46
|
+
return { type, senderPublicKey, plain };
|
|
47
|
+
}
|
|
48
|
+
// ---- plaintext payload builders/parsers ----
|
|
49
|
+
// ping req/resp plain: [type(1)][ping_id(8)]; the type byte is echoed inside
|
|
50
|
+
// (toxcore uses it as a sanity check). ping_id is opaque, echoed by the peer.
|
|
51
|
+
export function buildPingPlain(type, pingId) {
|
|
52
|
+
return concatBytes([Uint8Array.of(type), pingId.slice(0, 8)]);
|
|
53
|
+
}
|
|
54
|
+
export function parsePingPlain(plain) {
|
|
55
|
+
if (plain.length < 1 + 8)
|
|
56
|
+
return undefined;
|
|
57
|
+
return { innerType: plain[0], pingId: plain.slice(1, 9) };
|
|
58
|
+
}
|
|
59
|
+
// get_nodes plain: [target public key(32)][sendback ping_id(8)].
|
|
60
|
+
export function buildGetNodesPlain(targetPublicKey, pingId) {
|
|
61
|
+
return concatBytes([targetPublicKey.slice(0, 32), pingId.slice(0, 8)]);
|
|
62
|
+
}
|
|
63
|
+
export function parseGetNodesPlain(plain) {
|
|
64
|
+
if (plain.length < 32 + 8)
|
|
65
|
+
return undefined;
|
|
66
|
+
return { target: plain.slice(0, 32), pingId: plain.slice(32, 40) };
|
|
67
|
+
}
|
|
68
|
+
// send_nodes plain: [count(1)][packed nodes...][sendback ping_id(8)].
|
|
69
|
+
// (Nodes are parsed by peer.ts parsePackedNodes; we only split off the count
|
|
70
|
+
// prefix and trailing ping_id here.)
|
|
71
|
+
export function buildSendNodesPlain(packedNodes, count, pingId) {
|
|
72
|
+
return concatBytes([Uint8Array.of(count & 0xff), packedNodes, pingId.slice(0, 8)]);
|
|
73
|
+
}
|
|
74
|
+
export function parseSendNodesNodeBytes(plain) {
|
|
75
|
+
if (plain.length < 1 + 8)
|
|
76
|
+
return undefined;
|
|
77
|
+
const count = plain[0];
|
|
78
|
+
if (count === 0)
|
|
79
|
+
return new Uint8Array(0);
|
|
80
|
+
// node region is everything between the count byte and the trailing 8-byte ping_id
|
|
81
|
+
return plain.slice(1, plain.length - 8);
|
|
82
|
+
}
|
|
83
|
+
// packed UDP IPv4 node (toxcore Node_format): [family=2][ip(4)][port BE(2)][pk(32)] = 39 bytes
|
|
84
|
+
export function packUdpNodeV4(host, port, pk) {
|
|
85
|
+
const parts = host.split(".").map((p) => Number.parseInt(p, 10));
|
|
86
|
+
if (parts.length !== 4 || parts.some((n) => !(n >= 0 && n <= 255)))
|
|
87
|
+
return undefined;
|
|
88
|
+
if (pk.length !== 32 || !(port > 0 && port <= 0xffff))
|
|
89
|
+
return undefined;
|
|
90
|
+
const out = new Uint8Array(1 + 4 + 2 + 32);
|
|
91
|
+
out[0] = 2; // UDP_FAMILY_IPV4
|
|
92
|
+
out[1] = parts[0];
|
|
93
|
+
out[2] = parts[1];
|
|
94
|
+
out[3] = parts[2];
|
|
95
|
+
out[4] = parts[3];
|
|
96
|
+
out[5] = (port >> 8) & 0xff;
|
|
97
|
+
out[6] = port & 0xff;
|
|
98
|
+
out.set(pk, 7);
|
|
99
|
+
return out;
|
|
100
|
+
}
|
package/dist/peer.js
CHANGED
|
@@ -8,6 +8,7 @@ import { LegacyBootstrapClient } from "./compat/bootstrap.js";
|
|
|
8
8
|
import { PACKET_TYPE_MESSAGE, PACKET_TYPE_FRIEND_REQUEST, PACKET_TYPE_USERINFO, encodeUserInfoPacket, decodeCarrierPacket, encodeFriendMessagePacket, encodeFriendRequestPacket } from "./compat/packet.js";
|
|
9
9
|
import { NET_PACKET_ONION_ANNOUNCE_RESPONSE, NET_PACKET_ONION_DATA_RESPONSE, ONION_FRIEND_REQUEST_ID, createOnionAnnounceRequest, createOnionDataPacket, createOnionDataRequest, createOnionRequest0, openOnionAnnounceResponse, openOnionDataPacket, openOnionDataResponse } from "./compat/tox-onion.js";
|
|
10
10
|
import { CRYPTO_PACKET_DHTPK, CRYPTO_PACKET_FRIEND_REQ, NET_PACKET_CRYPTO, createToxDhtCryptoRequest, openToxDhtCryptoRequest } from "./compat/tox-dht-crypto.js";
|
|
11
|
+
import { NET_PACKET_PING_REQUEST, NET_PACKET_PING_RESPONSE, NET_PACKET_GET_NODES, NET_PACKET_SEND_NODES, encodeDhtRpc, decodeDhtRpc, buildPingPlain, parsePingPlain, buildGetNodesPlain, parseGetNodesPlain, buildSendNodesPlain, parseSendNodesNodeBytes, packUdpNodeV4 } from "./compat/dht-rpc.js";
|
|
11
12
|
import { NET_PACKET_CRYPTO_DATA, NET_PACKET_CRYPTO_HS, NET_PACKET_COOKIE_REQUEST, NET_PACKET_COOKIE_RESPONSE, createCookieRequest, createCookieResponse, createCryptoDataPacket, createCryptoHandshake, openCookieRequest, openCookieResponse, openCryptoDataPacket, openCryptoHandshake, incrementNonce } from "./compat/net-crypto.js";
|
|
12
13
|
import { LegacyExpressClient } from "./compat/express.js";
|
|
13
14
|
import { TcpRelayPool } from "./compat/tcp-relay-pool.js";
|
|
@@ -44,6 +45,10 @@ const NODE_BLACKLIST_MAX_TTL_MS = readEnvInt("DECENT_NODE_BLACKLIST_MAX_TTL_MS",
|
|
|
44
45
|
const FRIEND_ANNOUNCE_ATTEMPTS = readEnvInt("DECENT_FRIEND_ANNOUNCE_ATTEMPTS", 1);
|
|
45
46
|
const JOIN_ANNOUNCE_TIMEOUT_MS = readEnvInt("DECENT_JOIN_ANNOUNCE_TIMEOUT_MS", 12000);
|
|
46
47
|
const SELF_ANNOUNCE_INTERVAL_MS = readEnvInt("DECENT_SELF_ANNOUNCE_INTERVAL_MS", 20000);
|
|
48
|
+
// Classic-DHT maintenance cadence. Every tick we get_nodes toward our own key
|
|
49
|
+
// (so neighbours store our address and native peers can find our UDP endpoint)
|
|
50
|
+
// and toward each not-yet-UDP friend's key (so we find theirs and punch).
|
|
51
|
+
const DHT_MAINTENANCE_INTERVAL_MS = readEnvInt("DECENT_DHT_MAINTENANCE_INTERVAL_MS", 15000);
|
|
47
52
|
const MAX_SELF_ANNOUNCE_TARGETS = readEnvInt("DECENT_SELF_ANNOUNCE_TARGETS", 16);
|
|
48
53
|
const SELF_ANNOUNCE_ATTEMPTS = readEnvInt("DECENT_SELF_ANNOUNCE_ATTEMPTS", 3);
|
|
49
54
|
// Self-announce batch size — number of step1 / step2 requests to fan out
|
|
@@ -194,6 +199,7 @@ export class Peer {
|
|
|
194
199
|
#selfAnnounceTimer;
|
|
195
200
|
#friendConnectionTimer;
|
|
196
201
|
#lanDiscoveryTimer;
|
|
202
|
+
#dhtMaintenanceTimer;
|
|
197
203
|
// Per-friend last DHT-PK send time, keyed by friendId, used even when no
|
|
198
204
|
// session entry exists yet so the connection loop does not flood DHT-PK
|
|
199
205
|
// requests when route discovery keeps failing.
|
|
@@ -437,6 +443,10 @@ export class Peer {
|
|
|
437
443
|
clearInterval(this.#selfAnnounceTimer);
|
|
438
444
|
this.#selfAnnounceTimer = undefined;
|
|
439
445
|
}
|
|
446
|
+
if (this.#dhtMaintenanceTimer) {
|
|
447
|
+
clearInterval(this.#dhtMaintenanceTimer);
|
|
448
|
+
this.#dhtMaintenanceTimer = undefined;
|
|
449
|
+
}
|
|
440
450
|
if (this.#friendConnectionTimer) {
|
|
441
451
|
clearInterval(this.#friendConnectionTimer);
|
|
442
452
|
this.#friendConnectionTimer = undefined;
|
|
@@ -488,6 +498,7 @@ export class Peer {
|
|
|
488
498
|
this.#knownNodes = dedupeNodes([result.respondingNode, ...discovered, ...this.#opts.bootstrapNodes]);
|
|
489
499
|
await this.#runSelfAnnounce(false, Date.now() + JOIN_ANNOUNCE_TIMEOUT_MS);
|
|
490
500
|
this.#ensureSelfAnnounceLoop();
|
|
501
|
+
this.#ensureDhtMaintenanceLoop();
|
|
491
502
|
this.#ensureFriendConnectionLoop();
|
|
492
503
|
this.#ensureExpressPullLoop();
|
|
493
504
|
// Kick off an immediate friend connection cycle so persisted friends with
|
|
@@ -592,8 +603,8 @@ export class Peer {
|
|
|
592
603
|
}
|
|
593
604
|
this.#debugLog(`friend-request: announce stored on ${this.#lastSelfAnnounceStoredCount} node(s) before send`);
|
|
594
605
|
const appPayload = encodeFriendRequestPacket({
|
|
595
|
-
name: PEER_NICKNAME,
|
|
596
|
-
descr: PEER_STATUS_MESSAGE || "decent peer",
|
|
606
|
+
name: this.#opts.nickname ?? PEER_NICKNAME,
|
|
607
|
+
descr: (this.#opts.statusMessage ?? PEER_STATUS_MESSAGE) || "decent peer",
|
|
597
608
|
hello: hello ?? ""
|
|
598
609
|
});
|
|
599
610
|
const friendReqPayload = concatBytes([
|
|
@@ -1066,6 +1077,19 @@ export class Peer {
|
|
|
1066
1077
|
catch { /* best-effort */ }
|
|
1067
1078
|
}).catch(() => { });
|
|
1068
1079
|
}
|
|
1080
|
+
// Classic toxcore DHT RPC (ping / get_nodes / send_nodes). Answering these
|
|
1081
|
+
// is what makes us *findable* over UDP: a native iOS/Android peer searches
|
|
1082
|
+
// the DHT for our key, a neighbour that holds our address returns it, then
|
|
1083
|
+
// the native peer pings/get_nodes us directly and net_cryptos over UDP.
|
|
1084
|
+
// DHT is UDP-only, so ignore anything arriving via a TCP relay.
|
|
1085
|
+
if ((packet[0] === NET_PACKET_PING_REQUEST ||
|
|
1086
|
+
packet[0] === NET_PACKET_PING_RESPONSE ||
|
|
1087
|
+
packet[0] === NET_PACKET_GET_NODES ||
|
|
1088
|
+
packet[0] === NET_PACKET_SEND_NODES) &&
|
|
1089
|
+
!this.#remoteIsTcp(remote)) {
|
|
1090
|
+
this.#handleDhtRpc(packet, remote);
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1069
1093
|
if (packet[0] === NET_PACKET_CRYPTO) {
|
|
1070
1094
|
const opened = openToxDhtCryptoRequest(packet, this.#keyPair);
|
|
1071
1095
|
if (!opened) {
|
|
@@ -2700,8 +2724,15 @@ export class Peer {
|
|
|
2700
2724
|
// id (decentlan's IP channel = 163) via bulkDataPacketId so its
|
|
2701
2725
|
// high-volume traffic gets the same single-transport routing instead of
|
|
2702
2726
|
// fanning out over UDP + relay + TCP relay (which delivers 3-4 duplicates).
|
|
2703
|
-
|
|
2704
|
-
|
|
2727
|
+
// Chat (PACKET_ID_MESSAGE = 64) is LOW-volume and must be RELIABLE, so it
|
|
2728
|
+
// is NOT bulk: it dual-sends over UDP + relay and net_crypto retransmits,
|
|
2729
|
+
// so a reply still lands when the peer's UDP path is briefly stale (iOS
|
|
2730
|
+
// app suspend / NAT remap) instead of being fired into a dead endpoint and
|
|
2731
|
+
// silently lost. Only the app-declared high-volume channel (bulkDataPacketId,
|
|
2732
|
+
// e.g. decentlan IP=163) stays bulk / single-transport to avoid the relay
|
|
2733
|
+
// backlog + duplicate fan-out that high packet rates cause. Chat is sparse
|
|
2734
|
+
// and net_crypto dedups by packet number, so dual-send is safe here.
|
|
2735
|
+
const isBulkData = this.#opts.bulkDataPacketId !== undefined && kind === this.#opts.bulkDataPacketId;
|
|
2705
2736
|
await this.#sendToFriend(friendId, encrypted, session, isBulkData);
|
|
2706
2737
|
}
|
|
2707
2738
|
/**
|
|
@@ -2812,9 +2843,11 @@ export class Peer {
|
|
|
2812
2843
|
// callback. Most Carrier UIs read the displayed name from the
|
|
2813
2844
|
// userinfo flatbuffers payload below, but this keeps both paths
|
|
2814
2845
|
// consistent.
|
|
2815
|
-
const
|
|
2846
|
+
const nick = this.#opts.nickname ?? PEER_NICKNAME;
|
|
2847
|
+
const descr = this.#opts.statusMessage ?? PEER_STATUS_MESSAGE;
|
|
2848
|
+
const nameBytes = new TextEncoder().encode(nick);
|
|
2816
2849
|
await this.#sendMessengerPacket(friendId, PACKET_ID_NICKNAME, nameBytes);
|
|
2817
|
-
this.#debugLog(`nickname "${
|
|
2850
|
+
this.#debugLog(`nickname "${nick}" sent to ${friendId}`);
|
|
2818
2851
|
// Carrier C SDK transports its full user-profile (name, descr,
|
|
2819
2852
|
// gender, phone, email, region, has_avatar) as a FlatBuffers
|
|
2820
2853
|
// PACKET_TYPE_USERINFO (3) payload sent via toxcore's
|
|
@@ -2826,11 +2859,11 @@ export class Peer {
|
|
|
2826
2859
|
// __flatbuffers_soffset_read_from_pe (observed in Beagle 1.8.6).
|
|
2827
2860
|
await sleep(250);
|
|
2828
2861
|
const userInfo = encodeUserInfoPacket({
|
|
2829
|
-
name:
|
|
2830
|
-
descr
|
|
2862
|
+
name: nick,
|
|
2863
|
+
descr
|
|
2831
2864
|
});
|
|
2832
2865
|
await this.#sendMessengerPacket(friendId, PACKET_ID_STATUSMESSAGE, userInfo);
|
|
2833
|
-
this.#debugLog(`userinfo sent to ${friendId} (name="${
|
|
2866
|
+
this.#debugLog(`userinfo sent to ${friendId} (name="${nick}", descr="${descr}")`);
|
|
2834
2867
|
}
|
|
2835
2868
|
catch (error) {
|
|
2836
2869
|
this.#debugLog(`send profile failed for ${friendId}: ${error.message}`);
|
|
@@ -3542,6 +3575,193 @@ export class Peer {
|
|
|
3542
3575
|
this.#debugLog(`lan_sweep friend=${friendId} probes=${probes}`);
|
|
3543
3576
|
}
|
|
3544
3577
|
}
|
|
3578
|
+
// ===================== classic toxcore DHT (ping / get_nodes) ==========
|
|
3579
|
+
// Answering and issuing these makes a JS peer a real DHT node — the thing
|
|
3580
|
+
// native C++/iOS/Android Carrier clients rely on to find a friend's live UDP
|
|
3581
|
+
// ip:port and net_crypto to it directly. Previously peer.js was a DHT *client*
|
|
3582
|
+
// only (bootstrap get_nodes), so it was invisible: native peers couldn't find
|
|
3583
|
+
// our UDP endpoint and every session fell back to the slow TCP relay.
|
|
3584
|
+
#dhtPingId() {
|
|
3585
|
+
return randomBytes(8);
|
|
3586
|
+
}
|
|
3587
|
+
// The known nodes closest (XOR distance) to `target`, UDP-only, with a valid
|
|
3588
|
+
// 32-byte key — the candidates a get_nodes lookup should converge through.
|
|
3589
|
+
#closestKnownNodes(target, limit) {
|
|
3590
|
+
const withPk = [];
|
|
3591
|
+
for (const node of this.#knownNodes) {
|
|
3592
|
+
if (!node.pk || node.isTcp)
|
|
3593
|
+
continue;
|
|
3594
|
+
try {
|
|
3595
|
+
const pk = base58ToBytes(node.pk);
|
|
3596
|
+
if (pk.length === 32)
|
|
3597
|
+
withPk.push({ node, pk });
|
|
3598
|
+
}
|
|
3599
|
+
catch { /* skip malformed */ }
|
|
3600
|
+
}
|
|
3601
|
+
withPk.sort((a, b) => xorCloser(target, a.pk, b.pk));
|
|
3602
|
+
return withPk.slice(0, limit).map((e) => e.node);
|
|
3603
|
+
}
|
|
3604
|
+
#friendByDhtPk(dhtPkBase58) {
|
|
3605
|
+
for (const [friendId, session] of this.#friendSessions.entries()) {
|
|
3606
|
+
const dpk = session.friendDhtPublicKey;
|
|
3607
|
+
if (dpk && carrierIdFromPublicKey(dpk) === dhtPkBase58) {
|
|
3608
|
+
return { friendId, realPk: session.friendRealPublicKey ?? dpk };
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
return undefined;
|
|
3612
|
+
}
|
|
3613
|
+
#handleDhtRpc(packet, remote) {
|
|
3614
|
+
if (!this.#keyPair)
|
|
3615
|
+
return;
|
|
3616
|
+
const decoded = decodeDhtRpc(packet, this.#keyPair.secretKey);
|
|
3617
|
+
if (!decoded)
|
|
3618
|
+
return;
|
|
3619
|
+
const senderId = carrierIdFromPublicKey(decoded.senderPublicKey);
|
|
3620
|
+
// Learn the sender's real reachable UDP address: populates our table and
|
|
3621
|
+
// keeps mutual reachability alive across the mesh.
|
|
3622
|
+
if (remote.port > 0 && !this.#remoteIsTcp(remote)) {
|
|
3623
|
+
this.addKnownNodes([{ host: remote.address, port: remote.port, pk: senderId, isTcp: false }]);
|
|
3624
|
+
}
|
|
3625
|
+
if (decoded.type === NET_PACKET_PING_REQUEST) {
|
|
3626
|
+
const ping = parsePingPlain(decoded.plain);
|
|
3627
|
+
if (!ping)
|
|
3628
|
+
return;
|
|
3629
|
+
// Echo ping_id so the peer marks us alive and keeps us in its table
|
|
3630
|
+
// (= keeps us findable through it).
|
|
3631
|
+
const plain = buildPingPlain(NET_PACKET_PING_RESPONSE, ping.pingId);
|
|
3632
|
+
const resp = encodeDhtRpc(NET_PACKET_PING_RESPONSE, this.#keyPair, decoded.senderPublicKey, plain);
|
|
3633
|
+
void this.#sendPacket(resp, { host: remote.address, port: remote.port }).catch(() => undefined);
|
|
3634
|
+
return;
|
|
3635
|
+
}
|
|
3636
|
+
if (decoded.type === NET_PACKET_PING_RESPONSE) {
|
|
3637
|
+
return; // liveness already recorded via addKnownNodes above
|
|
3638
|
+
}
|
|
3639
|
+
if (decoded.type === NET_PACKET_GET_NODES) {
|
|
3640
|
+
const gn = parseGetNodesPlain(decoded.plain);
|
|
3641
|
+
if (!gn)
|
|
3642
|
+
return;
|
|
3643
|
+
// Reply with up to 4 known nodes closest to the searched key. When a
|
|
3644
|
+
// native peer searches for OUR key, replying from our socket reveals our
|
|
3645
|
+
// address to them directly, and the hint lets their lookup converge.
|
|
3646
|
+
const packedParts = [];
|
|
3647
|
+
let count = 0;
|
|
3648
|
+
for (const node of this.#closestKnownNodes(gn.target, 4)) {
|
|
3649
|
+
if (!node.pk)
|
|
3650
|
+
continue;
|
|
3651
|
+
let pk;
|
|
3652
|
+
try {
|
|
3653
|
+
pk = base58ToBytes(node.pk);
|
|
3654
|
+
}
|
|
3655
|
+
catch {
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3658
|
+
const packed = packUdpNodeV4(node.host, node.port, pk);
|
|
3659
|
+
if (packed) {
|
|
3660
|
+
packedParts.push(packed);
|
|
3661
|
+
count++;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
const nodesBytes = packedParts.length ? concatBytes(packedParts) : new Uint8Array(0);
|
|
3665
|
+
const plain = buildSendNodesPlain(nodesBytes, count, gn.pingId);
|
|
3666
|
+
const resp = encodeDhtRpc(NET_PACKET_SEND_NODES, this.#keyPair, decoded.senderPublicKey, plain);
|
|
3667
|
+
void this.#sendPacket(resp, { host: remote.address, port: remote.port }).catch(() => undefined);
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
if (decoded.type === NET_PACKET_SEND_NODES) {
|
|
3671
|
+
const nodeBytes = parseSendNodesNodeBytes(decoded.plain);
|
|
3672
|
+
if (!nodeBytes || nodeBytes.length === 0)
|
|
3673
|
+
return;
|
|
3674
|
+
const nodes = parsePackedNodes(nodeBytes).filter((n) => !n.isTcp);
|
|
3675
|
+
if (nodes.length)
|
|
3676
|
+
this.addKnownNodes(nodes);
|
|
3677
|
+
// If a returned node IS one of our friends' DHT keys, we just learned
|
|
3678
|
+
// their live UDP endpoint — cache it and kick a UDP cookie to punch.
|
|
3679
|
+
for (const node of nodes) {
|
|
3680
|
+
if (!node.pk)
|
|
3681
|
+
continue;
|
|
3682
|
+
const match = this.#friendByDhtPk(node.pk);
|
|
3683
|
+
if (!match)
|
|
3684
|
+
continue;
|
|
3685
|
+
try {
|
|
3686
|
+
this.#cacheFriendRemote(match.friendId, node.host, node.port, match.realPk, base58ToBytes(node.pk));
|
|
3687
|
+
this.#debugLog(`dht_search found UDP endpoint for ${match.friendId} at ${node.host}:${node.port}`);
|
|
3688
|
+
void this.#initiateSession(match.friendId).catch(() => undefined);
|
|
3689
|
+
}
|
|
3690
|
+
catch { /* malformed key */ }
|
|
3691
|
+
}
|
|
3692
|
+
return;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
async #sendDhtGetNodes(node, targetPublicKey) {
|
|
3696
|
+
if (!this.#keyPair || !node.pk || node.isTcp)
|
|
3697
|
+
return;
|
|
3698
|
+
let recipientPk;
|
|
3699
|
+
try {
|
|
3700
|
+
recipientPk = base58ToBytes(node.pk);
|
|
3701
|
+
}
|
|
3702
|
+
catch {
|
|
3703
|
+
return;
|
|
3704
|
+
}
|
|
3705
|
+
if (recipientPk.length !== 32)
|
|
3706
|
+
return;
|
|
3707
|
+
const plain = buildGetNodesPlain(targetPublicKey, this.#dhtPingId());
|
|
3708
|
+
const pkt = encodeDhtRpc(NET_PACKET_GET_NODES, this.#keyPair, recipientPk, plain);
|
|
3709
|
+
await this.#sendPacket(pkt, { host: node.host, port: node.port }).catch(() => undefined);
|
|
3710
|
+
}
|
|
3711
|
+
async #sendDhtPing(node) {
|
|
3712
|
+
if (!this.#keyPair || !node.pk || node.isTcp)
|
|
3713
|
+
return;
|
|
3714
|
+
let recipientPk;
|
|
3715
|
+
try {
|
|
3716
|
+
recipientPk = base58ToBytes(node.pk);
|
|
3717
|
+
}
|
|
3718
|
+
catch {
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
if (recipientPk.length !== 32)
|
|
3722
|
+
return;
|
|
3723
|
+
const plain = buildPingPlain(NET_PACKET_PING_REQUEST, this.#dhtPingId());
|
|
3724
|
+
const pkt = encodeDhtRpc(NET_PACKET_PING_REQUEST, this.#keyPair, recipientPk, plain);
|
|
3725
|
+
await this.#sendPacket(pkt, { host: node.host, port: node.port }).catch(() => undefined);
|
|
3726
|
+
}
|
|
3727
|
+
#ensureDhtMaintenanceLoop() {
|
|
3728
|
+
if (this.#dhtMaintenanceTimer || DHT_MAINTENANCE_INTERVAL_MS <= 0)
|
|
3729
|
+
return;
|
|
3730
|
+
this.#dhtMaintenanceTimer = setInterval(() => {
|
|
3731
|
+
void this.#doDhtMaintenance().catch(() => undefined);
|
|
3732
|
+
}, DHT_MAINTENANCE_INTERVAL_MS);
|
|
3733
|
+
if (typeof this.#dhtMaintenanceTimer.unref === "function")
|
|
3734
|
+
this.#dhtMaintenanceTimer.unref();
|
|
3735
|
+
void this.#doDhtMaintenance().catch(() => undefined); // become findable right after join
|
|
3736
|
+
}
|
|
3737
|
+
async #doDhtMaintenance() {
|
|
3738
|
+
if (!this.#keyPair)
|
|
3739
|
+
return;
|
|
3740
|
+
const selfPk = this.#keyPair.publicKey;
|
|
3741
|
+
// 1. get_nodes toward our OWN key → neighbours store our address → native
|
|
3742
|
+
// peers searching for us learn our UDP endpoint and can punch (findable).
|
|
3743
|
+
for (const node of this.#closestKnownNodes(selfPk, 8)) {
|
|
3744
|
+
void this.#sendDhtGetNodes(node, selfPk);
|
|
3745
|
+
}
|
|
3746
|
+
// 2. For each friend not on a confirmed UDP path, get_nodes toward THEIR
|
|
3747
|
+
// DHT key → learn their UDP endpoint → punch from our side.
|
|
3748
|
+
for (const [, session] of this.#friendSessions.entries()) {
|
|
3749
|
+
const dpk = session.friendDhtPublicKey;
|
|
3750
|
+
if (!dpk)
|
|
3751
|
+
continue;
|
|
3752
|
+
const udpConfirmed = session.lastUdpRecvMs !== undefined && Date.now() - session.lastUdpRecvMs < 8_000;
|
|
3753
|
+
if (udpConfirmed)
|
|
3754
|
+
continue;
|
|
3755
|
+
for (const node of this.#closestKnownNodes(dpk, 6)) {
|
|
3756
|
+
void this.#sendDhtGetNodes(node, dpk);
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
// 3. Keep a handful of nodes warm (and ourselves in their tables).
|
|
3760
|
+
for (const node of this.#knownNodes.slice(0, 8)) {
|
|
3761
|
+
if (!node.isTcp)
|
|
3762
|
+
void this.#sendDhtPing(node);
|
|
3763
|
+
}
|
|
3764
|
+
}
|
|
3545
3765
|
/**
|
|
3546
3766
|
* Send an onion DHT-PK announcement to a friend so they learn our DHT
|
|
3547
3767
|
* public key + endpoint hints. This is the bidirectional partner of the
|
|
@@ -3650,6 +3870,36 @@ export class Peer {
|
|
|
3650
3870
|
packed.push(entry);
|
|
3651
3871
|
}
|
|
3652
3872
|
}
|
|
3873
|
+
// Also advertise our PUBLIC reflexive (srflx) UDP endpoint, learned
|
|
3874
|
+
// via STUN. The LAN entries above only help same-LAN peers; a peer
|
|
3875
|
+
// across the internet — e.g. a native iOS/Android Beagle client —
|
|
3876
|
+
// needs our NAT-mapped public ip:port to send a UDP cookie request
|
|
3877
|
+
// and punch. Without it they only ever learn our TCP relays and the
|
|
3878
|
+
// session sits on the slow transpacific relay forever. This is the
|
|
3879
|
+
// "STUN-style reflexive discovery" the LAN-only code deferred; it's
|
|
3880
|
+
// what makes Mac<->phone text go UDP-direct. The receiver already
|
|
3881
|
+
// consumes 0x02 entries (#handleOnionDhtPk -> #cacheFriendRemote).
|
|
3882
|
+
try {
|
|
3883
|
+
const srflx = await this.#gatherOwnSrflx();
|
|
3884
|
+
if (srflx && srflx.port > 0 && !seenIps.has(srflx.host)) {
|
|
3885
|
+
const parts = srflx.host.split(".").map((p) => Number.parseInt(p, 10));
|
|
3886
|
+
if (parts.length === 4 && !parts.some((n) => !(n >= 0 && n <= 255))) {
|
|
3887
|
+
const entry = new Uint8Array(1 + 4 + 2 + 32);
|
|
3888
|
+
entry[0] = 0x02; // UDP_FAMILY_IPV4
|
|
3889
|
+
entry[1] = parts[0];
|
|
3890
|
+
entry[2] = parts[1];
|
|
3891
|
+
entry[3] = parts[2];
|
|
3892
|
+
entry[4] = parts[3];
|
|
3893
|
+
entry[5] = (srflx.port >> 8) & 0xff;
|
|
3894
|
+
entry[6] = srflx.port & 0xff;
|
|
3895
|
+
entry.set(ourDhtPk, 7);
|
|
3896
|
+
packed.push(entry);
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
}
|
|
3900
|
+
catch {
|
|
3901
|
+
// STUN gather failed — fall through with LAN + relay extras only.
|
|
3902
|
+
}
|
|
3653
3903
|
}
|
|
3654
3904
|
catch {
|
|
3655
3905
|
// os import or interface enumeration failed; skip UDP extras
|
package/dist/types/peer.d.ts
CHANGED
|
@@ -40,6 +40,16 @@ export type PeerOptions = {
|
|
|
40
40
|
* optimisation only applies to packet 64 and custom-id data is duplicated.
|
|
41
41
|
*/
|
|
42
42
|
bulkDataPacketId?: number;
|
|
43
|
+
/**
|
|
44
|
+
* Display name this peer advertises to friends (Carrier nickname, packet 48 +
|
|
45
|
+
* the userinfo profile). Without it every node reports the build default
|
|
46
|
+
* "@decentnetwork/peer" and friend lists can't tell anyone apart. decentlan
|
|
47
|
+
* passes its node name (e.g. "mac-dev", "cn", "tokyo"). Falls back to
|
|
48
|
+
* DECENT_PEER_NAME / the build default when unset.
|
|
49
|
+
*/
|
|
50
|
+
nickname?: string;
|
|
51
|
+
/** Status/description line shown alongside the nickname. */
|
|
52
|
+
statusMessage?: string;
|
|
43
53
|
compatibilityMode?: CompatibilityMode;
|
|
44
54
|
debugLabel?: string;
|
|
45
55
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decentnetwork/peer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41",
|
|
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",
|