@apocaliss92/nodelink-js 0.6.2 → 0.6.3

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.
@@ -11382,8 +11382,14 @@ async function getServerBinding(uid, options = {}) {
11382
11382
  headers: { Accept: "application/json" }
11383
11383
  });
11384
11384
  if (!res.ok) {
11385
+ let bodyPreview;
11386
+ try {
11387
+ const text = await res.text();
11388
+ bodyPreview = text.slice(0, 512).replace(/\s+/g, " ").trim();
11389
+ } catch {
11390
+ }
11385
11391
  logger?.log?.(
11386
- `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}`
11392
+ `[server-binding] ${uid}: HTTP ${res.status} ${res.statusText} from ${url}` + (bodyPreview ? ` \u2014 body=${bodyPreview}` : "")
11387
11393
  );
11388
11394
  cache.set(uid, { kind: "err", expires: now + NEGATIVE_TTL_MS });
11389
11395
  return void 0;
@@ -11499,6 +11505,68 @@ function parseServerBindingResponse(raw) {
11499
11505
  }
11500
11506
 
11501
11507
  // src/bcudp/BcUdpStream.ts
11508
+ async function probeEgressForHost(destHost, destPort) {
11509
+ return await new Promise((resolve, reject) => {
11510
+ const probe = import_node_dgram.default.createSocket("udp4");
11511
+ let settled = false;
11512
+ const finish = (err, out) => {
11513
+ if (settled) return;
11514
+ settled = true;
11515
+ try {
11516
+ probe.close();
11517
+ } catch {
11518
+ }
11519
+ if (err || !out) reject(err ?? new Error("egress probe failed"));
11520
+ else resolve(out);
11521
+ };
11522
+ probe.on("error", (e) => finish(e));
11523
+ try {
11524
+ probe.connect(destPort, destHost, () => {
11525
+ try {
11526
+ const a = probe.address();
11527
+ if (typeof a === "string") return finish(new Error("probe address is string"));
11528
+ finish(void 0, {
11529
+ localAddress: a.address,
11530
+ localPort: a.port
11531
+ });
11532
+ } catch (e) {
11533
+ finish(e);
11534
+ }
11535
+ });
11536
+ } catch (e) {
11537
+ finish(e);
11538
+ }
11539
+ });
11540
+ }
11541
+ function isSameSubnetAsAnyLocalIface(destHost, srcInfo) {
11542
+ if (!/^\d+\.\d+\.\d+\.\d+$/.test(destHost)) return "unknown";
11543
+ const dest = destHost.split(".").map((s) => Number(s));
11544
+ if (dest.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
11545
+ return "unknown";
11546
+ const ifaces = (0, import_node_os.networkInterfaces)();
11547
+ let ownerSubnet;
11548
+ for (const name of Object.keys(ifaces)) {
11549
+ const entries = ifaces[name];
11550
+ if (!entries) continue;
11551
+ for (const e of entries) {
11552
+ if (e.family !== "IPv4" || e.internal) continue;
11553
+ if (e.address !== srcInfo.localAddress) continue;
11554
+ const addr = e.address.split(".").map((s) => Number(s));
11555
+ const mask = e.netmask.split(".").map((s) => Number(s));
11556
+ if (addr.length !== 4 || mask.length !== 4 || addr.some((n) => !Number.isFinite(n)) || mask.some((n) => !Number.isFinite(n)))
11557
+ continue;
11558
+ ownerSubnet = { addr, mask };
11559
+ break;
11560
+ }
11561
+ if (ownerSubnet) break;
11562
+ }
11563
+ if (!ownerSubnet) return "unknown";
11564
+ for (let i = 0; i < 4; i++) {
11565
+ if ((ownerSubnet.addr[i] & ownerSubnet.mask[i]) !== (dest[i] & ownerSubnet.mask[i]))
11566
+ return "mismatch";
11567
+ }
11568
+ return "same";
11569
+ }
11502
11570
  var AckLatency = class {
11503
11571
  currentValues = [];
11504
11572
  lastReceiveTime = null;
@@ -11577,6 +11645,11 @@ function isUnroutableForP2P(ip) {
11577
11645
  var P2P_LOOKUP_PORT = 9999;
11578
11646
  var P2P_MAX_WAIT_MS = 15e3;
11579
11647
  var P2P_RESEND_WAIT_MS = 500;
11648
+ var inflightP2pLookups = /* @__PURE__ */ new Map();
11649
+ var cachedP2pLookups = /* @__PURE__ */ new Map();
11650
+ var negCachedP2pLookups = /* @__PURE__ */ new Map();
11651
+ var P2P_LOOKUP_CACHE_TTL_MS = 3e4;
11652
+ var P2P_LOOKUP_NEG_CACHE_TTL_MS = 15e3;
11580
11653
  var BcUdpStream = class extends import_node_events3.EventEmitter {
11581
11654
  opts;
11582
11655
  /**
@@ -11665,31 +11738,14 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
11665
11738
  });
11666
11739
  sock.on("error", (e) => this.emit("error", e));
11667
11740
  sock.on("close", () => this.emit("close"));
11668
- const portRange = Array.from({ length: 500 }, (_, i) => 53500 + i);
11669
- for (let i = portRange.length - 1; i > 0; i--) {
11670
- const j = Math.floor(Math.random() * (i + 1));
11671
- [portRange[i], portRange[j]] = [portRange[j], portRange[i]];
11672
- }
11673
- let bound = false;
11674
- for (const port of portRange) {
11675
- try {
11676
- await new Promise((resolve, reject) => {
11677
- sock.once("error", reject);
11678
- sock.bind(port, "0.0.0.0", () => {
11679
- sock.removeListener("error", reject);
11680
- resolve();
11681
- });
11682
- });
11683
- bound = true;
11684
- break;
11685
- } catch {
11686
- }
11687
- }
11688
- if (!bound) {
11689
- await new Promise(
11690
- (resolve) => sock.bind(0, "0.0.0.0", () => resolve())
11691
- );
11692
- }
11741
+ await new Promise((resolve, reject) => {
11742
+ const onErr = (e) => reject(e);
11743
+ sock.once("error", onErr);
11744
+ sock.bind(0, "0.0.0.0", () => {
11745
+ sock.removeListener("error", onErr);
11746
+ resolve();
11747
+ });
11748
+ });
11693
11749
  if (this.opts.mode === "direct") {
11694
11750
  this.remote = { host: this.opts.host, port: this.opts.port };
11695
11751
  this.clientId = this.opts.clientId;
@@ -11760,6 +11816,49 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
11760
11816
  this.remote = { host: connected.rhost, port: connected.rport };
11761
11817
  }
11762
11818
  async p2pUidLookup(sock, uid) {
11819
+ const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
11820
+ const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
11821
+ const cached = cachedP2pLookups.get(uid);
11822
+ if (cached && cached.expires > Date.now()) {
11823
+ log(
11824
+ `UID=${shortUid} cached lookup hit (relay=${cached.result.relay.ip}:${cached.result.relay.port})`
11825
+ );
11826
+ return cached.result;
11827
+ }
11828
+ const negCached = negCachedP2pLookups.get(uid);
11829
+ if (negCached && negCached.expires > Date.now()) {
11830
+ const remaining = negCached.expires - Date.now();
11831
+ log(
11832
+ `UID=${shortUid} negative-cache hit (fail-fast, retry in ${Math.ceil(remaining / 1e3)}s)`
11833
+ );
11834
+ throw negCached.error;
11835
+ }
11836
+ const inflight = inflightP2pLookups.get(uid);
11837
+ if (inflight) {
11838
+ log(`UID=${shortUid} sharing in-flight lookup with concurrent race lane`);
11839
+ return await inflight;
11840
+ }
11841
+ const work = this._doP2pUidLookupWork(sock, uid);
11842
+ inflightP2pLookups.set(uid, work);
11843
+ try {
11844
+ const result = await work;
11845
+ cachedP2pLookups.set(uid, {
11846
+ result,
11847
+ expires: Date.now() + P2P_LOOKUP_CACHE_TTL_MS
11848
+ });
11849
+ return result;
11850
+ } catch (e) {
11851
+ const err = e instanceof Error ? e : new Error(String(e));
11852
+ negCachedP2pLookups.set(uid, {
11853
+ error: err,
11854
+ expires: Date.now() + P2P_LOOKUP_NEG_CACHE_TTL_MS
11855
+ });
11856
+ throw err;
11857
+ } finally {
11858
+ inflightP2pLookups.delete(uid);
11859
+ }
11860
+ }
11861
+ async _doP2pUidLookupWork(sock, uid) {
11763
11862
  const log = (msg) => this.discoveryLogger?.log?.(`[P2P] ${msg}`);
11764
11863
  const shortUid = uid.length > 7 ? `${uid.slice(0, 5)}\u2026${uid.slice(-2)}` : uid;
11765
11864
  const t0 = Date.now();
@@ -12227,6 +12326,23 @@ var BcUdpStream = class extends import_node_events3.EventEmitter {
12227
12326
  log(
12228
12327
  `local discovery: mode=${localMode} uid=${shortUid} ports=[${ports.join(", ")}] broadcasts=[${broadcastHosts.join(", ")}]${directHost ? ` direct=${directHost}` : ""} localBindPort=${localPort} timeout=${discoveryTimeout}ms`
12229
12328
  );
12329
+ if (directHost && localMode === "local-direct") {
12330
+ try {
12331
+ const egress = await probeEgressForHost(directHost, ports[0] ?? 2015);
12332
+ const sameSubnet = isSameSubnetAsAnyLocalIface(directHost, egress);
12333
+ if (sameSubnet === "mismatch") {
12334
+ log(
12335
+ `WARN: kernel-chosen source IP ${egress.localAddress} is NOT in the same subnet as ${directHost}. Some Reolink battery cams silently drop discovery packets with off-subnet source IPs. If discovery fails, check your routing table \u2014 likely a Tailscale / VPN / secondary NIC stealing the default route.`
12336
+ );
12337
+ } else {
12338
+ log(
12339
+ `egress for ${directHost} \u2192 src=${egress.localAddress}` + (sameSubnet === "same" ? ` (same subnet \u2713)` : ` (subnet relationship unknown)`)
12340
+ );
12341
+ }
12342
+ } catch (e) {
12343
+ this.emit("debug", "egress_probe_failed", e);
12344
+ }
12345
+ }
12230
12346
  let bytesSent = 0;
12231
12347
  let pktsRecv = 0;
12232
12348
  sock.on("message", () => {