moshcode 0.61.0 → 0.63.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-system.mjs +18 -2
- package/src/dns.mjs +119 -11
package/package.json
CHANGED
package/src/dns-system.mjs
CHANGED
|
@@ -332,8 +332,24 @@ function defaultRunner(command, args) {
|
|
|
332
332
|
* root to listen on 5354, and requiring it to write a pidfile somewhere
|
|
333
333
|
* privileged would make the whole daemon need privileges it otherwise does not.
|
|
334
334
|
*/
|
|
335
|
-
export function pidfilePath() {
|
|
336
|
-
|
|
335
|
+
export function pidfilePath(env = process.env, exists = existsSync) {
|
|
336
|
+
// `dns enable` escalates, so the run that *starts* the bridge is root and the
|
|
337
|
+
// runs that later ask about it are not. Under sudo both XDG_RUNTIME_DIR and
|
|
338
|
+
// HOME belong to root, so the pidfile went to /root/.moshcode — a path the
|
|
339
|
+
// unprivileged `dns status` and `dns disable` never look at and could not
|
|
340
|
+
// read if they did. The bridge was reported "not running" for the rest of its
|
|
341
|
+
// life, and the fix status advised started a second one on top of it.
|
|
342
|
+
//
|
|
343
|
+
// So an escalated run records against the invoking user's runtime dir, and
|
|
344
|
+
// only when that directory is really there: deriving /run/user/<uid> on a
|
|
345
|
+
// machine without one trades an unreadable path for a nonexistent one. macOS
|
|
346
|
+
// has no /run/user and falls through unchanged — the escalated paths this
|
|
347
|
+
// matters for are the systemd-resolved ones.
|
|
348
|
+
const invoker = env.SUDO_UID ? `/run/user/${env.SUDO_UID}` : null;
|
|
349
|
+
const base = (invoker && exists(invoker) ? invoker : null)
|
|
350
|
+
|| env.XDG_RUNTIME_DIR
|
|
351
|
+
|| join(homedir(), ".moshcode")
|
|
352
|
+
|| tmpdir();
|
|
337
353
|
return join(base, "moshpit-dns.pid");
|
|
338
354
|
}
|
|
339
355
|
|
package/src/dns.mjs
CHANGED
|
@@ -1742,7 +1742,7 @@ export function parseUdpListeners(text) {
|
|
|
1742
1742
|
return out;
|
|
1743
1743
|
}
|
|
1744
1744
|
|
|
1745
|
-
const defaultUdpListeners = async () => {
|
|
1745
|
+
export const defaultUdpListeners = async () => {
|
|
1746
1746
|
const { execFile } = await import("node:child_process");
|
|
1747
1747
|
const text = await new Promise((resolve) => {
|
|
1748
1748
|
execFile("ss", ["-lnup"], { timeout: 5000 }, (err, stdout) => resolve(err ? "" : String(stdout)));
|
|
@@ -1783,6 +1783,89 @@ export function portHolder(listeners, { host = DEFAULT_HOST, port = DEFAULT_PORT
|
|
|
1783
1783
|
return null;
|
|
1784
1784
|
}
|
|
1785
1785
|
|
|
1786
|
+
/**
|
|
1787
|
+
* What is actually on the bridge's port, rather than what our pidfile claims.
|
|
1788
|
+
*
|
|
1789
|
+
* `status` used to answer this from the pidfile alone, and that file only ever
|
|
1790
|
+
* describes a bridge *this tool* started, in *this* privilege context. Every
|
|
1791
|
+
* other way a bridge reaches 5354 read as "not running": a systemd unit, a
|
|
1792
|
+
* hand-started `dns start`, or — the common one — an `enable` that escalated to
|
|
1793
|
+
* root and therefore wrote its pidfile under root's HOME instead of the
|
|
1794
|
+
* invoking user's runtime dir.
|
|
1795
|
+
*
|
|
1796
|
+
* That is not a cosmetic lie. Status followed it with "routing is in place but
|
|
1797
|
+
* the bridge is not running", and the fix it advised starts a second bridge on
|
|
1798
|
+
* 127.0.0.1 while the working one holds 0.0.0.0. The kernel delivers to the
|
|
1799
|
+
* more specific socket, so the advice shadows the bridge it was meant to
|
|
1800
|
+
* rescue and the machine stops resolving — the outage `portHolder` above
|
|
1801
|
+
* already describes, arrived at this time by following our own instructions.
|
|
1802
|
+
*
|
|
1803
|
+
* So the port gets asked. A bridge nothing here started is still a bridge.
|
|
1804
|
+
* Both questions are put because they fail differently: a resolver that has
|
|
1805
|
+
* stopped answering Moshpit names loses a namespace, and one that has stopped
|
|
1806
|
+
* forwarding takes the machine off the internet.
|
|
1807
|
+
*/
|
|
1808
|
+
export async function bridgePresence({
|
|
1809
|
+
host = DEFAULT_HOST,
|
|
1810
|
+
port = DEFAULT_PORT,
|
|
1811
|
+
recorded = { running: false, pid: null, stale: false },
|
|
1812
|
+
listeners = defaultUdpListeners,
|
|
1813
|
+
answers = probeResolver,
|
|
1814
|
+
forwards = probeForwarding,
|
|
1815
|
+
} = {}) {
|
|
1816
|
+
const [moshpit, clearnet] = await Promise.all([
|
|
1817
|
+
answers({ host, port }).catch(() => false),
|
|
1818
|
+
forwards({ host, port, name: CLEARNET_PROBE }).catch(() => false),
|
|
1819
|
+
]);
|
|
1820
|
+
const answering = Boolean(moshpit || clearnet);
|
|
1821
|
+
|
|
1822
|
+
// Ours and alive is the ordinary case, and the probe still runs first: a
|
|
1823
|
+
// recorded pid that no longer answers is worth saying out loud rather than
|
|
1824
|
+
// reporting as a healthy bridge on the strength of the file alone.
|
|
1825
|
+
if (recorded.running) {
|
|
1826
|
+
return { kind: "ours", pid: recorded.pid, answering, forwards: clearnet, moshpit };
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
if (!answering) {
|
|
1830
|
+
return recorded.stale
|
|
1831
|
+
? { kind: "stale", pid: recorded.pid, answering: false, forwards: false, moshpit: false }
|
|
1832
|
+
: { kind: "none", pid: null, answering: false, forwards: false, moshpit: false };
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// Only asked once something is known to be there, because `ss` is the
|
|
1836
|
+
// expensive half and an unattributable owner is not a reason to call a
|
|
1837
|
+
// demonstrably answering bridge absent.
|
|
1838
|
+
const holder = portHolder(await listeners().catch(() => []), { host, port });
|
|
1839
|
+
return {
|
|
1840
|
+
kind: "foreign",
|
|
1841
|
+
pid: holder?.pid ?? null,
|
|
1842
|
+
process: holder?.process ?? null,
|
|
1843
|
+
answering: true,
|
|
1844
|
+
forwards: clearnet,
|
|
1845
|
+
moshpit,
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/** One line for `status`, kept next to the states it names. */
|
|
1850
|
+
export function describeBridge(presence, { host = DEFAULT_HOST, port = DEFAULT_PORT } = {}) {
|
|
1851
|
+
switch (presence.kind) {
|
|
1852
|
+
case "ours":
|
|
1853
|
+
return presence.answering
|
|
1854
|
+
? `running (pid ${presence.pid})`
|
|
1855
|
+
: `running (pid ${presence.pid}) — but not answering on ${host}:${port}`;
|
|
1856
|
+
case "foreign": {
|
|
1857
|
+
const who = presence.pid
|
|
1858
|
+
? `pid ${presence.pid}${presence.process ? `, ${presence.process}` : ""}`
|
|
1859
|
+
: "owner not visible";
|
|
1860
|
+
return `answering on ${host}:${port} (${who}) — started by something other than \`dns enable\``;
|
|
1861
|
+
}
|
|
1862
|
+
case "stale":
|
|
1863
|
+
return `NOT running — stale pidfile for ${presence.pid}`;
|
|
1864
|
+
default:
|
|
1865
|
+
return "not running";
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1786
1869
|
/**
|
|
1787
1870
|
* Everything that has to be true of the machine before the routing is written.
|
|
1788
1871
|
*
|
|
@@ -2151,7 +2234,7 @@ import { existsSync } from "node:fs";
|
|
|
2151
2234
|
import { fileURLToPath } from "node:url";
|
|
2152
2235
|
import {
|
|
2153
2236
|
applyPlan, daemonStatus, describePlan, detectPlatform, disablePlan, enablePlan,
|
|
2154
|
-
requiredPort, startDaemon, stopDaemon,
|
|
2237
|
+
probeResolver, requiredPort, startDaemon, stopDaemon,
|
|
2155
2238
|
} from "./dns-system.mjs";
|
|
2156
2239
|
import { escalateSelf } from "./escalate.mjs";
|
|
2157
2240
|
|
|
@@ -2230,6 +2313,8 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
2230
2313
|
applyWith = applyWithRollback,
|
|
2231
2314
|
verify = verifyResolution,
|
|
2232
2315
|
bridgeStatus = daemonStatus,
|
|
2316
|
+
presenceImpl = bridgePresence,
|
|
2317
|
+
exists = existsSync,
|
|
2233
2318
|
startBridge = startDaemon,
|
|
2234
2319
|
proxyReachableImpl = proxyReachable,
|
|
2235
2320
|
findLocalProxyImpl = findLocalProxy,
|
|
@@ -3009,25 +3094,48 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
3009
3094
|
|
|
3010
3095
|
if (sub === "status") {
|
|
3011
3096
|
const platform = detectPlatform();
|
|
3012
|
-
const daemon = await
|
|
3097
|
+
const daemon = await bridgeStatus();
|
|
3098
|
+
const statusPort = requiredPort(platform, port);
|
|
3099
|
+
const presence = await presenceImpl({ port: statusPort, recorded: daemon });
|
|
3013
3100
|
out(`platform ${platform || process.platform}`);
|
|
3014
|
-
out(`bridge ${
|
|
3101
|
+
out(`bridge ${describeBridge(presence, { port: statusPort })}`);
|
|
3015
3102
|
|
|
3016
3103
|
// Routing is read off the filesystem rather than remembered, so a config
|
|
3017
3104
|
// someone edited or removed by hand is reported as it actually is.
|
|
3018
3105
|
const marker = platform === "macos" ? "/etc/resolver" : MOSHPIT_DROPIN;
|
|
3019
|
-
|
|
3106
|
+
// Injected like every other system call this command makes. Read straight
|
|
3107
|
+
// off the filesystem, "is this machine routed" made the status tests depend
|
|
3108
|
+
// on whether the machine running them happened to have Moshpit enabled —
|
|
3109
|
+
// green on a developer's box, red on a clean runner.
|
|
3110
|
+
const routed = platform === "linux" || platform === "macos" ? exists(marker) : null;
|
|
3020
3111
|
out(`routing ${routed === null ? "(check NRPT: Get-DnsClientNrptRule)" : routed ? `configured (${marker})` : "not configured"}`);
|
|
3021
3112
|
|
|
3022
|
-
// The state worth shouting about
|
|
3023
|
-
//
|
|
3024
|
-
|
|
3113
|
+
// The state worth shouting about, and the condition is "nothing answers"
|
|
3114
|
+
// rather than "our pidfile is empty". Those are not the same machine, and
|
|
3115
|
+
// shouting on the second one sent people to start a bridge that shadowed
|
|
3116
|
+
// the working one they already had.
|
|
3117
|
+
//
|
|
3118
|
+
// The advice drops its `sudo` too: the CLI escalates the one step that
|
|
3119
|
+
// needs root, and teaching `sudo moshcode` is how `sudo moshcode update`
|
|
3120
|
+
// ends up reinstalling the whole tool into /root.
|
|
3121
|
+
if (routed && !presence.answering) {
|
|
3122
|
+
out("");
|
|
3123
|
+
out(`! routing is in place but nothing answers on ${DEFAULT_HOST}:${statusPort} — Moshpit names will fail.`);
|
|
3124
|
+
out(" fix with: moshcode dns enable undo with: moshcode dns disable");
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// Answering but not forwarding is the dangerous half, and it is invisible
|
|
3128
|
+
// from the Moshpit side: names resolve, and everything else on the machine
|
|
3129
|
+
// stops. Catch-all routing is what makes it total.
|
|
3130
|
+
if (routed && presence.answering && !presence.forwards) {
|
|
3025
3131
|
out("");
|
|
3026
|
-
out(
|
|
3027
|
-
out("
|
|
3132
|
+
out(`! the bridge on ${DEFAULT_HOST}:${statusPort} answers Moshpit names but is not forwarding`);
|
|
3133
|
+
out(" clearnet lookups routed through it will fail. Restart it, or `moshcode dns disable`.");
|
|
3028
3134
|
}
|
|
3029
3135
|
|
|
3030
|
-
|
|
3136
|
+
// The injected one, like every other caller. Reaching past it here made
|
|
3137
|
+
// `status` the one subcommand that could not be tested without a network.
|
|
3138
|
+
const known = await fetchTldsImpl({ registryBase }).catch(() => null);
|
|
3031
3139
|
const probe = known
|
|
3032
3140
|
? await resolveName(`probe.${known[0] || "moshpit"}`, { registryBase }).catch(() => null)
|
|
3033
3141
|
: null;
|