socket-function 1.2.10 → 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/SocketFunction.d.ts +3 -2
- package/SocketFunction.ts +3 -3
- package/index.d.ts +9 -3
- package/package.json +1 -1
- package/src/CallFactory.ts +3 -3
- 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/SocketFunction.d.ts
CHANGED
|
@@ -10,11 +10,12 @@ type ExtractShape<ClassType, Shape> = {
|
|
|
10
10
|
export declare class SocketFunction {
|
|
11
11
|
static logMessages: boolean;
|
|
12
12
|
static trackMessageSizes: {
|
|
13
|
-
upload: ((size: number) => void)[];
|
|
14
|
-
download: ((size: number) => void)[];
|
|
13
|
+
upload: ((size: number, nodeId: string) => void)[];
|
|
14
|
+
download: ((size: number, nodeId: string) => void)[];
|
|
15
15
|
callTimes: ((obj: {
|
|
16
16
|
start: number;
|
|
17
17
|
end: number;
|
|
18
|
+
nodeId: string;
|
|
18
19
|
}) => void)[];
|
|
19
20
|
};
|
|
20
21
|
static MAX_MESSAGE_SIZE: number;
|
package/SocketFunction.ts
CHANGED
|
@@ -58,9 +58,9 @@ type ExtractShape<ClassType, Shape> = {
|
|
|
58
58
|
export class SocketFunction {
|
|
59
59
|
public static logMessages = false;
|
|
60
60
|
public static trackMessageSizes = {
|
|
61
|
-
upload: [] as ((size: number) => void)[],
|
|
62
|
-
download: [] as ((size: number) => void)[],
|
|
63
|
-
callTimes: [] as ((obj: { start: number; end: number; }) => void)[],
|
|
61
|
+
upload: [] as ((size: number, nodeId: string) => void)[],
|
|
62
|
+
download: [] as ((size: number, nodeId: string) => void)[],
|
|
63
|
+
callTimes: [] as ((obj: { start: number; end: number; nodeId: string; }) => void)[],
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
public static MAX_MESSAGE_SIZE = 1024 * 1024 * 32;
|
package/index.d.ts
CHANGED
|
@@ -19,11 +19,12 @@ declare module "socket-function/SocketFunction" {
|
|
|
19
19
|
export declare class SocketFunction {
|
|
20
20
|
static logMessages: boolean;
|
|
21
21
|
static trackMessageSizes: {
|
|
22
|
-
upload: ((size: number) => void)[];
|
|
23
|
-
download: ((size: number) => void)[];
|
|
22
|
+
upload: ((size: number, nodeId: string) => void)[];
|
|
23
|
+
download: ((size: number, nodeId: string) => void)[];
|
|
24
24
|
callTimes: ((obj: {
|
|
25
25
|
start: number;
|
|
26
26
|
end: number;
|
|
27
|
+
nodeId: string;
|
|
27
28
|
}) => void)[];
|
|
28
29
|
};
|
|
29
30
|
static MAX_MESSAGE_SIZE: number;
|
|
@@ -956,7 +957,12 @@ declare module "socket-function/src/forwardPort" {
|
|
|
956
957
|
}): Promise<void>;
|
|
957
958
|
/** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
|
|
958
959
|
* mapping points at us or at a different machine on the network. */
|
|
959
|
-
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>;
|
|
960
966
|
|
|
961
967
|
}
|
|
962
968
|
|
package/package.json
CHANGED
package/src/CallFactory.ts
CHANGED
|
@@ -277,7 +277,7 @@ export async function createCallFactory(
|
|
|
277
277
|
{
|
|
278
278
|
let resultSize = data.map(x => x.length).reduce((a, b) => a + b, 0);
|
|
279
279
|
for (let callback of SocketFunction.trackMessageSizes.upload) {
|
|
280
|
-
callback(resultSize);
|
|
280
|
+
callback(resultSize, nodeId);
|
|
281
281
|
}
|
|
282
282
|
if (SocketFunction.logMessages) {
|
|
283
283
|
let fncHack = "";
|
|
@@ -568,7 +568,7 @@ export async function createCallFactory(
|
|
|
568
568
|
}
|
|
569
569
|
let parseTime = Date.now() - time;
|
|
570
570
|
for (let callback of SocketFunction.trackMessageSizes.download) {
|
|
571
|
-
callback(resultSize);
|
|
571
|
+
callback(resultSize, nodeId);
|
|
572
572
|
}
|
|
573
573
|
|
|
574
574
|
let receiveStats: CompressionStats = {
|
|
@@ -659,7 +659,7 @@ export async function createCallFactory(
|
|
|
659
659
|
let start = time;
|
|
660
660
|
let end = Date.now();
|
|
661
661
|
for (let fnc of SocketFunction.trackMessageSizes.callTimes) {
|
|
662
|
-
fnc({ start, end });
|
|
662
|
+
fnc({ start, end, nodeId });
|
|
663
663
|
}
|
|
664
664
|
}
|
|
665
665
|
|
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));
|