moshcode 0.82.0 → 0.84.0
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/package.json +1 -1
- package/src/dns-service.mjs +194 -0
- package/src/dns.mjs +129 -5
package/package.json
CHANGED
package/src/dns-service.mjs
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// is guessed and nothing depends on PATH.
|
|
31
31
|
import { spawn } from "node:child_process";
|
|
32
32
|
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
33
|
+
import { existsSync } from "node:fs";
|
|
33
34
|
import { homedir } from "node:os";
|
|
34
35
|
import { dirname, join } from "node:path";
|
|
35
36
|
|
|
@@ -171,3 +172,196 @@ export async function removeService({ system = false, home = homedir(), exec = r
|
|
|
171
172
|
steps.push({ step: `${cmd} ${[...flags, "daemon-reload"].join(" ")}`, ok: reload.ok, error: reload.error });
|
|
172
173
|
return { ok: true, path, scope, steps };
|
|
173
174
|
}
|
|
175
|
+
|
|
176
|
+
/* --------------------------------------------------- the pinned-TLS proxy */
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The other half of a machine that can actually reach Moshpit names.
|
|
180
|
+
*
|
|
181
|
+
* The bridge makes names resolve. It cannot make them verifiable: no CA will
|
|
182
|
+
* ever sign for `.eggs`, so without a proxy every name answers its origin's own
|
|
183
|
+
* self-signed leaf and a stock client refuses it. moshpit-proxy terminates TLS
|
|
184
|
+
* with a local root instead — one root for every ending, rather than trusting
|
|
185
|
+
* certificates one name at a time.
|
|
186
|
+
*
|
|
187
|
+
* moshpit-proxy ships no unit of its own, so nothing ever started it. It was
|
|
188
|
+
* installed, trusted, and idle, which reads exactly like "not installed" from
|
|
189
|
+
* every direction: nothing on 443, no certificate, and `dns enable` correctly
|
|
190
|
+
* reporting no proxy on a machine that had one.
|
|
191
|
+
*/
|
|
192
|
+
export const PROXY_UNIT_NAME = "moshpit-proxy.service";
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* A system unit, unlike the bridge's.
|
|
196
|
+
*
|
|
197
|
+
* 443 is privileged and the proxy must have it: DNS carries an address and has
|
|
198
|
+
* nowhere to put a port, so a browser sent to a Moshpit name goes to 443 or
|
|
199
|
+
* nowhere. A user unit cannot bind it. So this is a system unit that drops to
|
|
200
|
+
* the operator's account and is granted the one capability it needs — rather
|
|
201
|
+
* than running as root, which it has no other use for.
|
|
202
|
+
*/
|
|
203
|
+
export function proxyServicePaths() {
|
|
204
|
+
return { path: join("/etc/systemd/system", PROXY_UNIT_NAME), systemctl: ["systemctl"], scope: "system" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The unit text, pinned to this install.
|
|
209
|
+
*
|
|
210
|
+
* `ExecStart` runs moshpit-proxy's own wrapper rather than reaching past it to
|
|
211
|
+
* an entry script, so a change to that project's layout does not silently break
|
|
212
|
+
* this. The wrapper execs `node`, which systemd's PATH does not have on a mise,
|
|
213
|
+
* nvm or asdf box — so PATH is set from the interpreter running this code,
|
|
214
|
+
* which is by definition one that works. That mistake has now been made three
|
|
215
|
+
* times in this codebase; it is made here on purpose and only once.
|
|
216
|
+
*/
|
|
217
|
+
export function proxyServiceUnit({
|
|
218
|
+
wrapper,
|
|
219
|
+
nodeDir,
|
|
220
|
+
home = homedir(),
|
|
221
|
+
user = process.env.SUDO_USER || process.env.USER || process.env.LOGNAME,
|
|
222
|
+
port = 443,
|
|
223
|
+
tlds = [],
|
|
224
|
+
} = {}) {
|
|
225
|
+
if (!wrapper) throw new Error("proxyServiceUnit needs the moshpit-proxy wrapper path");
|
|
226
|
+
if (!user) throw new Error("proxyServiceUnit needs the account the proxy runs as");
|
|
227
|
+
|
|
228
|
+
const path = [nodeDir, "/usr/local/bin", "/usr/bin", "/bin"].filter(Boolean).join(":");
|
|
229
|
+
// `.moshpit` under the operator's home is where the local root already lives,
|
|
230
|
+
// put there by moshpit-proxy's own installer. Naming it explicitly keeps the
|
|
231
|
+
// service off /root/.moshpit, which is where a system unit would otherwise
|
|
232
|
+
// look and where there is nothing.
|
|
233
|
+
const dir = join(home, ".moshpit");
|
|
234
|
+
|
|
235
|
+
const lines = [
|
|
236
|
+
"# Generated by `moshcode dns enable`. Regenerate rather than editing:",
|
|
237
|
+
"# the paths below are this install's, and a node or proxy that moves leaves",
|
|
238
|
+
"# a unit that fails at 203/EXEC with nothing else to say.",
|
|
239
|
+
"[Unit]",
|
|
240
|
+
"Description=Moshpit pinned-TLS proxy",
|
|
241
|
+
"Documentation=https://github.com/profullstack/moshpit-proxy",
|
|
242
|
+
"After=network-online.target",
|
|
243
|
+
"Wants=network-online.target",
|
|
244
|
+
"",
|
|
245
|
+
"[Service]",
|
|
246
|
+
"Type=simple",
|
|
247
|
+
`User=${user}`,
|
|
248
|
+
`Environment=PATH=${path}`,
|
|
249
|
+
`Environment=MOSHPIT_PROXY_PORT=${port}`,
|
|
250
|
+
`Environment=MOSHPIT_PROXY_DIR=${dir}`,
|
|
251
|
+
];
|
|
252
|
+
// Only the endings it is asked to serve. Left unset it defaults to `.moshpit`
|
|
253
|
+
// alone, which is why a proxy can be running, healthy, and unable to present
|
|
254
|
+
// a certificate for the name someone is actually trying to reach.
|
|
255
|
+
if (tlds.length) lines.push(`Environment=MOSHPIT_PROXY_TLDS=${tlds.join(",")}`);
|
|
256
|
+
|
|
257
|
+
lines.push(
|
|
258
|
+
`ExecStart=${wrapper}`,
|
|
259
|
+
"Restart=always",
|
|
260
|
+
"RestartSec=2",
|
|
261
|
+
// The whole reason this is a system unit. Granted rather than inherited:
|
|
262
|
+
// the proxy runs as the operator and needs exactly one privilege.
|
|
263
|
+
"AmbientCapabilities=CAP_NET_BIND_SERVICE",
|
|
264
|
+
"CapabilityBoundingSet=CAP_NET_BIND_SERVICE",
|
|
265
|
+
"NoNewPrivileges=yes",
|
|
266
|
+
"PrivateTmp=yes",
|
|
267
|
+
"",
|
|
268
|
+
"[Install]",
|
|
269
|
+
"WantedBy=multi-user.target",
|
|
270
|
+
"",
|
|
271
|
+
);
|
|
272
|
+
return lines.join("\n");
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Where moshpit-proxy's installer puts its wrapper, if it ran. */
|
|
276
|
+
export function proxyWrapperPath({ home = homedir(), exists = existsSync } = {}) {
|
|
277
|
+
const candidate = join(home, ".local/bin/moshpit-proxy");
|
|
278
|
+
return exists(candidate) ? candidate : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Put the proxy under supervision, and wait for it to actually hold 443.
|
|
283
|
+
*
|
|
284
|
+
* Returns a plain report rather than throwing, and every caller treats a
|
|
285
|
+
* failure as "no proxy" rather than as a failed run. A machine without a
|
|
286
|
+
* working proxy resolves Moshpit names and cannot verify them, which is worse
|
|
287
|
+
* than it sounds but is still enormously better than a machine whose DNS was
|
|
288
|
+
* refused because an optional component would not start.
|
|
289
|
+
*
|
|
290
|
+
* `listening` is asked rather than assumed: `Type=simple` reports active the
|
|
291
|
+
* moment it forks, so "started" and "serving" are different questions and this
|
|
292
|
+
* has to answer the second one. The proxy fetches a registry pin before it can
|
|
293
|
+
* answer, so the wait is generous.
|
|
294
|
+
*/
|
|
295
|
+
export async function ensureProxyService({
|
|
296
|
+
home = homedir(),
|
|
297
|
+
user = process.env.SUDO_USER || process.env.USER || process.env.LOGNAME,
|
|
298
|
+
nodeDir = dirname(process.execPath),
|
|
299
|
+
tlds = [],
|
|
300
|
+
port = 443,
|
|
301
|
+
exec = run,
|
|
302
|
+
read = async (f) => (await import("node:fs/promises")).readFile(f, "utf8"),
|
|
303
|
+
listening = defaultPortHeld,
|
|
304
|
+
waitMs = 30000,
|
|
305
|
+
} = {}) {
|
|
306
|
+
const wrapper = proxyWrapperPath({ home });
|
|
307
|
+
if (!wrapper) return { ok: false, reason: "not-installed", steps: [] };
|
|
308
|
+
|
|
309
|
+
const { path, systemctl } = proxyServicePaths();
|
|
310
|
+
const unit = proxyServiceUnit({ wrapper, nodeDir, home, user, port, tlds });
|
|
311
|
+
// Read before writing so the manifest can put back whatever was here — which
|
|
312
|
+
// is usually nothing, and "nothing" has to be recorded as precisely as
|
|
313
|
+
// content would be, or disable leaves a unit nobody asked for.
|
|
314
|
+
const before = await read(path).catch(() => null);
|
|
315
|
+
|
|
316
|
+
const steps = [];
|
|
317
|
+
try {
|
|
318
|
+
await mkdir(dirname(path), { recursive: true });
|
|
319
|
+
await writeFile(path, unit);
|
|
320
|
+
steps.push({ step: `wrote ${path}`, ok: true });
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return { ok: false, reason: "write-failed", error: error.message, before, steps };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const [cmd, ...flags] = systemctl;
|
|
326
|
+
for (const args of [[...flags, "daemon-reload"], [...flags, "enable", "--now", PROXY_UNIT_NAME]]) {
|
|
327
|
+
const result = await exec(cmd, args);
|
|
328
|
+
steps.push({ step: `${cmd} ${args.join(" ")}`, ok: result.ok, error: result.error });
|
|
329
|
+
if (!result.ok) return { ok: false, reason: "systemctl-failed", before, steps, path, unit };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const held = await listening(port, waitMs);
|
|
333
|
+
steps.push({ step: `proxy holds 127.0.0.1:${port}`, ok: held });
|
|
334
|
+
return { ok: held, reason: held ? null : "not-listening", before, steps, path, unit };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Poll rather than sleep once: a proxy that comes up in 2s should not cost 30. */
|
|
338
|
+
async function defaultPortHeld(port, waitMs) {
|
|
339
|
+
const { connect } = await import("node:net");
|
|
340
|
+
const deadline = Date.now() + waitMs;
|
|
341
|
+
while (Date.now() < deadline) {
|
|
342
|
+
const open = await new Promise((resolve) => {
|
|
343
|
+
const socket = connect({ host: "127.0.0.1", port });
|
|
344
|
+
const done = (v) => { try { socket.destroy(); } catch { /* gone */ } resolve(v); };
|
|
345
|
+
socket.once("connect", () => done(true));
|
|
346
|
+
socket.once("error", () => done(false));
|
|
347
|
+
setTimeout(() => done(false), 1000);
|
|
348
|
+
});
|
|
349
|
+
if (open) return true;
|
|
350
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Take the proxy service away. Missing is not a failure. */
|
|
356
|
+
export async function removeProxyService({ exec = run } = {}) {
|
|
357
|
+
const { path, systemctl } = proxyServicePaths();
|
|
358
|
+
const [cmd, ...flags] = systemctl;
|
|
359
|
+
const steps = [];
|
|
360
|
+
const off = await exec(cmd, [...flags, "disable", "--now", PROXY_UNIT_NAME]);
|
|
361
|
+
steps.push({ step: `${cmd} ${[...flags, "disable", "--now", PROXY_UNIT_NAME].join(" ")}`, ok: off.ok, error: off.error });
|
|
362
|
+
await rm(path, { force: true });
|
|
363
|
+
steps.push({ step: `removed ${path}`, ok: true });
|
|
364
|
+
const reload = await exec(cmd, [...flags, "daemon-reload"]);
|
|
365
|
+
steps.push({ step: `${cmd} ${[...flags, "daemon-reload"].join(" ")}`, ok: reload.ok, error: reload.error });
|
|
366
|
+
return { ok: true, path, steps };
|
|
367
|
+
}
|
package/src/dns.mjs
CHANGED
|
@@ -1664,6 +1664,37 @@ export function dnsmasqCatchAllConf({ host = DEFAULT_HOST, port = DEFAULT_PORT }
|
|
|
1664
1664
|
* Repeatable and comma-separated both work. `address#port` matches resolv.conf
|
|
1665
1665
|
* and dnsmasq rather than inventing a third spelling.
|
|
1666
1666
|
*/
|
|
1667
|
+
/**
|
|
1668
|
+
* Which name to detect the pinned-TLS proxy with.
|
|
1669
|
+
*
|
|
1670
|
+
* The probe is a TLS handshake with the name in SNI, and the proxy can only
|
|
1671
|
+
* present a certificate for a name that really exists — it fetches the
|
|
1672
|
+
* registry pin to mint one. A synthesised `a.<ending>` is never real, so the
|
|
1673
|
+
* handshake yields no certificate and a proxy that is installed, trusted and
|
|
1674
|
+
* listening reports as absent. Measured against a running proxy:
|
|
1675
|
+
*
|
|
1676
|
+
* a.moshpit -> no certificate
|
|
1677
|
+
* a.2600 -> no certificate
|
|
1678
|
+
* alt.2600 -> issuer=CN=Moshpit Local CA
|
|
1679
|
+
*
|
|
1680
|
+
* No registry endpoint lists names, so a real one cannot be discovered here.
|
|
1681
|
+
* `--proxy-probe <name>` supplies it. A bare flag is a typo rather than a
|
|
1682
|
+
* request to probe with nothing, and is reported instead of silently falling
|
|
1683
|
+
* back to the synthetic name that cannot work.
|
|
1684
|
+
*/
|
|
1685
|
+
export function proxyProbeFromArgs(args = [], claimed = []) {
|
|
1686
|
+
const at = args.indexOf("--proxy-probe");
|
|
1687
|
+
if (at >= 0) {
|
|
1688
|
+
const value = args[at + 1];
|
|
1689
|
+
if (value === undefined || value.startsWith("--")) return { name: null, invalid: true };
|
|
1690
|
+
return { name: value, invalid: false };
|
|
1691
|
+
}
|
|
1692
|
+
// The historical default. Kept because it is right on a machine whose proxy
|
|
1693
|
+
// serves every ending, and because removing it would turn "detected nothing"
|
|
1694
|
+
// into "refused to look".
|
|
1695
|
+
return { name: claimed[0] ? `a.${claimed[0]}` : null, invalid: false };
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1667
1698
|
export function upstreamsFromArgs(args = []) {
|
|
1668
1699
|
const servers = [];
|
|
1669
1700
|
const invalid = [];
|
|
@@ -2230,12 +2261,21 @@ export async function captureRestorePoint({
|
|
|
2230
2261
|
dropins = readDropins,
|
|
2231
2262
|
read = defaultReadMaybe,
|
|
2232
2263
|
now = () => new Date().toISOString(),
|
|
2264
|
+
// Files this run creates that are not part of the resolver plan — the two
|
|
2265
|
+
// service units, and the local root in the system trust store. Recorded the
|
|
2266
|
+
// same way as everything else: prior content, or null for "was not here", so
|
|
2267
|
+
// `disable` replays rather than guesses. A unit removed because it happened
|
|
2268
|
+
// to exist is the failure this shape prevents.
|
|
2269
|
+
extraPaths = [],
|
|
2233
2270
|
} = {}) {
|
|
2234
2271
|
const files = new Map();
|
|
2235
2272
|
for (const file of await dropins({ dir }).catch(() => [])) {
|
|
2236
2273
|
if (!dropinNameservers(file.content).length && !dropinDomains(file.content).length) continue;
|
|
2237
2274
|
files.set(`${dir}/${file.name}`, file.content);
|
|
2238
2275
|
}
|
|
2276
|
+
for (const path of extraPaths) {
|
|
2277
|
+
if (path && !files.has(path)) files.set(path, await read(path));
|
|
2278
|
+
}
|
|
2239
2279
|
for (const step of plan?.steps || []) {
|
|
2240
2280
|
if (step.kind !== "write" && step.kind !== "remove") continue;
|
|
2241
2281
|
if (!files.has(step.path)) files.set(step.path, await read(step.path));
|
|
@@ -2337,7 +2377,7 @@ import { readFile, writeFile } from "node:fs/promises";
|
|
|
2337
2377
|
import { existsSync } from "node:fs";
|
|
2338
2378
|
import { fileURLToPath } from "node:url";
|
|
2339
2379
|
import { isRealTld } from "./iana-tlds.mjs";
|
|
2340
|
-
import { installService, removeService, serviceUnit, servicePaths, UNIT_NAME } from "./dns-service.mjs";
|
|
2380
|
+
import { installService, removeService, serviceUnit, servicePaths, UNIT_NAME, ensureProxyService, removeProxyService, proxyServicePaths, proxyWrapperPath } from "./dns-service.mjs";
|
|
2341
2381
|
import {
|
|
2342
2382
|
applyPlan, daemonStatus, describePlan, detectPlatform, disablePlan, enablePlan,
|
|
2343
2383
|
probeResolver, requiredPort, startDaemon, stopDaemon,
|
|
@@ -2377,6 +2417,8 @@ const USAGE = `moshcode dns — resolve Moshpit names on this machine
|
|
|
2377
2417
|
running across reboots; --write installs and
|
|
2378
2418
|
starts it, --system for a system unit rather
|
|
2379
2419
|
than this user's, --remove takes it away
|
|
2420
|
+
--proxy-probe NAME a real Moshpit name to detect
|
|
2421
|
+
the pinned-TLS proxy with (it cannot serve a made-up one)
|
|
2380
2422
|
|
|
2381
2423
|
moshcode dns filter block ads, trackers, malware and phishing at the
|
|
2382
2424
|
resolver — \`moshcode dns filter help\` for the verbs
|
|
@@ -2440,6 +2482,12 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
2440
2482
|
findLocalProxyImpl = findLocalProxy,
|
|
2441
2483
|
autoTrustImpl = createAutoTrust,
|
|
2442
2484
|
stopBridge = stopDaemon,
|
|
2485
|
+
// The two proxy-service calls, injected for the same reason as every
|
|
2486
|
+
// other system call here: a test must be able to exercise the branch
|
|
2487
|
+
// without shelling out to systemctl or writing to /etc.
|
|
2488
|
+
ensureProxy = ensureProxyService,
|
|
2489
|
+
removeProxy = removeProxyService,
|
|
2490
|
+
proxyWrapper = proxyWrapperPath,
|
|
2443
2491
|
dropins = readDropins,
|
|
2444
2492
|
manifestFile = manifestPath(),
|
|
2445
2493
|
readManifest = async (path) => parseManifest(await defaultReadMaybe(path)),
|
|
@@ -2792,10 +2840,31 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
2792
2840
|
// that resolves but cannot complete a TLS handshake reads as broken to the
|
|
2793
2841
|
// person who typed the URL. Asked now, while this command can ask; the unit
|
|
2794
2842
|
// it writes runs at boot and has no way to find out later.
|
|
2843
|
+
// The probe is a TLS handshake with the name in SNI, and the proxy can only
|
|
2844
|
+
// present a certificate for a name that actually exists — it fetches the
|
|
2845
|
+
// registry pin to make one. A synthesised `a.<ending>` is never a real name,
|
|
2846
|
+
// so the handshake yields no certificate and a proxy that is installed,
|
|
2847
|
+
// trusted and listening reports as absent. Measured on a running proxy:
|
|
2848
|
+
//
|
|
2849
|
+
// a.moshpit -> no certificate
|
|
2850
|
+
// a.2600 -> no certificate
|
|
2851
|
+
// alt.2600 -> issuer=CN=Moshpit Local CA
|
|
2852
|
+
//
|
|
2853
|
+
// There is no registry endpoint that lists names, so the tool cannot find a
|
|
2854
|
+
// real one by itself. `--proxy-probe <name>` supplies one. The issuer check
|
|
2855
|
+
// still runs against it: naming a probe says which name to ask about, never
|
|
2856
|
+
// that a proxy is there.
|
|
2795
2857
|
let proxy = null;
|
|
2796
2858
|
if (!rest.includes("--no-proxy")) {
|
|
2797
|
-
const
|
|
2798
|
-
|
|
2859
|
+
const explicit = proxyProbeFromArgs(rest);
|
|
2860
|
+
if (explicit.invalid) {
|
|
2861
|
+
out("! --proxy-probe needs a Moshpit name, e.g. --proxy-probe blue.eggs");
|
|
2862
|
+
out("");
|
|
2863
|
+
}
|
|
2864
|
+
// The registry is only consulted when no name was given: fetching 18000
|
|
2865
|
+
// endings to build a probe that cannot work is pure cost.
|
|
2866
|
+
const claimed = explicit.name ? [] : await fetchTlds({ registryBase }).catch(() => []);
|
|
2867
|
+
const probeName = proxyProbeFromArgs(rest, claimed).name;
|
|
2799
2868
|
if (probeName) {
|
|
2800
2869
|
const local = await findLocalProxyImpl(probeName).catch(() => ({ found: false }));
|
|
2801
2870
|
if (local.found) proxy = local.address.v4 || local.address.v6;
|
|
@@ -2810,9 +2879,14 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
2810
2879
|
} else if (!rest.includes("--no-proxy")) {
|
|
2811
2880
|
out("! no pinned-TLS proxy found on this machine");
|
|
2812
2881
|
out(" names will answer their origin, and a stock client cannot verify those —");
|
|
2813
|
-
out(" https:// will fail even though the name resolves.
|
|
2882
|
+
out(" https:// will fail even though the name resolves.");
|
|
2883
|
+
out("");
|
|
2884
|
+
out(" If it is not installed:");
|
|
2814
2885
|
out(" curl -fsSL https://raw.githubusercontent.com/profullstack/moshpit-proxy/main/install.sh | sh");
|
|
2815
|
-
out("
|
|
2886
|
+
out("");
|
|
2887
|
+
out(" If it IS installed and listening, the probe used a name that does not");
|
|
2888
|
+
out(" exist — the proxy can only serve a certificate for a real one. Name one:");
|
|
2889
|
+
out(" moshcode dns service --proxy-probe <a.real.name> --write");
|
|
2816
2890
|
out("");
|
|
2817
2891
|
}
|
|
2818
2892
|
|
|
@@ -3122,6 +3196,30 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
3122
3196
|
|
|
3123
3197
|
const stopped = await stopBridge();
|
|
3124
3198
|
out(stopped.stopped ? " ok bridge stopped" : ` ok bridge was not running${stopped.reason ? ` (${stopped.reason})` : ""}`);
|
|
3199
|
+
|
|
3200
|
+
// The proxy service `enable` installed. Taken away here, because a
|
|
3201
|
+
// supervised proxy left holding 443 after Moshpit is turned off is a
|
|
3202
|
+
// service the operator never asked to keep and would not think to look
|
|
3203
|
+
// for. Safe from an escalated run: it is a system unit, so root is
|
|
3204
|
+
// exactly the right context to stop it in.
|
|
3205
|
+
//
|
|
3206
|
+
// The manifest still carries whatever was at that path beforehand, so a
|
|
3207
|
+
// machine that already had a unit of its own gets it back rather than
|
|
3208
|
+
// losing it to a cleanup it never asked for.
|
|
3209
|
+
// Only when there is one. A unit that was never installed needs no
|
|
3210
|
+
// systemctl call to not exist, and reaching for /etc on a machine that
|
|
3211
|
+
// never had a proxy is a side effect nobody asked this command for.
|
|
3212
|
+
// Gated on the restore point, not on what happens to be in /etc. `disable`
|
|
3213
|
+
// undoes what `enable` did; a proxy unit this tool never installed is
|
|
3214
|
+
// somebody else's, and stopping it because it shares a filename is
|
|
3215
|
+
// exactly the guessing the manifest exists to prevent.
|
|
3216
|
+
const proxyWasOurs = (restore?.files || []).some((f) => f.path === proxyServicePaths().path);
|
|
3217
|
+
if (platform === "linux" && proxyWasOurs) {
|
|
3218
|
+
const px = await removeProxy();
|
|
3219
|
+
for (const step of px.steps || []) {
|
|
3220
|
+
if (step.ok) out(` ok ${step.step}`);
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3125
3223
|
// Consumed. Leaving it would let a later `disable` restore a machine to a
|
|
3126
3224
|
// state that is two changes old.
|
|
3127
3225
|
if (restore) {
|
|
@@ -3156,12 +3254,38 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
3156
3254
|
// run killed halfway leaves behind the one thing needed to undo it. The
|
|
3157
3255
|
// per-file backup covers the file this run overwrites; this covers the
|
|
3158
3256
|
// machine, which is a different question and the one `disable` has to ask.
|
|
3257
|
+
// Supervised before anything goes looking for it. moshpit-proxy ships no
|
|
3258
|
+
// unit of its own, so on every machine that installed it, it sat there
|
|
3259
|
+
// installed, trusted and never started — which is indistinguishable from
|
|
3260
|
+
// absent: nothing on 443, no certificate, and this command correctly
|
|
3261
|
+
// reporting no proxy on a box that had one.
|
|
3262
|
+
//
|
|
3263
|
+
// Non-fatal in every direction. A machine without a working proxy resolves
|
|
3264
|
+
// Moshpit names and cannot verify them, which is bad; a machine whose DNS
|
|
3265
|
+
// was refused because an optional component would not start is worse.
|
|
3266
|
+
let proxyUnitPath = null;
|
|
3267
|
+
if (!rest.includes("--no-proxy") && platform === "linux") {
|
|
3268
|
+
if (!proxyWrapper()) {
|
|
3269
|
+
out(" -- no pinned-TLS proxy installed — https:// on a name will not verify");
|
|
3270
|
+
out(" moshcode update installs it, or:");
|
|
3271
|
+
out(" curl -fsSL https://raw.githubusercontent.com/profullstack/moshpit-proxy/main/install.sh | sh");
|
|
3272
|
+
} else {
|
|
3273
|
+
proxyUnitPath = proxyServicePaths().path;
|
|
3274
|
+
const ensured = await ensureProxy({ tlds });
|
|
3275
|
+
for (const step of ensured.steps || []) {
|
|
3276
|
+
out(` ${step.ok ? "ok " : "-- "} ${step.step}${step.error ? ` — ${step.error}` : ""}`);
|
|
3277
|
+
}
|
|
3278
|
+
if (!ensured.ok) out(` -- the proxy is not serving (${ensured.reason}) — https:// will not verify`);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3159
3282
|
const point = await captureRestorePoint({
|
|
3160
3283
|
plan,
|
|
3161
3284
|
platform,
|
|
3162
3285
|
backend: platform === "linux" ? linuxBackend : platform,
|
|
3163
3286
|
bridge: `${DEFAULT_HOST}:${wanted}`,
|
|
3164
3287
|
dropins,
|
|
3288
|
+
extraPaths: [proxyUnitPath, servicePaths({ system: false }).path].filter(Boolean),
|
|
3165
3289
|
});
|
|
3166
3290
|
const recorded2 = await applyPlan({
|
|
3167
3291
|
steps: [{ kind: "write", path: manifestFile, content: `${JSON.stringify(point, null, 2)}\n`, why: "so disable can put this machine back" }],
|