socket-function 1.2.21 → 1.2.23

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/index.d.ts CHANGED
@@ -984,6 +984,9 @@ declare module "socket-function/src/forwardPort" {
984
984
  externalPort: number;
985
985
  internalPort: number;
986
986
  duration?: number;
987
+ /** Keep the lease alive by re-creating the mapping at 80% of `duration`, forever. On by
988
+ * default, which is what you want — an un-maintained forward silently expires. */
989
+ autoRenew?: boolean;
987
990
  }): Promise<void>;
988
991
  /** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
989
992
  * mapping points at us or at a different machine on the network. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "socket-function",
3
- "version": "1.2.21",
3
+ "version": "1.2.23",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "dependencies": {
@@ -19,6 +19,9 @@ export declare function forwardPort(config: {
19
19
  externalPort: number;
20
20
  internalPort: number;
21
21
  duration?: number;
22
+ /** Keep the lease alive by re-creating the mapping at 80% of `duration`, forever. On by
23
+ * default, which is what you want — an un-maintained forward silently expires. */
24
+ autoRenew?: boolean;
22
25
  }): Promise<void>;
23
26
  /** Our machine's LAN IP, as the router sees it — used to tell whether an existing port
24
27
  * mapping points at us or at a different machine on the network. */
@@ -74,36 +74,58 @@ export async function listPortMappings(): Promise<PortMapping[]> {
74
74
  throw new Error(`Failed to list port mappings, could not find a working controlURL. Last error: ${(lastError as Error)?.stack ?? lastError}`);
75
75
  }
76
76
 
77
+ // A UPnP lease expires on its own, so a forward left un-renewed silently dies partway through.
78
+ // Renew at 80% of the duration: enough margin to cover a failed attempt (we retry on the next
79
+ // tick) while the previous lease is still live, so the mapping never actually lapses.
80
+ const LEASE_RENEW_FRACTION = 0.8;
81
+
77
82
  export async function forwardPort(config: {
78
83
  externalPort: number;
79
84
  internalPort: number;
80
85
  duration?: number;
86
+ /** Keep the lease alive by re-creating the mapping at 80% of `duration`, forever. On by
87
+ * default, which is what you want — an un-maintained forward silently expires. */
88
+ autoRenew?: boolean;
81
89
  }) {
82
- try {
83
- const { externalPort, internalPort } = config;
84
- let duration = config.duration ?? timeInHour;
85
-
86
- const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
90
+ let duration = config.duration ?? timeInHour;
91
+ let autoRenew = config.autoRenew ?? true;
87
92
 
88
- for (let controlURL of controlURLs) {
89
- try {
90
- await createPortMapping({
91
- externalPort, internalPort,
92
- gatewayIP,
93
- controlPort,
94
- controlPath: controlURL,
95
- internalIP,
96
- duration,
97
- });
98
- console.log(`Port mapping created on ${gatewayIP}:${externalPort} -> ${internalIP}:${internalPort}`);
99
- return;
100
- } catch (e) {
101
- console.error(`Failed to create port mapping using controlURL ${controlURL}`, e);
93
+ async function createMapping() {
94
+ try {
95
+ const { externalPort, internalPort } = config;
96
+ const { internalIP, gatewayIP, controlPort, controlURLs } = await resolveGateway();
97
+
98
+ for (let controlURL of controlURLs) {
99
+ try {
100
+ await createPortMapping({
101
+ externalPort, internalPort,
102
+ gatewayIP,
103
+ controlPort,
104
+ controlPath: controlURL,
105
+ internalIP,
106
+ duration,
107
+ });
108
+ console.log(`Port mapping created on ${gatewayIP}:${externalPort} -> ${internalIP}:${internalPort}`);
109
+ return;
110
+ } catch (e) {
111
+ console.error(`Failed to create port mapping using controlURL ${controlURL}`, e);
112
+ }
102
113
  }
114
+ console.error("Failed to create port mapping, could not find controlURL");
115
+ } catch (e) {
116
+ console.error("Error in forwardPort", e);
103
117
  }
104
- console.error("Failed to create port mapping, could not find controlURL");
105
- } catch (e) {
106
- console.error("Error in forwardPort", e);
118
+ }
119
+
120
+ await createMapping();
121
+
122
+ if (autoRenew) {
123
+ // unref so a script that only wants a one-off forward (and then exits) isn't held open by
124
+ // the renewal timer; a running server stays alive on its own and the renewals keep firing.
125
+ let timer = setInterval(() => {
126
+ void createMapping();
127
+ }, Math.max(1, Math.floor(duration * LEASE_RENEW_FRACTION)));
128
+ timer.unref?.();
107
129
  }
108
130
  }
109
131
 
@@ -35,6 +35,17 @@ function encodeFlagBit(b: boolean): string { return b ? "1" : "0"; }
35
35
  // connecting through a Let's Encrypt cert on a public domain.
36
36
  const WILDCARD_TARGET = "";
37
37
 
38
+ // The target match is on the host portion only, not the exact listening port. A
39
+ // server can restart (or rebind) on a new port while keeping the same host identity,
40
+ // and a client still holding the old nodeId (host:oldPort) should be accepted rather
41
+ // than rejected as a stale thread. Strip only the trailing :port (last colon segment),
42
+ // so hosts that themselves contain colons survive.
43
+ function stripPort(nodeId: string): string {
44
+ let idx = nodeId.lastIndexOf(":");
45
+ if (idx < 0) return nodeId;
46
+ return nodeId.slice(0, idx);
47
+ }
48
+
38
49
  function encodeOne(target: string, flags: ConnectionFlags): string {
39
50
  let plain = `${PROTOCOL_VERSION}|${target}|clz4=${encodeFlagBit(flags.clientLZ4)}|slz4=${encodeFlagBit(flags.serverLZ4)}`;
40
51
  return hexEncode(plain);
@@ -105,7 +116,8 @@ export function chooseProtocol(
105
116
  for (let hex of proposed) {
106
117
  let decoded = decodeProtocol(hex);
107
118
  if (!decoded) continue;
108
- if (decoded.target !== WILDCARD_TARGET && decoded.target !== serverNodeId) continue;
119
+ // NOTE: We allow the pork to be different. This doesn't degrade security as we're still using TLS, so proxies don't reduce our security, However, it does make it a lot easier for a server to listen on multiple ports and redirect them all to the single-mounted port (As socket function doesn't support mounting on multiple ports because it makes it complicated for code that wants to say where we mounted to if we could be mounted to multiple ports at once).
120
+ if (decoded.target !== WILDCARD_TARGET && stripPort(decoded.target) !== stripPort(serverNodeId)) continue;
109
121
  // Server capability check: if the proposal asks the server to receive
110
122
  // LZ4 (slz4=1) but the server doesn't support it, skip.
111
123
  if (decoded.flags.serverLZ4 && !serverCapabilities.lz4) continue;
@@ -11,8 +11,8 @@ import { parseSNIExtension, parseTLSHello, SNIType } from "./tlsParsing";
11
11
  import debugbreak from "debugbreak";
12
12
  import { getNodeId } from "./nodeCache";
13
13
  import crypto from "crypto";
14
- import { Watchable, getRootDomain, timeInHour, timeInMinute } from "./misc";
15
- import { delay, runInfinitePoll } from "./batching";
14
+ import { Watchable, getRootDomain } from "./misc";
15
+ import { delay } from "./batching";
16
16
  import { magenta, red } from "./formatting/logColors";
17
17
  import { yellow } from "./formatting/logColors";
18
18
  import { green } from "./formatting/logColors";
@@ -439,12 +439,9 @@ export async function startSocketServer(
439
439
  port = (realServer.address() as net.AddressInfo).port;
440
440
 
441
441
  if (doForward) {
442
- // The mapping is claimed above; keep refreshing the lease so it doesn't expire.
443
- async function refreshForward() {
444
- await forwardPort({ externalPort: port, internalPort: port });
445
- console.log(magenta(`Refreshed port forward ${port} to our machine`));
446
- }
447
- runInfinitePoll(timeInMinute * 30, refreshForward);
442
+ // The mapping is claimed above; this establishes the maintained forward, which
443
+ // auto-renews its own lease (at 80% of the duration) so it doesn't expire.
444
+ await forwardPort({ externalPort: port, internalPort: port });
448
445
  }
449
446
 
450
447
  let nodeId = getNodeId(getCommonName(config.cert), port);