socket-function 1.2.11 → 1.2.12
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/.claude/settings.local.json +8 -0
- package/index.d.ts +6 -1
- package/package.json +1 -1
- package/src/forwardPort.d.ts +6 -1
- package/src/forwardPort.ts +64 -66
- package/src/webSocketServer.ts +5 -6
- package/test.ts +9 -5
|
@@ -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
|
|
package/package.json
CHANGED
package/src/forwardPort.d.ts
CHANGED
|
@@ -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>;
|
package/src/forwardPort.ts
CHANGED
|
@@ -15,14 +15,18 @@ async function resolveGateway(): Promise<{
|
|
|
15
15
|
controlPort: number;
|
|
16
16
|
controlURLs: string[];
|
|
17
17
|
}> {
|
|
18
|
-
|
|
19
|
-
if (!
|
|
18
|
+
let internalIP = await getOutboundIP();
|
|
19
|
+
if (!internalIP) throw new Error("Could not determine our local network address");
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
|
112
|
+
export async function getLocalInternalIP(): Promise<string | undefined> {
|
|
113
|
+
return getOutboundIP();
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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/webSocketServer.ts
CHANGED
|
@@ -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.
|
|
366
|
-
//
|
|
367
|
-
const doForward = !!(config.autoForwardPort && config.public &&
|
|
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
|
|
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
|
-
|
|
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));
|