moshcode 0.77.0 → 0.79.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/examples/templates/bun-caddy-sqlite/deploy/moshcode-dns.service +20 -9
- package/examples/templates/caddy-proxy/deploy/moshcode-dns.service +20 -9
- package/examples/templates/caddy-static/deploy/moshcode-dns.service +20 -9
- package/package.json +1 -1
- package/src/cli-schema.mjs +13 -3
- package/src/dns-service.mjs +165 -0
- package/src/dns.mjs +129 -3
|
@@ -7,8 +7,22 @@
|
|
|
7
7
|
# and every Moshpit name stops resolving with no obvious cause. This unit is
|
|
8
8
|
# the missing half.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# GENERATE IT, DO NOT COPY THIS ONE:
|
|
11
|
+
#
|
|
12
|
+
# moshcode dns service --write # this user's own service, needs no root
|
|
13
|
+
# moshcode dns service --system | sudo tee /etc/systemd/system/moshcode-dns.service
|
|
14
|
+
#
|
|
15
|
+
# `dns service` writes ExecStart from the running process, so it names the node
|
|
16
|
+
# that demonstrably works and the install that is actually on the box. A static
|
|
17
|
+
# file cannot: moshcode installs under $HOME, and its wrapper execs whatever
|
|
18
|
+
# `node` is first on PATH — which on any mise, nvm or asdf machine is another
|
|
19
|
+
# shim under $HOME. systemd has neither on its PATH and, with ProtectHome or
|
|
20
|
+
# DynamicUser set, could not read them if it did. A unit that gets this wrong
|
|
21
|
+
# fails at 203/EXEC with nothing useful in the journal.
|
|
22
|
+
#
|
|
23
|
+
# What is left below therefore assumes the one case it can safely assume: both
|
|
24
|
+
# moshcode and node installed system-wide, service running as root. Check
|
|
25
|
+
# `systemctl status moshcode-dns` before believing it.
|
|
12
26
|
#
|
|
13
27
|
# Install this on machines that need to REACH Moshpit names. A box that only
|
|
14
28
|
# serves one does not need it — Caddy answers whatever Host header arrives and
|
|
@@ -22,18 +36,15 @@ Before=systemd-resolved.service
|
|
|
22
36
|
|
|
23
37
|
[Service]
|
|
24
38
|
Type=simple
|
|
25
|
-
# Port 5354 is unprivileged, so this does not need root
|
|
26
|
-
# the parking responder cannot take port 80 and falls back to
|
|
27
|
-
# parking address — which only affects names that point nowhere yet.
|
|
28
|
-
ExecStart=/usr/bin/
|
|
39
|
+
# Port 5354 is unprivileged, so this does not need root for the DNS side. The
|
|
40
|
+
# trade-off is that the parking responder cannot take port 80 and falls back to
|
|
41
|
+
# the public parking address — which only affects names that point nowhere yet.
|
|
42
|
+
ExecStart=/usr/local/bin/moshcode dns start --port 5354
|
|
29
43
|
Restart=always
|
|
30
44
|
RestartSec=2
|
|
31
45
|
|
|
32
|
-
DynamicUser=yes
|
|
33
46
|
NoNewPrivileges=yes
|
|
34
47
|
PrivateTmp=yes
|
|
35
|
-
ProtectSystem=strict
|
|
36
|
-
ProtectHome=yes
|
|
37
48
|
|
|
38
49
|
[Install]
|
|
39
50
|
WantedBy=multi-user.target
|
|
@@ -7,8 +7,22 @@
|
|
|
7
7
|
# and every Moshpit name stops resolving with no obvious cause. This unit is
|
|
8
8
|
# the missing half.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# GENERATE IT, DO NOT COPY THIS ONE:
|
|
11
|
+
#
|
|
12
|
+
# moshcode dns service --write # this user's own service, needs no root
|
|
13
|
+
# moshcode dns service --system | sudo tee /etc/systemd/system/moshcode-dns.service
|
|
14
|
+
#
|
|
15
|
+
# `dns service` writes ExecStart from the running process, so it names the node
|
|
16
|
+
# that demonstrably works and the install that is actually on the box. A static
|
|
17
|
+
# file cannot: moshcode installs under $HOME, and its wrapper execs whatever
|
|
18
|
+
# `node` is first on PATH — which on any mise, nvm or asdf machine is another
|
|
19
|
+
# shim under $HOME. systemd has neither on its PATH and, with ProtectHome or
|
|
20
|
+
# DynamicUser set, could not read them if it did. A unit that gets this wrong
|
|
21
|
+
# fails at 203/EXEC with nothing useful in the journal.
|
|
22
|
+
#
|
|
23
|
+
# What is left below therefore assumes the one case it can safely assume: both
|
|
24
|
+
# moshcode and node installed system-wide, service running as root. Check
|
|
25
|
+
# `systemctl status moshcode-dns` before believing it.
|
|
12
26
|
#
|
|
13
27
|
# Install this on machines that need to REACH Moshpit names. A box that only
|
|
14
28
|
# serves one does not need it — Caddy answers whatever Host header arrives and
|
|
@@ -22,18 +36,15 @@ Before=systemd-resolved.service
|
|
|
22
36
|
|
|
23
37
|
[Service]
|
|
24
38
|
Type=simple
|
|
25
|
-
# Port 5354 is unprivileged, so this does not need root
|
|
26
|
-
# the parking responder cannot take port 80 and falls back to
|
|
27
|
-
# parking address — which only affects names that point nowhere yet.
|
|
28
|
-
ExecStart=/usr/bin/
|
|
39
|
+
# Port 5354 is unprivileged, so this does not need root for the DNS side. The
|
|
40
|
+
# trade-off is that the parking responder cannot take port 80 and falls back to
|
|
41
|
+
# the public parking address — which only affects names that point nowhere yet.
|
|
42
|
+
ExecStart=/usr/local/bin/moshcode dns start --port 5354
|
|
29
43
|
Restart=always
|
|
30
44
|
RestartSec=2
|
|
31
45
|
|
|
32
|
-
DynamicUser=yes
|
|
33
46
|
NoNewPrivileges=yes
|
|
34
47
|
PrivateTmp=yes
|
|
35
|
-
ProtectSystem=strict
|
|
36
|
-
ProtectHome=yes
|
|
37
48
|
|
|
38
49
|
[Install]
|
|
39
50
|
WantedBy=multi-user.target
|
|
@@ -7,8 +7,22 @@
|
|
|
7
7
|
# and every Moshpit name stops resolving with no obvious cause. This unit is
|
|
8
8
|
# the missing half.
|
|
9
9
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
10
|
+
# GENERATE IT, DO NOT COPY THIS ONE:
|
|
11
|
+
#
|
|
12
|
+
# moshcode dns service --write # this user's own service, needs no root
|
|
13
|
+
# moshcode dns service --system | sudo tee /etc/systemd/system/moshcode-dns.service
|
|
14
|
+
#
|
|
15
|
+
# `dns service` writes ExecStart from the running process, so it names the node
|
|
16
|
+
# that demonstrably works and the install that is actually on the box. A static
|
|
17
|
+
# file cannot: moshcode installs under $HOME, and its wrapper execs whatever
|
|
18
|
+
# `node` is first on PATH — which on any mise, nvm or asdf machine is another
|
|
19
|
+
# shim under $HOME. systemd has neither on its PATH and, with ProtectHome or
|
|
20
|
+
# DynamicUser set, could not read them if it did. A unit that gets this wrong
|
|
21
|
+
# fails at 203/EXEC with nothing useful in the journal.
|
|
22
|
+
#
|
|
23
|
+
# What is left below therefore assumes the one case it can safely assume: both
|
|
24
|
+
# moshcode and node installed system-wide, service running as root. Check
|
|
25
|
+
# `systemctl status moshcode-dns` before believing it.
|
|
12
26
|
#
|
|
13
27
|
# Install this on machines that need to REACH Moshpit names. A box that only
|
|
14
28
|
# serves one does not need it — Caddy answers whatever Host header arrives and
|
|
@@ -22,18 +36,15 @@ Before=systemd-resolved.service
|
|
|
22
36
|
|
|
23
37
|
[Service]
|
|
24
38
|
Type=simple
|
|
25
|
-
# Port 5354 is unprivileged, so this does not need root
|
|
26
|
-
# the parking responder cannot take port 80 and falls back to
|
|
27
|
-
# parking address — which only affects names that point nowhere yet.
|
|
28
|
-
ExecStart=/usr/bin/
|
|
39
|
+
# Port 5354 is unprivileged, so this does not need root for the DNS side. The
|
|
40
|
+
# trade-off is that the parking responder cannot take port 80 and falls back to
|
|
41
|
+
# the public parking address — which only affects names that point nowhere yet.
|
|
42
|
+
ExecStart=/usr/local/bin/moshcode dns start --port 5354
|
|
29
43
|
Restart=always
|
|
30
44
|
RestartSec=2
|
|
31
45
|
|
|
32
|
-
DynamicUser=yes
|
|
33
46
|
NoNewPrivileges=yes
|
|
34
47
|
PrivateTmp=yes
|
|
35
|
-
ProtectSystem=strict
|
|
36
|
-
ProtectHome=yes
|
|
37
48
|
|
|
38
49
|
[Install]
|
|
39
50
|
WantedBy=multi-user.target
|
package/package.json
CHANGED
package/src/cli-schema.mjs
CHANGED
|
@@ -390,7 +390,7 @@ export const CORE_CLI_COMMANDS = [
|
|
|
390
390
|
["--no-proxy", "with enable: answer origins rather than the local proxy", ""],
|
|
391
391
|
],
|
|
392
392
|
examples: [
|
|
393
|
-
["
|
|
393
|
+
["moshcode dns enable", "route Moshpit endings here (it escalates itself)"],
|
|
394
394
|
["moshcode dns resolve blue.eggs", "what a machine actually gets"],
|
|
395
395
|
],
|
|
396
396
|
seeAlso: ["doh", "site"],
|
|
@@ -1007,9 +1007,19 @@ export const DNS_VERBS = [
|
|
|
1007
1007
|
{ name: "disable", description: "undo enable" },
|
|
1008
1008
|
{ name: "status", description: "what is running, what is routed, does it work" },
|
|
1009
1009
|
{ name: "refresh", description: "re-apply routing for endings claimed since" },
|
|
1010
|
-
{ name: "start", description: "run the bridge in the foreground" },
|
|
1010
|
+
{ name: "start", description: "run the bridge in the foreground (--upstream IP to say where to forward)" },
|
|
1011
1011
|
{ name: "install", description: "print the resolver config without applying it" },
|
|
1012
|
-
{
|
|
1012
|
+
{
|
|
1013
|
+
name: "service",
|
|
1014
|
+
description: "keep the bridge running across reboots — the half `enable` does not survive",
|
|
1015
|
+
synopsis: [
|
|
1016
|
+
["moshcode dns service", "print the unit for this install, change nothing"],
|
|
1017
|
+
["moshcode dns service --write", "install and start it as this user; needs no root"],
|
|
1018
|
+
["moshcode dns service --system", "a system unit instead — place it with sudo tee"],
|
|
1019
|
+
["moshcode dns service --remove", "stop it and take the unit away"],
|
|
1020
|
+
["moshcode dns service --upstream IP", "where to forward, when discovery cannot see past the bridge"],
|
|
1021
|
+
],
|
|
1022
|
+
},
|
|
1013
1023
|
{ name: "tlds", description: "list the endings claimed in the Pit" },
|
|
1014
1024
|
{ name: "resolve", description: "what a name resolves to, and why" },
|
|
1015
1025
|
{ name: "trust", description: "trust one name's certificate, after checking it against the registry pin" },
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// Keeping the bridge running across reboots.
|
|
2
|
+
//
|
|
3
|
+
// `dns enable` sets up two halves with different lifetimes: a systemd-resolved
|
|
4
|
+
// drop-in, which is a file and survives a reboot on its own, and the bridge
|
|
5
|
+
// process, which does not. After a restart the routing still points at a port
|
|
6
|
+
// with nothing behind it and every Moshpit name stops resolving with no
|
|
7
|
+
// obvious cause. This is the missing half, and it is why Moshpit DNS has held
|
|
8
|
+
// on servers — which got a unit installed by hand — and quietly fallen over on
|
|
9
|
+
// desktops, which never did.
|
|
10
|
+
//
|
|
11
|
+
// The unit is *generated* rather than shipped as a file, because no static
|
|
12
|
+
// unit can be correct for this tool. moshcode installs under the invoking
|
|
13
|
+
// user's $HOME, and its wrapper execs whatever `node` is first on PATH — which
|
|
14
|
+
// on any mise, nvm or asdf box is another shim under $HOME. The unit that
|
|
15
|
+
// shipped in examples/templates said:
|
|
16
|
+
//
|
|
17
|
+
// ExecStart=/usr/bin/env moshcode dns start --port 5354
|
|
18
|
+
// DynamicUser=yes
|
|
19
|
+
// ProtectHome=yes
|
|
20
|
+
//
|
|
21
|
+
// and could not start on an ordinary install three times over: `moshcode` is
|
|
22
|
+
// not on systemd's PATH, the `node` its wrapper needs is not on it either, and
|
|
23
|
+
// ProtectHome hides the install from the service even if both had been found.
|
|
24
|
+
// It starts only where moshcode and node are both installed system-wide, which
|
|
25
|
+
// is the server case — the one that was already working.
|
|
26
|
+
//
|
|
27
|
+
// So the unit is written from the running process instead. `process.execPath`
|
|
28
|
+
// is an interpreter demonstrably able to run this code, because it is running
|
|
29
|
+
// it, and the entry is the script this very command was invoked from. Nothing
|
|
30
|
+
// is guessed and nothing depends on PATH.
|
|
31
|
+
import { spawn } from "node:child_process";
|
|
32
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
33
|
+
import { homedir } from "node:os";
|
|
34
|
+
import { dirname, join } from "node:path";
|
|
35
|
+
|
|
36
|
+
export const UNIT_NAME = "moshcode-dns.service";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Where the unit goes, and which systemctl reaches it.
|
|
40
|
+
*
|
|
41
|
+
* The user scope is the default because it is the one that fits how moshcode
|
|
42
|
+
* is installed: a per-user tool, run by the user who owns the install, with
|
|
43
|
+
* $HOME and the mise shims reachable exactly as they are in a shell. It also
|
|
44
|
+
* puts the pidfile where the unprivileged `dns status` looks — systemd sets
|
|
45
|
+
* XDG_RUNTIME_DIR for user units, so `pidfilePath()` resolves to
|
|
46
|
+
* /run/user/<uid>/moshpit-dns.pid for the daemon and for the person asking
|
|
47
|
+
* after it. Under a system unit those are two different paths.
|
|
48
|
+
*/
|
|
49
|
+
export function servicePaths({ system = false, home = homedir() } = {}) {
|
|
50
|
+
return system
|
|
51
|
+
? { path: join("/etc/systemd/system", UNIT_NAME), systemctl: ["systemctl"], scope: "system" }
|
|
52
|
+
: { path: join(home, ".config/systemd/user", UNIT_NAME), systemctl: ["systemctl", "--user"], scope: "user" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The unit text, pinned to this install.
|
|
57
|
+
*
|
|
58
|
+
* `ProtectHome` is deliberately absent rather than set to a weaker value: the
|
|
59
|
+
* whole program lives under $HOME, so there is no setting of it that both
|
|
60
|
+
* protects anything and lets the service start. The hardening that survives is
|
|
61
|
+
* the hardening that does not contradict where the code is.
|
|
62
|
+
*/
|
|
63
|
+
export function serviceUnit({
|
|
64
|
+
system = false,
|
|
65
|
+
execPath = process.execPath,
|
|
66
|
+
entry,
|
|
67
|
+
port,
|
|
68
|
+
registryBase = null,
|
|
69
|
+
upstreams = [],
|
|
70
|
+
user = process.env.USER || process.env.LOGNAME,
|
|
71
|
+
} = {}) {
|
|
72
|
+
if (!entry) throw new Error("serviceUnit needs the entry script to run");
|
|
73
|
+
|
|
74
|
+
const args = [entry, "dns", "start", "--port", String(port)];
|
|
75
|
+
if (registryBase) args.push("--registry", registryBase);
|
|
76
|
+
// The reason this unit exists at all is that the bridge now starts at boot —
|
|
77
|
+
// and at boot the resolved drop-in is already in place, so the only
|
|
78
|
+
// nameserver discovery can find is this bridge. It refuses loopback, comes up
|
|
79
|
+
// with nowhere to forward, and NXDOMAINs every clearnet name including the
|
|
80
|
+
// registry. Recorded here, while a working resolver is still around to be
|
|
81
|
+
// asked, rather than rediscovered at boot when it cannot be.
|
|
82
|
+
if (upstreams.length) args.push("--upstream", upstreams.join(","));
|
|
83
|
+
const exec = [execPath, ...args].map((part) => (/\s/.test(part) ? JSON.stringify(part) : part)).join(" ");
|
|
84
|
+
|
|
85
|
+
const lines = [
|
|
86
|
+
"# Generated by `moshcode dns service`. Regenerate it rather than editing:",
|
|
87
|
+
"# the paths below are this install's, and a moshcode or node that moves",
|
|
88
|
+
"# leaves a unit that fails at 203/EXEC with nothing else to say.",
|
|
89
|
+
"[Unit]",
|
|
90
|
+
"Description=Moshpit DNS bridge",
|
|
91
|
+
"Documentation=https://github.com/moshcoder/moshcode",
|
|
92
|
+
"After=network-online.target",
|
|
93
|
+
"Wants=network-online.target",
|
|
94
|
+
"",
|
|
95
|
+
"[Service]",
|
|
96
|
+
"Type=simple",
|
|
97
|
+
`ExecStart=${exec}`,
|
|
98
|
+
"Restart=always",
|
|
99
|
+
"RestartSec=2",
|
|
100
|
+
// 5354 is unprivileged, so nothing here needs root or a capability.
|
|
101
|
+
"NoNewPrivileges=yes",
|
|
102
|
+
"PrivateTmp=yes",
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (system) {
|
|
106
|
+
// A system unit has no user of its own, so it has to be told which install
|
|
107
|
+
// to run — and it is the only scope where that question has a wrong answer.
|
|
108
|
+
lines.push(`User=${user}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
lines.push(
|
|
112
|
+
"",
|
|
113
|
+
"[Install]",
|
|
114
|
+
system ? "WantedBy=multi-user.target" : "WantedBy=default.target",
|
|
115
|
+
"",
|
|
116
|
+
);
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function run(command, args) {
|
|
121
|
+
return new Promise((resolve) => {
|
|
122
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
123
|
+
let err = "";
|
|
124
|
+
child.stderr.on("data", (d) => (err += d));
|
|
125
|
+
child.on("error", (error) => resolve({ ok: false, error: error.message }));
|
|
126
|
+
child.on("exit", (code) => resolve({ ok: code === 0, error: err.trim() }));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Write the unit and start it. Returns the steps taken, in order, for printing. */
|
|
131
|
+
export async function installService(unit, { system = false, home = homedir(), exec = run } = {}) {
|
|
132
|
+
const { path, systemctl, scope } = servicePaths({ system, home });
|
|
133
|
+
const steps = [];
|
|
134
|
+
try {
|
|
135
|
+
await mkdir(dirname(path), { recursive: true });
|
|
136
|
+
await writeFile(path, unit);
|
|
137
|
+
steps.push({ step: `wrote ${path}`, ok: true });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
return { ok: false, path, scope, steps: [{ step: `could not write ${path}: ${error.message}`, ok: false }] };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const [cmd, ...flags] = systemctl;
|
|
143
|
+
for (const args of [[...flags, "daemon-reload"], [...flags, "enable", "--now", UNIT_NAME]]) {
|
|
144
|
+
const result = await exec(cmd, args);
|
|
145
|
+
steps.push({ step: `${cmd} ${args.join(" ")}`, ok: result.ok, error: result.error });
|
|
146
|
+
if (!result.ok) return { ok: false, path, scope, steps };
|
|
147
|
+
}
|
|
148
|
+
return { ok: true, path, scope, steps };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Stop it and take the unit away. Missing is not a failure — removal is idempotent. */
|
|
152
|
+
export async function removeService({ system = false, home = homedir(), exec = run } = {}) {
|
|
153
|
+
const { path, systemctl, scope } = servicePaths({ system, home });
|
|
154
|
+
const [cmd, ...flags] = systemctl;
|
|
155
|
+
const steps = [];
|
|
156
|
+
for (const args of [[...flags, "disable", "--now", UNIT_NAME]]) {
|
|
157
|
+
const result = await exec(cmd, args);
|
|
158
|
+
steps.push({ step: `${cmd} ${args.join(" ")}`, ok: result.ok, error: result.error });
|
|
159
|
+
}
|
|
160
|
+
await rm(path, { force: true });
|
|
161
|
+
steps.push({ step: `removed ${path}`, ok: true });
|
|
162
|
+
const reload = await exec(cmd, [...flags, "daemon-reload"]);
|
|
163
|
+
steps.push({ step: `${cmd} ${[...flags, "daemon-reload"].join(" ")}`, ok: reload.ok, error: reload.error });
|
|
164
|
+
return { ok: true, path, scope, steps };
|
|
165
|
+
}
|
package/src/dns.mjs
CHANGED
|
@@ -1629,6 +1629,48 @@ export function dnsmasqCatchAllConf({ host = DEFAULT_HOST, port = DEFAULT_PORT }
|
|
|
1629
1629
|
* wrote 127.0.0.53 into resolv.conf is the thing sending us the query, and
|
|
1630
1630
|
* forwarding back to it is a loop that ends in a timeout rather than an answer.
|
|
1631
1631
|
*/
|
|
1632
|
+
/**
|
|
1633
|
+
* Upstreams named on the command line, which override discovery entirely.
|
|
1634
|
+
*
|
|
1635
|
+
* Discovery reads the machine's resolv.conf and drops loopback, which is right
|
|
1636
|
+
* until the machine's resolver is this bridge. Then the only nameserver on file
|
|
1637
|
+
* IS the bridge, discovery correctly refuses to return it, and the daemon comes
|
|
1638
|
+
* up with nowhere to forward — every clearnet name NXDOMAIN, including the
|
|
1639
|
+
* registry it needs in order to know which endings are Moshpit at all. The box
|
|
1640
|
+
* loses DNS entirely and the bridge cannot bootstrap out of it, because the
|
|
1641
|
+
* lookup that would fix it goes through the bridge.
|
|
1642
|
+
*
|
|
1643
|
+
* `dns enable` never hit this: it runs before the routing exists. A supervised
|
|
1644
|
+
* bridge starting at boot hits it every time, because the drop-in is a file and
|
|
1645
|
+
* is already in place. So the upstreams have to be sayable rather than only
|
|
1646
|
+
* discoverable, and `dns service` bakes the ones it found into the unit.
|
|
1647
|
+
*
|
|
1648
|
+
* Repeatable and comma-separated both work. `address#port` matches resolv.conf
|
|
1649
|
+
* and dnsmasq rather than inventing a third spelling.
|
|
1650
|
+
*/
|
|
1651
|
+
export function upstreamsFromArgs(args = []) {
|
|
1652
|
+
const servers = [];
|
|
1653
|
+
const invalid = [];
|
|
1654
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1655
|
+
if (args[i] !== "--upstream") continue;
|
|
1656
|
+
const value = args[i + 1];
|
|
1657
|
+
// A bare trailing `--upstream`, or one followed by the next flag, is a
|
|
1658
|
+
// typo rather than a request for no upstreams. Reported, not ignored.
|
|
1659
|
+
if (value === undefined || value.startsWith("--")) {
|
|
1660
|
+
invalid.push("(missing value)");
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
for (const part of value.split(",")) {
|
|
1664
|
+
const server = part.trim();
|
|
1665
|
+
if (!server) continue;
|
|
1666
|
+
const [address] = server.split("#");
|
|
1667
|
+
if (!isIP(address)) invalid.push(server);
|
|
1668
|
+
else if (!servers.includes(server)) servers.push(server);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return { servers, invalid };
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1632
1674
|
export function parseUpstreams(resolvConf) {
|
|
1633
1675
|
const out = [];
|
|
1634
1676
|
for (const line of String(resolvConf ?? "").split("\n")) {
|
|
@@ -2278,6 +2320,7 @@ import { applyTrust, applyUntrust, createAutoTrust, trustName, verifyStockTls }
|
|
|
2278
2320
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2279
2321
|
import { existsSync } from "node:fs";
|
|
2280
2322
|
import { fileURLToPath } from "node:url";
|
|
2323
|
+
import { installService, removeService, serviceUnit, servicePaths, UNIT_NAME } from "./dns-service.mjs";
|
|
2281
2324
|
import {
|
|
2282
2325
|
applyPlan, daemonStatus, describePlan, detectPlatform, disablePlan, enablePlan,
|
|
2283
2326
|
probeResolver, requiredPort, startDaemon, stopDaemon,
|
|
@@ -2306,10 +2349,17 @@ const USAGE = `moshcode dns — resolve Moshpit names on this machine
|
|
|
2306
2349
|
look a name up; --open opens a parked name in the Pit
|
|
2307
2350
|
--json prints one stable document for scripts
|
|
2308
2351
|
moshcode dns start [--port N] run the resolver in the foreground
|
|
2352
|
+
--upstream IP[,IP] where to forward clearnet
|
|
2353
|
+
lookups; overrides resolv.conf discovery, which
|
|
2354
|
+
finds nothing once this machine is routed here
|
|
2309
2355
|
also serves parked names over HTTP so \`curl <name>\`
|
|
2310
2356
|
lands on the Pit; --parking-port N, --no-parking-http
|
|
2311
2357
|
--no-filter runs it with blocklists off
|
|
2312
2358
|
moshcode dns install [--write] print the resolver config without applying it
|
|
2359
|
+
moshcode dns service print a systemd unit that keeps the bridge
|
|
2360
|
+
running across reboots; --write installs and
|
|
2361
|
+
starts it, --system for a system unit rather
|
|
2362
|
+
than this user's, --remove takes it away
|
|
2313
2363
|
|
|
2314
2364
|
moshcode dns filter block ads, trackers, malware and phishing at the
|
|
2315
2365
|
resolver — \`moshcode dns filter help\` for the verbs
|
|
@@ -2509,7 +2559,12 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
2509
2559
|
if (!park) out("! parking host did not resolve — unpointed names will return NXDOMAIN");
|
|
2510
2560
|
// Without these the bridge answers only for endings it is authoritative
|
|
2511
2561
|
// for, which is correct for per-ending routing and fatal for catch-all.
|
|
2512
|
-
|
|
2562
|
+
// Named upstreams win outright. Discovery is a fallback for the ordinary
|
|
2563
|
+
// case, not a second opinion: someone who says where to forward has almost
|
|
2564
|
+
// always said it because discovery got it wrong.
|
|
2565
|
+
const named = upstreamsFromArgs(rest);
|
|
2566
|
+
for (const bad of named.invalid) out(`! ignoring --upstream ${bad} — not an IP address`);
|
|
2567
|
+
const upstreams = named.servers.length ? named.servers : await discoverUpstreams();
|
|
2513
2568
|
// Swallowing this was the quietest way to turn the namespace off. An empty
|
|
2514
2569
|
// ending set makes isOurs() say no to every name, so with upstreams present
|
|
2515
2570
|
// the bridge forwards the whole of Moshpit to the clearnet, which denies it
|
|
@@ -2523,8 +2578,17 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
2523
2578
|
(err) => ({ error: err?.message || String(err) }),
|
|
2524
2579
|
);
|
|
2525
2580
|
const tldSet = new Set(tlds.found || []);
|
|
2526
|
-
if (upstreams.length)
|
|
2527
|
-
|
|
2581
|
+
if (upstreams.length) {
|
|
2582
|
+
out(`forwarding non-Moshpit lookups to ${upstreams.join(", ")}${named.servers.length ? " (--upstream)" : ""}`);
|
|
2583
|
+
} else {
|
|
2584
|
+
out("! no upstreams found in /etc/resolv.conf — this bridge can only answer Moshpit names");
|
|
2585
|
+
// The specific way this happens is worth naming, because the symptom
|
|
2586
|
+
// (every name NXDOMAIN) looks nothing like the cause and the machine
|
|
2587
|
+
// cannot look the cause up.
|
|
2588
|
+
out(" if this machine's resolver is already routed here, discovery has only");
|
|
2589
|
+
out(" the bridge to find and correctly refuses it — say where to forward:");
|
|
2590
|
+
out(` moshcode dns start --port ${port} --upstream 1.1.1.1`);
|
|
2591
|
+
}
|
|
2528
2592
|
if (tldSet.size) out(`answering for ${tldSet.size} endings`);
|
|
2529
2593
|
else {
|
|
2530
2594
|
out(`! could not read the ending list from ${registryBase}${tlds.error ? ` — ${tlds.error}` : ""}`);
|
|
@@ -2684,6 +2748,68 @@ export async function dnsCommand(args = [], out = console.log, deps = {}) {
|
|
|
2684
2748
|
return 0;
|
|
2685
2749
|
}
|
|
2686
2750
|
|
|
2751
|
+
// The half of `enable` that a reboot takes away. Kept a separate verb rather
|
|
2752
|
+
// than folded into `enable`, because installing a service is exactly the kind
|
|
2753
|
+
// of thing that must stay something a person asked for.
|
|
2754
|
+
if (sub === "service") {
|
|
2755
|
+
const system = rest.includes("--system");
|
|
2756
|
+
const { path: unitPath, systemctl, scope } = servicePaths({ system });
|
|
2757
|
+
|
|
2758
|
+
const report = (steps) => {
|
|
2759
|
+
for (const s of steps) out(`${s.ok ? "ok" : "!!"} ${s.step}${s.error ? ` — ${s.error}` : ""}`);
|
|
2760
|
+
};
|
|
2761
|
+
|
|
2762
|
+
if (rest.includes("--remove")) {
|
|
2763
|
+
report((await removeService({ system })).steps);
|
|
2764
|
+
out("");
|
|
2765
|
+
out("the routing is untouched — `moshcode dns disable` is what removes that");
|
|
2766
|
+
return 0;
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
// Asked now, not at boot. This command runs while the machine still has a
|
|
2770
|
+
// resolver that answers; the service it writes will not.
|
|
2771
|
+
const upstreams = upstreamsFromArgs(rest).servers.length
|
|
2772
|
+
? upstreamsFromArgs(rest).servers
|
|
2773
|
+
: await discoverUpstreams();
|
|
2774
|
+
const unit = serviceUnit({ system, entry: cliEntry(), port, registryBase, upstreams });
|
|
2775
|
+
|
|
2776
|
+
if (!upstreams.length) {
|
|
2777
|
+
out("! no upstream nameservers found, and none given");
|
|
2778
|
+
out(" a bridge with nowhere to forward answers NXDOMAIN for every clearnet");
|
|
2779
|
+
out(" name — including the registry it needs to know which endings exist.");
|
|
2780
|
+
out(" If this machine is already routed here, discovery can only see the");
|
|
2781
|
+
out(" bridge and correctly refuses it. Say where to forward:");
|
|
2782
|
+
out(` moshcode dns service --upstream 1.1.1.1${rest.includes("--write") ? " --write" : ""}`);
|
|
2783
|
+
out("");
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
if (rest.includes("--write")) {
|
|
2787
|
+
const result = await installService(unit, { system });
|
|
2788
|
+
report(result.steps);
|
|
2789
|
+
if (!result.ok) {
|
|
2790
|
+
out("");
|
|
2791
|
+
out(system
|
|
2792
|
+
? `(writing ${unitPath} needs root — pipe it instead: moshcode dns service --system | sudo tee ${unitPath})`
|
|
2793
|
+
: "(the user scope needs no privileges — this failure is something else)");
|
|
2794
|
+
return 1;
|
|
2795
|
+
}
|
|
2796
|
+
out("");
|
|
2797
|
+
out(`the bridge now starts with the machine (${scope} scope)`);
|
|
2798
|
+
if (!system) {
|
|
2799
|
+
const who = process.env.USER || process.env.LOGNAME || "$USER";
|
|
2800
|
+
out(` a user service stops at logout — to survive one: loginctl enable-linger ${who}`);
|
|
2801
|
+
}
|
|
2802
|
+
out(" check it: moshcode dns status");
|
|
2803
|
+
return 0;
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
out(`# ${unitPath}`);
|
|
2807
|
+
out(unit);
|
|
2808
|
+
out(`# install it with: moshcode dns service --write${system ? " --system" : ""}`);
|
|
2809
|
+
out(`# then: ${systemctl.join(" ")} status ${UNIT_NAME}`);
|
|
2810
|
+
return 0;
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2687
2813
|
if (sub === "enable" || sub === "disable") {
|
|
2688
2814
|
const platform = platformImpl();
|
|
2689
2815
|
if (!platform) {
|