@decentnetwork/peer 0.1.17 → 0.1.18

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/peer.js CHANGED
@@ -16,6 +16,7 @@ import { loadOrCreateKeyPair } from "./crypto/keypair.js";
16
16
  import { base58ToBytes } from "./utils/base58.js";
17
17
  import { UdpTransport } from "./transport/udp.js";
18
18
  import { bytesToHex, concatBytes, randomBytes } from "./utils/bytes.js";
19
+ import { buildBindingRequest, decodeStun, decodeXorMappedAddress, findAttr, STUN_ATTR_XOR_MAPPED_ADDRESS, STUN_BINDING_SUCCESS } from "./stun.js";
19
20
  const ANNOUNCE_WAIT_TIMEOUT_MS = readEnvInt("DECENT_ANNOUNCE_WAIT_TIMEOUT_MS", 4000);
20
21
  const MAX_FRIEND_ROUTE_ATTEMPTS = readEnvInt("DECENT_FRIEND_ROUTE_MAX_ATTEMPTS", 12);
21
22
  // How many in-flight announce step1 requests to fan out at once. Toxcore
@@ -115,6 +116,13 @@ const PACKET_ID_USERSTATUS = 50; // friend's user status (1 byte enum)
115
116
  const PACKET_ID_TYPING = 51; // typing indicator (1 byte bool)
116
117
  const PACKET_ID_MESSAGE = 64; // text message (Carrier FlatBuffers payload)
117
118
  const PACKET_ID_ACTION = 65; // /me action message
119
+ // Custom lossless range (toxcore reserves 160-191 for custom lossless
120
+ // packets). We use 160 to carry a peer's server-reflexive UDP endpoint
121
+ // so the other side can hole-punch directly — the job onion-announce was
122
+ // supposed to do but can't against the live bootnodes. Peers that don't
123
+ // understand this ID simply fall through and ignore it. Payload is 6
124
+ // bytes: 4-byte IPv4 + 2-byte big-endian port.
125
+ const PACKET_ID_UDP_ENDPOINT = 160;
118
126
  export class Peer {
119
127
  #opts;
120
128
  #events = new EventEmitter();
@@ -192,6 +200,11 @@ export class Peer {
192
200
  // #initiateSession every few seconds for every friend; without this
193
201
  // dedupe, lower-pubkey side floods the log with "deferring to peer".
194
202
  #initiateSkipLogged = new Set();
203
+ // Cached server-reflexive (public) UDP endpoint of our #udp socket,
204
+ // learned via STUN. Cone-NAT mappings are stable for the socket
205
+ // lifetime, so we only re-probe every ~60s. Used to tell friends where
206
+ // to hole-punch us (PACKET_ID_UDP_ENDPOINT).
207
+ #srflxCache;
195
208
  // Per-friend tracking sets so we send our nickname/status/greeting once
196
209
  // per session rather than on every PACKET_ID_ONLINE arrival.
197
210
  #profileSentTo = new Set();
@@ -1212,6 +1225,15 @@ export class Peer {
1212
1225
  this.#debugLog(`hs_recv friend=${friendId} initiated=${weInitiated ? 1 : 0} remote=${remote.address}:${remote.port}`);
1213
1226
  if (!wasEstablished) {
1214
1227
  this.#debugLog(`friend_connected friend=${friendId} remote=${remote.address}:${remote.port}`);
1228
+ // If we only have a TCP-relay path, immediately tell the peer our
1229
+ // public UDP endpoint so they can hole-punch — don't wait for the
1230
+ // 15s UDP-retry loop, which can miss a flapping relay session's
1231
+ // short-lived establishment window. Both sides do this on every
1232
+ // (re)establishment, so a brief tcp-relay window is enough to
1233
+ // bootstrap the direct UDP path. See docs/UDP-DIRECT-PLAN.md.
1234
+ if (state.hasTcpRoute && !state.remote) {
1235
+ void this.#sendUdpEndpointOffer(friendId).catch(() => undefined);
1236
+ }
1215
1237
  }
1216
1238
  const friend = this.#friends.get(friendId);
1217
1239
  if (friend) {
@@ -1309,6 +1331,10 @@ export class Peer {
1309
1331
  }
1310
1332
  else {
1311
1333
  state.remote = { host: remote.address, port: remote.port };
1334
+ if (!state.lastUdpRecvMs) {
1335
+ this.#debugLog(`udp_confirmed friend=${friendId} via=${remote.address}:${remote.port} (direct UDP path live)`);
1336
+ }
1337
+ state.lastUdpRecvMs = Date.now();
1312
1338
  }
1313
1339
  state.receiveBufferStart = Math.max(state.receiveBufferStart ?? 0, (opened.packetNumber + 1) >>> 0);
1314
1340
  const kind = opened.payload[0];
@@ -1339,6 +1365,11 @@ export class Peer {
1339
1365
  this.#debugVerboseLog(`crypto alive (keepalive) received from ${friendId}`);
1340
1366
  return;
1341
1367
  }
1368
+ if (kind === PACKET_ID_UDP_ENDPOINT) {
1369
+ // Peer told us their public UDP endpoint — hole-punch to it.
1370
+ this.#handleUdpEndpointOffer(friendId, inner);
1371
+ return;
1372
+ }
1342
1373
  if (kind === PACKET_ID_REQUEST) {
1343
1374
  // Lossless retransmission request from peer. Full reliable stream
1344
1375
  // not yet implemented in JS port, so just acknowledge by logging.
@@ -2125,14 +2156,36 @@ export class Peer {
2125
2156
  // with the real endpoint and #sendMessengerPacket picks UDP
2126
2157
  // over TCP from then on. Throttled to UDP_RETRY_INTERVAL_MS so
2127
2158
  // the loop's 250ms tick doesn't flood DHT lookups.
2128
- const realUdpRemote = session.remote &&
2129
- !session.remote.host?.startsWith("tcp:") &&
2130
- session.remote.port !== 0;
2131
- if (!realUdpRemote && session.hasTcpRoute) {
2132
- const UDP_RETRY_INTERVAL_MS = 15_000;
2159
+ // Gate on whether direct UDP is *confirmed* (we've actually
2160
+ // received a packet over a real UDP endpoint recently), NOT on
2161
+ // whether session.remote happens to hold a UDP-shaped value —
2162
+ // #handleUdpEndpointOffer sets session.remote optimistically
2163
+ // before the punch is proven, so using it here would stop the
2164
+ // offer/punch retry prematurely and deadlock a half-punched pair.
2165
+ // Treat the UDP path as needing re-punch once it's been quiet for
2166
+ // >7s. This is deliberately close to #sendToFriend's 10s
2167
+ // relay-fallback threshold: if UDP stalls, we want the re-offer +
2168
+ // re-punch to fire almost immediately so the direct path recovers
2169
+ // before much traffic spills onto the slow relay (whose queue can
2170
+ // back up 20s+ on the China↔US leg). A healthy path refreshes
2171
+ // lastUdpRecvMs every ~1s via keepalive/data, so this never fires
2172
+ // while UDP is working.
2173
+ const udpConfirmed = session.lastUdpRecvMs !== undefined && now - session.lastUdpRecvMs < 7_000;
2174
+ if (!udpConfirmed && session.hasTcpRoute) {
2175
+ // Aggressive re-punch cadence: the relay path is lossy, so a few
2176
+ // offers may drop before one lands and both sides punch in the
2177
+ // same window. 3s keeps recovery snappy without flooding.
2178
+ const UDP_RETRY_INTERVAL_MS = 3_000;
2133
2179
  const lastTry = session.lastUdpRetryMs ?? 0;
2134
2180
  if (now - lastTry > UDP_RETRY_INTERVAL_MS) {
2135
2181
  session.lastUdpRetryMs = now;
2182
+ // Out-of-band UDP endpoint exchange (works where the broken
2183
+ // onion-announce DHT discovery below does not): tell the peer
2184
+ // our STUN-learned public endpoint over the messenger channel.
2185
+ // When they do the same, both sides feed each other's endpoint
2186
+ // into endpointCandidates and hole-punch. Cone↔cone gets a
2187
+ // direct ~one-RTT UDP path; see docs/UDP-DIRECT-PLAN.md.
2188
+ void this.#sendUdpEndpointOffer(friendId).catch(() => undefined);
2136
2189
  // Recompute friend's real public key for the DHT-PK push.
2137
2190
  // Same fallback chain as the cooldown branch below.
2138
2191
  let friendRealPk = session.friendRealPublicKey;
@@ -2312,7 +2365,17 @@ export class Peer {
2312
2365
  incrementNonce(session.ourBaseNonce);
2313
2366
  session.sendPacketNumber = (packetNumber + 1) >>> 0;
2314
2367
  session.lastPingSentMs = Date.now();
2315
- await this.#sendToFriend(friendId, encrypted, session);
2368
+ // Bulk IP-forwarding data (PACKET_ID_MESSAGE) is the high-volume
2369
+ // traffic. Once UDP is established it should ride UDP exclusively and
2370
+ // NEVER spill onto the TCP relay during a UDP hiccup — the relay's
2371
+ // China↔US queue backs up 20s+ and every spilled packet returns as a
2372
+ // hugely-delayed duplicate. Control packets (keepalives, endpoint
2373
+ // offers, profile) keep the relay fallback so the session and the
2374
+ // re-punch survive a UDP outage. Brief UDP gaps just drop a few data
2375
+ // packets (the proxied TCP stream retransmits) and the re-punch loop
2376
+ // restores the direct path within a few seconds.
2377
+ const isBulkData = kind === PACKET_ID_MESSAGE;
2378
+ await this.#sendToFriend(friendId, encrypted, session, isBulkData);
2316
2379
  }
2317
2380
  /**
2318
2381
  * Send `packet` to `friendId` over whichever transport(s) are
@@ -2322,11 +2385,12 @@ export class Peer {
2322
2385
  * receiving end (toxcore does the same when both transports are up).
2323
2386
  * Throws only if zero transports succeeded.
2324
2387
  */
2325
- async #sendToFriend(friendId, packet, session) {
2388
+ async #sendToFriend(friendId, packet, session, isBulkData = false) {
2326
2389
  const s = session ?? this.#friendSessions.get(friendId);
2327
2390
  let udpOk = false;
2328
2391
  let tcpOk = false;
2329
2392
  let firstError;
2393
+ const realUdpRemote = s?.remote && !s.remote.host?.startsWith("tcp:") && s.remote.port !== 0;
2330
2394
  if (s?.remote) {
2331
2395
  try {
2332
2396
  await this.#sendPacket(packet, s.remote);
@@ -2336,6 +2400,29 @@ export class Peer {
2336
2400
  firstError = error;
2337
2401
  }
2338
2402
  }
2403
+ // UDP-confirmed-ever: we've successfully received at least one UDP
2404
+ // packet from this peer, so the direct path has worked at some point.
2405
+ const udpEverConfirmed = s?.lastUdpRecvMs !== undefined;
2406
+ // Bulk IP-forwarding data: send UDP-ONLY once the direct path has ever
2407
+ // been confirmed. Never spill onto the relay (its backlog is the
2408
+ // source of the 20s-late duplicates). A transient UDP gap just drops a
2409
+ // few packets; the re-punch loop (every 3s when UDP is quiet >7s)
2410
+ // restores the path, and control traffic on the relay keeps the
2411
+ // session alive meanwhile.
2412
+ if (isBulkData && udpOk && realUdpRemote && udpEverConfirmed) {
2413
+ return;
2414
+ }
2415
+ // Control traffic: once direct UDP is confirmed *recently* (<10s),
2416
+ // also send UDP-only to avoid steady-state duplicates; otherwise fan
2417
+ // out over UDP + relay so control reaches the peer even when the
2418
+ // direct path is down (this is what lets the re-punch recover).
2419
+ const udpLiveRecent = udpOk &&
2420
+ realUdpRemote &&
2421
+ s?.lastUdpRecvMs !== undefined &&
2422
+ Date.now() - s.lastUdpRecvMs < 10_000;
2423
+ if (udpLiveRecent) {
2424
+ return;
2425
+ }
2339
2426
  if (this.#tcpRelays) {
2340
2427
  // The TCP relay routes by the friend's DHT pubkey (= the pubkey
2341
2428
  // they handshook the relay with), not by the friend's real
@@ -2473,6 +2560,147 @@ export class Peer {
2473
2560
  .sort((a, b) => b.updatedMs - a.updatedMs)
2474
2561
  .slice(0, 12);
2475
2562
  }
2563
+ /**
2564
+ * Learn our own server-reflexive (public) UDP endpoint by sending a
2565
+ * STUN binding-request to a bootnode on its STUN port (3478) over the
2566
+ * SAME #udp socket net_crypto uses — so the reflexive port matches
2567
+ * where our crypto data will actually arrive. Cached briefly because
2568
+ * a cone NAT's mapping is stable for the socket's lifetime.
2569
+ *
2570
+ * Returns undefined if no bootnode answers within the timeout.
2571
+ */
2572
+ async #gatherOwnSrflx() {
2573
+ const CACHE_MS = 60_000;
2574
+ const now = Date.now();
2575
+ if (this.#srflxCache && now - this.#srflxCache.atMs < CACHE_MS) {
2576
+ return this.#srflxCache.addr;
2577
+ }
2578
+ const bootnodes = this.#opts.bootstrapNodes;
2579
+ if (!bootnodes || bootnodes.length === 0)
2580
+ return undefined;
2581
+ // Try up to 3 bootnodes; first answer wins.
2582
+ for (const bn of bootnodes.slice(0, 3)) {
2583
+ const req = buildBindingRequest();
2584
+ const txnHex = Buffer.from(req.slice(8, 20)).toString("hex");
2585
+ const addr = await new Promise((resolve) => {
2586
+ const interceptor = (data, rinfo) => {
2587
+ if (rinfo.address !== bn.host || rinfo.port !== 3478)
2588
+ return false;
2589
+ const msg = decodeStun(data);
2590
+ if (!msg || msg.type !== STUN_BINDING_SUCCESS)
2591
+ return false;
2592
+ if (Buffer.from(msg.transactionId).toString("hex") !== txnHex)
2593
+ return false;
2594
+ const xma = findAttr(msg, STUN_ATTR_XOR_MAPPED_ADDRESS);
2595
+ const parsed = xma ? decodeXorMappedAddress(xma, msg.transactionId) : undefined;
2596
+ this.#udp.removeStunInterceptor(interceptor);
2597
+ clearTimeout(timer);
2598
+ resolve(parsed ? { host: parsed.address, port: parsed.port } : undefined);
2599
+ return true; // consume — don't let it reach net_crypto/DHT dispatch
2600
+ };
2601
+ const timer = setTimeout(() => {
2602
+ this.#udp.removeStunInterceptor(interceptor);
2603
+ resolve(undefined);
2604
+ }, 2000);
2605
+ this.#udp.addStunInterceptor(interceptor);
2606
+ this.#udp.sendDirect(Buffer.from(req), bn.host, 3478).catch(() => {
2607
+ this.#udp.removeStunInterceptor(interceptor);
2608
+ clearTimeout(timer);
2609
+ resolve(undefined);
2610
+ });
2611
+ });
2612
+ if (addr) {
2613
+ this.#srflxCache = { addr, atMs: now };
2614
+ return addr;
2615
+ }
2616
+ }
2617
+ return undefined;
2618
+ }
2619
+ /**
2620
+ * Tell a friend our public UDP endpoint over the (already-working,
2621
+ * possibly tcp-relay) messenger channel, so they can hole-punch to us.
2622
+ * This is the out-of-band substitute for the broken DHT/onion-announce
2623
+ * endpoint discovery. Both peers do this symmetrically; on receipt each
2624
+ * feeds the other's endpoint into endpointCandidates and punches.
2625
+ */
2626
+ async #sendUdpEndpointOffer(friendId) {
2627
+ const session = this.#friendSessions.get(friendId);
2628
+ if (!session?.established)
2629
+ return;
2630
+ const srflx = await this.#gatherOwnSrflx();
2631
+ if (!srflx)
2632
+ return;
2633
+ const octets = srflx.host.split(".").map((s) => parseInt(s, 10));
2634
+ if (octets.length !== 4 || octets.some((o) => Number.isNaN(o)))
2635
+ return;
2636
+ const payload = new Uint8Array(6);
2637
+ payload.set(octets, 0);
2638
+ payload[4] = (srflx.port >> 8) & 0xff;
2639
+ payload[5] = srflx.port & 0xff;
2640
+ try {
2641
+ await this.#sendMessengerPacket(friendId, PACKET_ID_UDP_ENDPOINT, payload);
2642
+ this.#debugLog(`udp-endpoint offer sent to ${friendId}: ${srflx.host}:${srflx.port}`);
2643
+ }
2644
+ catch {
2645
+ // best-effort — the retry loop will try again
2646
+ }
2647
+ }
2648
+ /**
2649
+ * A friend told us their public UDP endpoint. Feed it as a high-value
2650
+ * candidate and immediately spray a few raw punch datagrams at it to
2651
+ * open our own cone-NAT mapping toward their address:port, then kick a
2652
+ * session initiation (the higher-pubkey side sends the cookie request;
2653
+ * both sides' mappings are now open so it gets through). Mirrors the
2654
+ * proven standalone hole-punch.
2655
+ */
2656
+ #handleUdpEndpointOffer(friendId, payload) {
2657
+ if (payload.length < 6)
2658
+ return;
2659
+ const host = `${payload[0]}.${payload[1]}.${payload[2]}.${payload[3]}`;
2660
+ const port = ((payload[4] << 8) | payload[5]) >>> 0;
2661
+ if (port === 0)
2662
+ return;
2663
+ const session = this.#friendSessions.get(friendId);
2664
+ if (!session)
2665
+ return;
2666
+ // Don't punch toward ourselves.
2667
+ if (getLocalIpv4Addresses().includes(host) && this.#udp.localPort() === port)
2668
+ return;
2669
+ this.#rememberEndpointCandidate(session, host, port);
2670
+ this.#debugLog(`udp-endpoint offer from ${friendId}: ${host}:${port} — punching`);
2671
+ // Spray a handful of tiny punch datagrams (0xF2 is not a recognized
2672
+ // Carrier/DHT/net_crypto packet type, so the peer's #onDatagram
2673
+ // ignores them — their only purpose is opening our NAT mapping
2674
+ // toward host:port so the peer's crypto data can reach us).
2675
+ const punch = Uint8Array.of(0xf2);
2676
+ let n = 0;
2677
+ const punchTimer = setInterval(() => {
2678
+ this.#udp.sendDirectSync(Buffer.from(punch), host, port);
2679
+ if (++n >= 6)
2680
+ clearInterval(punchTimer);
2681
+ }, 120);
2682
+ // Point this session's UDP remote at the peer's reflexive endpoint
2683
+ // (unless we already have a confirmed real UDP remote). hasTcpRoute
2684
+ // stays true, so #sendToFriend fans crypto data out over BOTH UDP
2685
+ // and TCP relay: TCP keeps the session alive even if the punch
2686
+ // fails, while every UDP-bound keepalive/packet that makes it
2687
+ // through the freshly-punched hole causes the peer's CRYPTO_DATA
2688
+ // handler to set THEIR remote to our endpoint (peer.ts ~1564) —
2689
+ // upgrading the path to direct UDP in both directions with no new
2690
+ // handshake. If UDP never works, the periodic offer re-punches.
2691
+ const haveRealUdp = session.remote && !session.remote.host?.startsWith("tcp:") && session.remote.port !== 0;
2692
+ if (!haveRealUdp) {
2693
+ session.remote = { host, port };
2694
+ }
2695
+ // Nudge a keepalive out immediately so the upgrade doesn't wait for
2696
+ // the next periodic ALIVE — fans out over UDP (punched) + TCP.
2697
+ if (session.established) {
2698
+ void this.#sendMessengerPacket(friendId, PACKET_ID_ALIVE, new Uint8Array()).catch(() => undefined);
2699
+ }
2700
+ else {
2701
+ void this.#initiateSession(friendId).catch(() => undefined);
2702
+ }
2703
+ }
2476
2704
  #collectSessionEndpointCandidates(friendId, friend, session) {
2477
2705
  // Three buckets: same-LAN private (highest priority for direct UDP),
2478
2706
  // public/routable, and other private (different LAN, last-resort).
package/dist/stun.d.ts ADDED
@@ -0,0 +1,117 @@
1
+ /**
2
+ * STUN message codec (RFC 5389 + parts of 5766 for TURN integration).
3
+ *
4
+ * Supports the subset we need:
5
+ * - Binding Request / Binding Success Response (RFC 5389) for SRFLX
6
+ * discovery and ICE connectivity checks.
7
+ * - Long-term credential MESSAGE-INTEGRITY (HMAC-SHA1) and FINGERPRINT,
8
+ * needed by the bootnode TURN servers (coturn).
9
+ * - XOR-MAPPED-ADDRESS decoding.
10
+ * - USERNAME, REALM, NONCE, ERROR-CODE attributes (TURN auth dance).
11
+ * - SOFTWARE / FINGERPRINT attributes for being polite.
12
+ *
13
+ * Wire format:
14
+ *
15
+ * 0 1 2 3
16
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
17
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18
+ * |0 0| STUN Message Type | Message Length |
19
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20
+ * | Magic Cookie |
21
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
22
+ * | |
23
+ * | Transaction ID (96 bits) |
24
+ * | |
25
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
26
+ *
27
+ * Attributes follow as { type: u16, length: u16, value: bytes, pad to 4 }.
28
+ *
29
+ * MESSAGE-INTEGRITY is HMAC-SHA1 over the entire message up to (not
30
+ * including) the MESSAGE-INTEGRITY attribute itself, with the message
31
+ * length pre-adjusted as if MESSAGE-INTEGRITY were present.
32
+ */
33
+ export declare const STUN_MAGIC_COOKIE = 554869826;
34
+ export declare const STUN_BINDING_REQUEST = 1;
35
+ export declare const STUN_BINDING_SUCCESS = 257;
36
+ export declare const STUN_BINDING_ERROR = 273;
37
+ export declare const TURN_ALLOCATE_REQUEST = 3;
38
+ export declare const TURN_ALLOCATE_SUCCESS = 259;
39
+ export declare const TURN_ALLOCATE_ERROR = 275;
40
+ export declare const TURN_REFRESH_REQUEST = 4;
41
+ export declare const TURN_REFRESH_SUCCESS = 260;
42
+ export declare const TURN_CREATE_PERMISSION_REQUEST = 8;
43
+ export declare const TURN_CREATE_PERMISSION_SUCCESS = 264;
44
+ export declare const TURN_SEND_INDICATION = 22;
45
+ export declare const TURN_DATA_INDICATION = 23;
46
+ export declare const STUN_ATTR_MAPPED_ADDRESS = 1;
47
+ export declare const STUN_ATTR_USERNAME = 6;
48
+ export declare const STUN_ATTR_MESSAGE_INTEGRITY = 8;
49
+ export declare const STUN_ATTR_ERROR_CODE = 9;
50
+ export declare const STUN_ATTR_REALM = 20;
51
+ export declare const STUN_ATTR_NONCE = 21;
52
+ export declare const STUN_ATTR_XOR_MAPPED_ADDRESS = 32;
53
+ export declare const STUN_ATTR_SOFTWARE = 32802;
54
+ export declare const STUN_ATTR_FINGERPRINT = 32808;
55
+ export declare const STUN_ATTR_CHANNEL_NUMBER = 12;
56
+ export declare const STUN_ATTR_LIFETIME = 13;
57
+ export declare const STUN_ATTR_XOR_PEER_ADDRESS = 18;
58
+ export declare const STUN_ATTR_DATA = 19;
59
+ export declare const STUN_ATTR_XOR_RELAYED_ADDRESS = 22;
60
+ export declare const STUN_ATTR_REQUESTED_TRANSPORT = 25;
61
+ export declare const STUN_ATTR_DONT_FRAGMENT = 26;
62
+ export interface StunAttribute {
63
+ type: number;
64
+ value: Uint8Array;
65
+ }
66
+ export interface StunMessage {
67
+ type: number;
68
+ transactionId: Uint8Array;
69
+ attributes: StunAttribute[];
70
+ }
71
+ export interface AddressValue {
72
+ family: 4 | 6;
73
+ address: string;
74
+ port: number;
75
+ }
76
+ export declare function newTransactionId(): Uint8Array;
77
+ /**
78
+ * Encode a STUN message. If `integrityKey` is provided, a
79
+ * MESSAGE-INTEGRITY attribute (HMAC-SHA1) is appended. If `fingerprint`
80
+ * is true, a FINGERPRINT (CRC32 XOR 0x5354554e) is appended last.
81
+ *
82
+ * The order matters because MESSAGE-INTEGRITY must be computed over
83
+ * the message *as if* it were the last attribute (with length already
84
+ * adjusted), and FINGERPRINT must be computed over the message
85
+ * including MESSAGE-INTEGRITY.
86
+ */
87
+ export declare function encodeStun(message: StunMessage, opts?: {
88
+ integrityKey?: Uint8Array;
89
+ fingerprint?: boolean;
90
+ }): Uint8Array;
91
+ export declare function decodeStun(data: Uint8Array): StunMessage | undefined;
92
+ export declare function findAttr(message: StunMessage, type: number): Uint8Array | undefined;
93
+ /**
94
+ * Decode XOR-MAPPED-ADDRESS (RFC 5389 §15.2). The high bits of the
95
+ * port and the address are XORed with the magic cookie + transactionId.
96
+ */
97
+ export declare function decodeXorMappedAddress(value: Uint8Array, transactionId: Uint8Array): AddressValue | undefined;
98
+ /** Encode XOR-MAPPED-ADDRESS for outgoing STUN responses (TURN, conn check). */
99
+ export declare function encodeXorMappedAddress(addr: AddressValue, transactionId: Uint8Array): Uint8Array;
100
+ export declare function encodeErrorCode(code: number, reason: string): Uint8Array;
101
+ export declare function decodeErrorCode(value: Uint8Array): {
102
+ code: number;
103
+ reason: string;
104
+ } | undefined;
105
+ /**
106
+ * Long-term-credential integrity key: MD5("username:realm:password").
107
+ * RFC 5389 §15.4. coturn uses this.
108
+ */
109
+ export declare function longTermIntegrityKey(username: string, realm: string, password: string): Uint8Array;
110
+ /** Short-term-credential integrity key: just the password as UTF-8. */
111
+ export declare function shortTermIntegrityKey(password: string): Uint8Array;
112
+ export declare function buildBindingRequest(transactionId?: Uint8Array): Uint8Array;
113
+ export declare function buildBindingRequestWithIntegrity(opts: {
114
+ username: string;
115
+ password: string;
116
+ transactionId?: Uint8Array;
117
+ }): Uint8Array;