socket-function 1.2.11 → 1.2.13

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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(mkdir -p C:/Users/quent/AppData/Local/Temp/claude/D--repos-socket-function/117d10bd-31c6-4c68-938d-1fff47788fa6/scratchpad/tm-recover)",
5
+ "Bash(cp -r 'C:/Users/quent/AppData/Local/BraveSoftware/Brave-Browser/User Data/Default/Local Extension Settings/dhdgffkkebhmkfjojejmpbldmpobfkfo' C:/Users/quent/AppData/Local/Temp/claude/D--repos-socket-function/117d10bd-31c6-4c68-938d-1fff47788fa6/scratchpad/tm-recover/LocalExtSettings)"
6
+ ]
7
+ }
8
+ }
package/index.d.ts CHANGED
@@ -957,7 +957,12 @@ declare module "socket-function/src/forwardPort" {
957
957
  }): Promise<void>;
958
958
  /** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
959
959
  * mapping points at us or at a different machine on the network. */
960
- export declare function getLocalInternalIP(): string | undefined;
960
+ export declare function getLocalInternalIP(): Promise<string | undefined>;
961
+ /** True when our outbound address is private/CGNAT — i.e. a NAT sits between us and the
962
+ * internet, so forwarding a port is worthwhile. A public outbound address means we're
963
+ * directly reachable and forwarding is unnecessary. This is the cross-platform gate that
964
+ * replaced the old "skip forwarding on linux" check, so Linux hosts behind NAT forward. */
965
+ export declare function isBehindNAT(): Promise<boolean>;
961
966
 
962
967
  }
963
968
 
@@ -1144,6 +1149,7 @@ declare module "socket-function/src/misc" {
1144
1149
  export declare function watchSlowPromise<T>(title: string, promise: Promise<T>, config?: {
1145
1150
  interval?: number;
1146
1151
  }): Promise<T>;
1152
+ export declare function isIpDomain(nodeIdOrHost: string): boolean;
1147
1153
 
1148
1154
  }
1149
1155
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "1.2.11",
3
+ "version": "1.2.13",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -500,7 +500,7 @@ export async function createCallFactory(
500
500
  console.error("Error getting alternate node IDs", e);
501
501
  }
502
502
 
503
- let newWebSocket = createWebsocket(nodeId, proposeProtocols(!SocketFunction.isClient() ? nodeId : undefined, { lz4: true }));
503
+ let newWebSocket = createWebsocket(nodeId, proposeProtocols(nodeId, { lz4: true }));
504
504
  await initializeWebsocket(newWebSocket);
505
505
 
506
506
  return newWebSocket;
@@ -22,4 +22,9 @@ export declare function forwardPort(config: {
22
22
  }): Promise<void>;
23
23
  /** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
24
24
  * mapping points at us or at a different machine on the network. */
25
- export declare function getLocalInternalIP(): string | undefined;
25
+ export declare function getLocalInternalIP(): Promise<string | undefined>;
26
+ /** True when our outbound address is private/CGNAT — i.e. a NAT sits between us and the
27
+ * internet, so forwarding a port is worthwhile. A public outbound address means we're
28
+ * directly reachable and forwarding is unnecessary. This is the cross-platform gate that
29
+ * replaced the old "skip forwarding on linux" check, so Linux hosts behind NAT forward. */
30
+ export declare function isBehindNAT(): Promise<boolean>;
@@ -15,14 +15,18 @@ async function resolveGateway(): Promise<{
15
15
  controlPort: number;
16
16
  controlURLs: string[];
17
17
  }> {
18
- const localObj = getLocalInterfaceAddress();
19
- if (!localObj) throw new Error("Could not find the local address / gateway");
18
+ let internalIP = await getOutboundIP();
19
+ if (!internalIP) throw new Error("Could not determine our local network address");
20
20
 
21
- const { internalIP, gatewayIP } = localObj;
22
- console.log(`Local IP: ${internalIP}, Gateway IP: ${gatewayIP}`);
21
+ // The gateway that answers SSDP discovery is the router we forward through; take its IP
22
+ // and control port straight from the discovered device URL rather than parsing routes.
23
23
  let gateway = await discoverGateway(internalIP);
24
+ let gatewayURL = new URL(gateway);
25
+ let gatewayIP = gatewayURL.hostname;
26
+ let controlPort = Number(gatewayURL.port);
24
27
  let controlURLs = await getControlPaths(gateway);
25
- let controlPort = Number(new URL(gateway).port);
28
+
29
+ console.log(`Local IP: ${internalIP}, Gateway IP: ${gatewayIP}`);
26
30
 
27
31
  return { internalIP, gatewayIP, controlPort, controlURLs };
28
32
  }
@@ -45,8 +49,6 @@ export interface PortMapping {
45
49
  * GetGenericPortMappingEntry from index 0 until the gateway reports the index
46
50
  * is out of range (SOAP error 713 / a non-200 response). */
47
51
  export async function listPortMappings(): Promise<PortMapping[]> {
48
- if (os.platform() === "linux") return [];
49
-
50
52
  const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
51
53
 
52
54
  let lastError: unknown;
@@ -77,9 +79,6 @@ export async function forwardPort(config: {
77
79
  internalPort: number;
78
80
  duration?: number;
79
81
  }) {
80
- // On linux, just return, the server probably doesn't require forwarding, and if it does,
81
- // it probably this code probably won't work anyways.
82
- if (os.platform() === "linux") return;
83
82
  try {
84
83
  const { externalPort, internalPort } = config;
85
84
  let duration = config.duration ?? timeInHour;
@@ -110,72 +109,71 @@ export async function forwardPort(config: {
110
109
 
111
110
  /** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
112
111
  * mapping points at us or at a different machine on the network. */
113
- export function getLocalInternalIP(): string | undefined {
114
- return getLocalInterfaceAddress()?.internalIP;
112
+ export async function getLocalInternalIP(): Promise<string | undefined> {
113
+ return getOutboundIP();
115
114
  }
116
115
 
117
- function getLocalInterfaceAddress(): { internalIP: string; gatewayIP: string; } | undefined {
118
- let looksLikeRouter = (ip: string) => ip.startsWith("10.0.0") || ip.startsWith("10.0.1") || ip.startsWith("192.168.0");
119
-
120
- // On windows, run `ipconfig` and parse the output
121
- // Otherwise, ifconfig
122
- if (os.platform() === "win32") {
123
- let output = require("child_process").execSync("ipconfig").toString();
124
- let sections = output.split("\r\n\r\n");
125
-
126
- for (let section of sections) {
127
- if (section.includes("IPv4 Address")) {
128
- let ipv4Match = section.match(/IPv4 Address[.\s]*: ([\d.]+)/);
129
- let gatewayMatch = section.match(/Default Gateway[.\s]*: ([\d.]+)/);
130
-
131
- if (ipv4Match && gatewayMatch && looksLikeRouter(gatewayMatch[1])) {
132
- return {
133
- internalIP: ipv4Match[1],
134
- gatewayIP: gatewayMatch[1]
135
- };
136
- }
137
- }
138
- }
139
- } else {
140
- let gatewayMatch: RegExpMatchArray | undefined;
141
- try {
142
- // Attempt to get the gateway using "ip route" command (more universal)
143
- const routeOutput = require("child_process").execSync("ip route show default").toString();
144
- gatewayMatch = routeOutput.match(/default via (\d+\.\d+\.\d+\.\d+)/);
145
- } catch (err) {
146
- console.error("Failed to execute 'ip route show default', trying fallback", err);
147
- }
116
+ /** True when our outbound address is private/CGNAT i.e. a NAT sits between us and the
117
+ * internet, so forwarding a port is worthwhile. A public outbound address means we're
118
+ * directly reachable and forwarding is unnecessary. This is the cross-platform gate that
119
+ * replaced the old "skip forwarding on linux" check, so Linux hosts behind NAT forward. */
120
+ export async function isBehindNAT(): Promise<boolean> {
121
+ let ip = await getOutboundIP();
122
+ if (!ip) {
123
+ return false;
124
+ }
125
+ return isPrivateIPv4(ip);
126
+ }
148
127
 
149
- if (!gatewayMatch) {
128
+ // RFC-1918 private ranges, plus 100.64/10 (carrier-grade NAT) and 169.254/16 (link-local).
129
+ // Any of these as our outbound address means there's a NAT between us and the internet.
130
+ function isPrivateIPv4(ip: string): boolean {
131
+ let parts = ip.split(".").map(Number);
132
+ if (parts.length !== 4 || parts.some(n => !Number.isInteger(n))) {
133
+ return false;
134
+ }
135
+ let [a, b] = parts;
136
+ return (
137
+ a === 10 ||
138
+ (a === 172 && b >= 16 && b <= 31) ||
139
+ (a === 192 && b === 168) ||
140
+ (a === 100 && b >= 64 && b <= 127) ||
141
+ (a === 169 && b === 254)
142
+ );
143
+ }
144
+
145
+ // The source IPv4 the kernel would use to reach the internet. Connecting a UDP socket runs a
146
+ // route lookup that assigns the local address without sending any packets, so it picks the
147
+ // correct interface even when several exist (VPN, docker, ...). Falls back to scanning the
148
+ // interface list when the route probe can't run.
149
+ async function getOutboundIP(): Promise<string | undefined> {
150
+ let viaRoute = await new Promise<string | undefined>(resolve => {
151
+ const socket = dgram.createSocket("udp4");
152
+ let finish = (ip: string | undefined) => {
150
153
  try {
151
- // Fallback to "netstat -rn" for older systems
152
- const netstatOutput = require("child_process").execSync("netstat -rn").toString();
153
- gatewayMatch = netstatOutput.match(/default\s+(\d+\.\d+\.\d+\.\d+)/);
154
- } catch (err) {
155
- console.error("Failed to execute 'netstat -rn', unable to find gateway", err);
154
+ socket.close();
155
+ } catch {
156
156
  }
157
+ resolve(ip);
158
+ };
159
+ socket.on("error", () => finish(undefined));
160
+ try {
161
+ socket.connect(53, "8.8.8.8", () => finish(socket.address().address));
162
+ } catch {
163
+ finish(undefined);
157
164
  }
165
+ });
166
+ if (viaRoute) {
167
+ return viaRoute;
168
+ }
158
169
 
159
- if (gatewayMatch) {
160
- try {
161
- // Use "ip addr" to get internal IP (more universal)
162
- const ipOutput = require("child_process").execSync("ip addr").toString();
163
- const ipMatch = ipOutput.match(/inet (?!127\.0\.0\.1)(\d+\.\d+\.\d+\.\d+)\//);
164
-
165
- if (ipMatch) {
166
- return {
167
- internalIP: ipMatch[1],
168
- gatewayIP: gatewayMatch[1]
169
- };
170
- } else {
171
- console.error("Failed to match internal IP");
172
- }
173
- } catch (err) {
174
- console.error("Failed to execute 'ip addr'", err);
170
+ for (let addrs of Object.values(os.networkInterfaces())) {
171
+ for (let addr of addrs ?? []) {
172
+ if (addr.family === "IPv4" && !addr.internal) {
173
+ return addr.address;
175
174
  }
176
175
  }
177
176
  }
178
-
179
177
  return undefined;
180
178
  }
181
179
 
package/src/misc.d.ts CHANGED
@@ -98,3 +98,4 @@ export declare function errorToWarning<T>(promise: Promise<T>): void;
98
98
  export declare function watchSlowPromise<T>(title: string, promise: Promise<T>, config?: {
99
99
  interval?: number;
100
100
  }): Promise<T>;
101
+ export declare function isIpDomain(nodeIdOrHost: string): boolean;
package/src/misc.ts CHANGED
@@ -476,4 +476,21 @@ export function watchSlowPromise<T>(title: string, promise: Promise<T>, config?:
476
476
  }
477
477
  })();
478
478
  return promise;
479
+ }
480
+
481
+ // An ip domain (127-0-0-1.example.com, or a raw ip) is an address alias for a machine, not a node identity — a server's real nodeId will never equal it.
482
+ export function isIpDomain(nodeIdOrHost: string): boolean {
483
+ let host = nodeIdOrHost.split(":")[0];
484
+ return isIpParts(host.split(".")[0].split("-")) || isIpParts(host.split("."));
485
+ }
486
+ function isIpParts(parts: string[]): boolean {
487
+ if (parts.length !== 4) return false;
488
+ for (let part of parts) {
489
+ if (!part || part.length > 3) return false;
490
+ for (let char of part) {
491
+ if (char < "0" || char > "9") return false;
492
+ }
493
+ if (parseInt(part) > 255) return false;
494
+ }
495
+ return true;
479
496
  }
@@ -5,6 +5,8 @@
5
5
  // the server returns no protocol and the handshake fails — which is exactly the
6
6
  // rejection semantics we want (indistinguishable from "node not reachable").
7
7
 
8
+ import { isIpDomain } from "./misc";
9
+
8
10
  const PROTOCOL_VERSION = "v1";
9
11
 
10
12
  export type ConnectionFlags = {
@@ -70,6 +72,10 @@ export function decodeProtocol(hex: string): DecodedProtocol | undefined {
70
72
  // cert). The server then accepts the connection regardless of its identity,
71
73
  // while still negotiating flags.
72
74
  export function proposeProtocols(target: string | undefined, clientCapabilities: { lz4: boolean }): string[] {
75
+ // An ip domain is an address alias, not a node identity — the server's real nodeId can never match it, so connect as a wildcard client (exactly like browsers do).
76
+ if (target && isIpDomain(target)) {
77
+ target = undefined;
78
+ }
73
79
  let out: string[] = [];
74
80
  let clientLZ4Options = clientCapabilities.lz4 ? [true, false] : [false];
75
81
  let serverLZ4Options = [true, false];
@@ -18,8 +18,7 @@ import { yellow } from "./formatting/logColors";
18
18
  import { green } from "./formatting/logColors";
19
19
  import { formatTime } from "./formatting/format";
20
20
  import { getExternalIP, testTCPIsListening } from "./networking";
21
- import { forwardPort, listPortMappings, getLocalInternalIP, PortMapping } from "./forwardPort";
22
- import os from "os";
21
+ import { forwardPort, listPortMappings, getLocalInternalIP, isBehindNAT, PortMapping } from "./forwardPort";
23
22
 
24
23
  // When a requested port is taken and useAvailablePortIfPortInUse is set, we scan
25
24
  // upwards from this base instead of binding a random OS-assigned port, so restarts
@@ -362,15 +361,15 @@ export async function startSocketServer(
362
361
  }
363
362
 
364
363
  // Forwarding maps the external port to an equal internal port, so a candidate is only
365
- // usable if we can also own its external mapping on the router. Skipped on linux,
366
- // where forwardPort is a no-op anyway.
367
- const doForward = !!(config.autoForwardPort && config.public && os.platform() !== "linux");
364
+ // usable if we can also own its external mapping on the router. Only worthwhile when a
365
+ // NAT sits between us and the internet; a directly-reachable public host needs no forward.
366
+ const doForward = !!(config.autoForwardPort && config.public) && await isBehindNAT();
368
367
 
369
368
  // Ensures the router's external mapping for `externalPort` belongs to us. Returns true
370
369
  // if it's ours to keep (existing-and-ours → refresh the lease; free → create and
371
370
  // confirm we won it), false if another machine owns it and we should try a new port.
372
371
  async function claimPortForward(externalPort: number): Promise<boolean> {
373
- const ourIP = getLocalInternalIP();
372
+ const ourIP = await getLocalInternalIP();
374
373
  const matches = (m: PortMapping) => m.externalPort === externalPort && m.protocol.toUpperCase() === "TCP";
375
374
 
376
375
  const existing = (await listPortMappings()).find(matches);
package/test.ts CHANGED
@@ -1,17 +1,21 @@
1
- import * as fs from "fs";
2
- import * as path from "path";
3
- import { listPortMappings } from "./src/forwardPort";
1
+ import { listPortMappings, getLocalInternalIP, isBehindNAT } from "./src/forwardPort";
4
2
 
5
3
 
6
4
  async function main() {
5
+ let ourIP = await getLocalInternalIP();
6
+ let behindNAT = await isBehindNAT();
7
+ console.log(`Outbound / local IP: ${ourIP ?? "(could not determine)"}`);
8
+ console.log(`Behind NAT (forwarding ${behindNAT ? "WILL" : "will NOT"} be attempted): ${behindNAT}`);
9
+
7
10
  let mappings = await listPortMappings();
8
11
  console.log(`Found ${mappings.length} port mapping(s):`);
9
12
  for (let mapping of mappings) {
10
13
  let host = mapping.remoteHost || "*";
11
14
  let lease = mapping.leaseDuration === 0 ? "permanent" : `${mapping.leaseDuration}s left`;
12
- console.log(` ${mapping.protocol} ${host}:${mapping.externalPort} -> ${mapping.internalClient}:${mapping.internalPort} (${mapping.enabled ? "enabled" : "disabled"}, ${lease})${mapping.description ? ` "${mapping.description}"` : ""}`);
15
+ let ours = ourIP && mapping.internalClient === ourIP ? " <-- OURS" : "";
16
+ console.log(` ${mapping.protocol} ${host}:${mapping.externalPort} -> ${mapping.internalClient}:${mapping.internalPort} (${mapping.enabled ? "enabled" : "disabled"}, ${lease})${mapping.description ? ` "${mapping.description}"` : ""}${ours}`);
13
17
  }
14
18
  console.table(mappings);
15
19
  }
16
20
 
17
- main().catch(e => console.error(e)).finally(() => process.exit(0));
21
+ main().catch(e => console.error(e.stack ?? e)).finally(() => process.exit(0));