@decentnetwork/peer 0.1.56 → 0.1.57

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.d.ts CHANGED
@@ -194,6 +194,9 @@ export declare class Peer {
194
194
  * tcp-relay, discovery itself is broken — we don't even know
195
195
  * where to send a cookie request. */
196
196
  endpointCandidatesCount: number;
197
+ /** The actual candidate endpoints (host:port) — lets diag see whether a
198
+ * same-LAN host candidate (e.g. 10.0.0.x) was received but not adopted. */
199
+ endpointCandidates: string[];
197
200
  /** Last time we sent an outbound cookie request via UDP for this
198
201
  * peer (null = never). Used to confirm Phase 1.2 retries are
199
202
  * actually firing. */
package/dist/peer.js CHANGED
@@ -1062,6 +1062,7 @@ export class Peer {
1062
1062
  ourLocalUdpPort: this.#udp?.localPort() ?? null,
1063
1063
  friendUdpEndpoint: friendUdp,
1064
1064
  endpointCandidatesCount: s.endpointCandidates?.length ?? 0,
1065
+ endpointCandidates: (s.endpointCandidates ?? []).map((c) => `${c.host}:${c.port}`),
1065
1066
  cookieRequestSentMs: s.cookieRequestSentMs ?? null,
1066
1067
  };
1067
1068
  }
@@ -1628,7 +1629,19 @@ export class Peer {
1628
1629
  state.lastRelayRecvMs = Date.now();
1629
1630
  }
1630
1631
  else {
1631
- state.remote = { host: remote.address, port: remote.port };
1632
+ // Adopt the source as our send endpoint — but NEVER downgrade from a
1633
+ // same-LAN (physical) path to a non-LAN source. A packet that loops in
1634
+ // via the peer's public / Tailscale-exit endpoint must not knock us off
1635
+ // the direct LAN path, which is strictly better. (This is the other
1636
+ // half of the "use the physical LAN, not Tailscale" rule.)
1637
+ const cur = state.remote?.host;
1638
+ const onSameLanAlready = !!cur && !cur.startsWith("tcp:") && isPrivateAddress(cur) && !isCgnatAddress(cur) &&
1639
+ getPhysicalLanSubnets().some((s) => isInIpv4Subnet(cur, s));
1640
+ const incomingSameLan = isPrivateAddress(remote.address) && !isCgnatAddress(remote.address) &&
1641
+ getPhysicalLanSubnets().some((s) => isInIpv4Subnet(remote.address, s));
1642
+ if (incomingSameLan || !onSameLanAlready) {
1643
+ state.remote = { host: remote.address, port: remote.port };
1644
+ }
1632
1645
  if (!state.lastUdpRecvMs) {
1633
1646
  this.#debugLog(`udp_confirmed friend=${friendId} via=${remote.address}:${remote.port} (direct UDP path live)`);
1634
1647
  }
@@ -3353,8 +3366,19 @@ export class Peer {
3353
3366
  if (++ln >= 6)
3354
3367
  clearInterval(lanTimer);
3355
3368
  }, 120);
3356
- const haveRealUdp = session.remote && !session.remote.host?.startsWith("tcp:") && session.remote.port !== 0;
3357
- if (!haveRealUdp) {
3369
+ // The physical LAN is strictly better than ANY public path — direct, no
3370
+ // NAT, no relay, and crucially no Tailscale-exit hairpin (a peer whose
3371
+ // internet egresses via Tailscale advertises its exit's public IP as
3372
+ // its srflx, so the "direct" public path actually loops out through
3373
+ // Tailscale and back). So adopt the LAN candidate as session.remote even
3374
+ // when we already have a public "real UDP" remote — UNLESS we're already
3375
+ // on a same-LAN address (don't flap between two LAN candidates). Without
3376
+ // this the session stuck to the public/Tailscale endpoint and never sent
3377
+ // over the LAN (observed: session.remote stayed public, data on relay).
3378
+ const cur = session.remote?.host;
3379
+ const onSameLanAlready = !!cur && !cur.startsWith("tcp:") && isPrivateAddress(cur) && !isCgnatAddress(cur) &&
3380
+ getPhysicalLanSubnets().some((s) => isInIpv4Subnet(cur, s));
3381
+ if (!onSameLanAlready) {
3358
3382
  session.remote = { host: lanHost, port: lanPort };
3359
3383
  }
3360
3384
  if (session.established) {
@@ -4920,7 +4944,15 @@ function isInIpv4Subnet(host, subnet) {
4920
4944
  // path onto the overlay or loops into our own mesh, then silently falls back to
4921
4945
  // the relay — the snoopy/lili "lastUdpRecv=never -> bufferbloat" path. (lili
4922
4946
  // runs Tailscale; that's exactly this.)
4923
- const VIRTUAL_IFACE_RE = /^(utun|tun|tap|wg|tailscale|zt|ham|agentnet|awdl|llw|gif|stf|bridge|vnic|vmnet|veth|docker)/i;
4947
+ // Names of interfaces that are NOT a real physical LAN — overlays (tailscale,
4948
+ // zerotier, wireguard, the agentnet TUN), and especially container/VM bridges.
4949
+ // Docker creates `docker0` AND per-network `br-<hash>` bridges (172.x); libvirt
4950
+ // `virbr0`; k8s `cni0`/`flannel`/`kube`. These were leaking their 172.x address
4951
+ // into the LAN host-candidate offer, so a peer on a real 10.x LAN advertised a
4952
+ // Docker-bridge IP its LAN partner could never match → LAN-direct never engaged
4953
+ // (observed: two boxes on one 10.0.0.0/24 stuck hairpinning through the public
4954
+ // internet). `^bridge` did NOT cover `br-…`.
4955
+ const VIRTUAL_IFACE_RE = /^(utun|tun|tap|wg|tailscale|zt|ham|agentnet|awdl|llw|gif|stf|bridge|br-|virbr|cni|flannel|weave|kube|vnet|vnic|vmnet|veth|docker)/i;
4924
4956
  /** True for RFC 6598 carrier-grade-NAT space 100.64.0.0/10 (Tailscale's range). */
4925
4957
  function isCgnatAddress(host) {
4926
4958
  const o = host.split(".");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decentnetwork/peer",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
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",