clavarium-relay 0.1.1 → 0.2.0-beta.1

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.
Files changed (3) hide show
  1. package/README.md +25 -2
  2. package/clavarium-relay +546 -56
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -3,14 +3,37 @@
3
3
  The maintenance program of a Clavarium relay, a small root program in bash:
4
4
 
5
5
  clavarium-relay status | repair | update-os [--reboot] | update-relay X.Y.Z | rotate-key tunnel|keeper KEY
6
+ clavarium-relay homes | add-home NAME --key KEY | remove-home NAME | name-principal NAME
6
7
 
7
8
  It is installed by `relay.sh` (or by `sudo clavarium online --relay HOST`) as
8
- `/usr/local/sbin/clavarium-relay`. The home machine reaches it through the
9
- account `clavarium-keeper`, which may run this program and nothing else.
9
+ `/usr/local/sbin/clavarium-relay`. The home machine that set the relay up, the
10
+ principal, reaches it through the account `clavarium-keeper`, which may run
11
+ this program and nothing else.
10
12
 
11
13
  `update-relay` is told a version number only: it fetches this package from the
12
14
  npm registry itself and checks the registry's sha512 before installing it.
13
15
 
16
+ ## Several homes on one relay
17
+
18
+ A relay can carry several Clavarium homes, each on its own domain name. It
19
+ stays blind: it never decrypts anything and holds no certificate.
20
+
21
+ - On port 443 a small router reads the name the visitor asks for (SNI, sent in
22
+ the clear at the start of TLS) and passes the bytes, untouched, to that
23
+ home's tunnel. On port 80 it reads the Host line. An unknown name is closed
24
+ (443) or answered 421 (80); it never goes to a default home.
25
+ - Each home has its own tunnel key, allowed to hold its own two ports and no
26
+ other. The list is `/etc/clavarium-relay/homes`.
27
+ - Homes are added and removed from the principal only:
28
+ `sudo clavarium relay invite NAME` gives a one-time code, which the new
29
+ home types: `sudo clavarium online --relay PRINCIPAL --domain NAME --join CODE`.
30
+ - A relay set up with an older version keeps working as it was. Its next
31
+ `update-relay` moves its one home to the list, as the principal; routing by
32
+ name starts once the principal has told the relay its name.
33
+
34
+ For each name, add a CAA record that names that home's certificate account:
35
+ otherwise whoever controls the relay could obtain a certificate for the name.
36
+
14
37
  - Site: https://clavarium.org
15
38
  - Source: https://github.com/clavarium/clavarium (folder relay/)
16
39
 
package/clavarium-relay CHANGED
@@ -2,29 +2,62 @@
2
2
  # ---------------------------------------------------------------------------
3
3
  # THE MAINTENANCE PROGRAM OF A CLAVARIUM RELAY. Runs as root, on the relay.
4
4
  #
5
- # clavarium-relay status what is set up, what runs, what is due
5
+ # clavarium-relay status what is set up, what runs, what is due, the homes
6
6
  # clavarium-relay repair writes the rules and the port service again
7
7
  # clavarium-relay update-os [--reboot] Ubuntu's updates now (and a restart if one is due)
8
8
  # clavarium-relay update-relay X.Y.Z this program at exactly that version, from npm
9
9
  # clavarium-relay rotate-key tunnel|keeper "ssh-ed25519 AAAA... name"
10
- # clavarium-relay setup --key LINE [--keeper-key LINE] (relay.sh runs it once)
10
+ # clavarium-relay homes the homes this relay serves: names, ports, connected or not
11
+ # clavarium-relay add-home NAME --key "ssh-ed25519 AAAA... name" one more home; prints its ports
12
+ # clavarium-relay remove-home NAME that home out: its key, its ports, its tunnel cut
13
+ # clavarium-relay name-principal NAME the name of the first home (a relay set up before 0.2.0 has none)
14
+ # clavarium-relay setup --key LINE [--keeper-key LINE] [--name NAME] (relay.sh runs it once)
11
15
  # clavarium-relay version
12
16
  #
13
- # WHO RUNS IT. Root, by hand; or the home machine, through the maintenance
14
- # account "clavarium-keeper": that account's SSH key leads to "door" (below)
15
- # and nowhere else, and sudo lets it run this one program, nothing else.
17
+ # WHO RUNS IT. Root, by hand; or the FIRST home (the principal), through the
18
+ # maintenance account "clavarium-keeper": that account's SSH key leads to
19
+ # "door" (below) and nowhere else, and sudo lets it run this one program,
20
+ # nothing else. Only the principal holds that key. Another home never gets
21
+ # it, nor any way to log in here: the principal's owner adds it (add-home,
22
+ # usually through a one-time code typed on the new home, see deploy/online.mjs
23
+ # of Clavarium), and removes it.
24
+ #
25
+ # SEVERAL HOMES, ONE RELAY. Each home has its own tunnel key and its own two
26
+ # ports. The list is /etc/clavarium-relay/homes, one home a line:
27
+ # NAMES TLS-PORT HTTP-PORT ssh-ed25519 AAAA... [comment]
28
+ # NAMES is one name or several, separated by commas; "-" while not known (a
29
+ # relay set up before 0.2.0 knew no name). The first line is the principal:
30
+ # ports 8443 and 8080, as before, which its tunnel may hold on any address.
31
+ # Every other home gets the next free pair (8444 and 8081, then 8445 and
32
+ # 8082...), which its tunnel may hold on 127.0.0.1 ONLY: the key line says
33
+ # permitlisten="127.0.0.1:<its port>", so a home can never take the ports of
34
+ # another one, nor be reached but through the router.
35
+ #
36
+ # WHAT ARRIVES ON 443 AND 80:
37
+ # - while a home has no name (a relay of one home, set up before 0.2.0): a
38
+ # kernel rule sends 443 to 8443 and 80 to 8080, exactly as before;
39
+ # - once every home has a name: THE ROUTER, a small Node.js program written
40
+ # by this one (below, router.js). On 443 it reads the name the browser
41
+ # asks for (SNI, in the clear in the first TLS message) WITHOUT decrypting
42
+ # anything, then passes the raw bytes, untouched, to that home's port. On
43
+ # 80 it reads the Host line and does the same. A name it does not know is
44
+ # closed (443) or answered "421" (80): never sent to a default home.
45
+ # The relay stays blind: TLS still starts in the browser and ends at home;
46
+ # no certificate and no secret is here.
16
47
  #
17
48
  # IT NEVER TAKES CODE FROM THE HOME MACHINE. update-relay is told a version
18
49
  # number only; it fetches the package "clavarium-relay" of that version from
19
50
  # the npm registry itself, checks it against the registry's sha512, and only
20
- # then replaces this file.
51
+ # then replaces this file. The router is part of this file, so it comes the
52
+ # same way. Its first run (repair) moves a relay of one home to the list:
53
+ # that home stays the principal, on the same ports, with the same key.
21
54
  #
22
55
  # Exit codes: 0 done (status: all is well); 1 something is wrong (said);
23
56
  # 64 bad parameters; 78 refused.
24
57
  # ---------------------------------------------------------------------------
25
58
  set -euo pipefail
26
59
 
27
- VERSION_OF_THIS="0.1.1"
60
+ VERSION_OF_THIS="0.2.0-beta.1"
28
61
  # Tests run it on a folder that stands for "/": nothing outside it is touched.
29
62
  ROOT="${CLAVARIUM_RELAY_ROOT:-}"
30
63
  REGISTRY="https://registry.npmjs.org"
@@ -34,20 +67,34 @@ TUNNEL="clavarium-tunnel"
34
67
  KEEPER="clavarium-keeper"
35
68
  TLS_PORT="8443"
36
69
  HTTP_PORT="8080"
70
+ # Homes after the first: TLS on TLS_PORT+N, HTTP on HTTP_PORT+N, N from 1 to HOMES_MAX.
71
+ HOMES_MAX="200"
37
72
  PROGRAM="/usr/local/sbin/clavarium-relay"
38
73
  KEYS="/etc/ssh/clavarium-relay"
74
+ HOMES="$ROOT/etc/clavarium-relay/homes"
75
+ ROUTER_JS="/usr/local/lib/clavarium-relay/router.js"
39
76
  SSHD_FILE="$ROOT/etc/ssh/sshd_config.d/10-clavarium-relay.conf"
40
77
  RULES="$ROOT/etc/clavarium-relay.nft"
41
78
  UNIT="$ROOT/etc/systemd/system/clavarium-relay-ports.service"
79
+ ROUTER_UNIT="$ROOT/etc/systemd/system/clavarium-relay-router.service"
42
80
  SUDOERS="$ROOT/etc/sudoers.d/clavarium-relay"
43
81
  UPGRADES="$ROOT/etc/apt/apt.conf.d/52clavarium-relay"
44
82
  KEY_RE='^ssh-ed25519 [A-Za-z0-9+/]{68}( [A-Za-z0-9@._-]{1,64})?$'
45
83
  VERSION_RE='^[0-9]{1,4}\.[0-9]{1,4}\.[0-9]{1,6}(-[0-9A-Za-z.-]{1,40})?$'
84
+ NAME_RE='^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,24}$'
46
85
 
47
- bad() { echo "clavarium-relay: $*" >&2; echo "usage: clavarium-relay status | repair | update-os [--reboot] | update-relay X.Y.Z | rotate-key tunnel|keeper KEY | version" >&2; exit 64; }
86
+ bad() { echo "clavarium-relay: $*" >&2; echo "usage: clavarium-relay status | repair | update-os [--reboot] | update-relay X.Y.Z | rotate-key tunnel|keeper KEY | homes | add-home NAME --key KEY | remove-home NAME | name-principal NAME | version" >&2; exit 64; }
48
87
  refuse() { echo "clavarium-relay: REFUSED: $*" >&2; exit 78; }
49
88
  as_root() { [ -n "$ROOT" ] || [ "$(id -u)" -eq 0 ] || refuse "run it as root (sudo)"; }
50
89
  key_ok() { case "$1" in *"PRIVATE KEY"*) return 1 ;; esac; [[ "$1" =~ $KEY_RE ]]; }
90
+ # NAMES: one host name or several, separated by commas (8 at most), lower case.
91
+ names_ok() {
92
+ local n list
93
+ [[ "$1" =~ ^[a-z0-9.,-]{1,600}$ ]] || return 1
94
+ IFS=, read -r -a list <<< "$1"
95
+ [ "${#list[@]}" -ge 1 ] && [ "${#list[@]}" -le 8 ] || return 1
96
+ for n in "${list[@]}"; do [[ "$n" =~ $NAME_RE ]] || return 1; done
97
+ }
51
98
 
52
99
  # --- fetch_relay: shared, word for word, with deploy/relay.sh (a test says so) ---
53
100
  # fetch_relay VERSION DIR: the package clavarium-relay@VERSION from the registry,
@@ -68,34 +115,283 @@ fetch_relay() {
68
115
  }
69
116
  # --- end of fetch_relay ---
70
117
 
71
- write_key() { # write_key ACCOUNT LINE: the one key of that account, in a file root owns
72
- local account="$1" key="$2" options
73
- key_ok "$key" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
74
- if [ "$account" = "$TUNNEL" ]; then options="restrict,port-forwarding,permitlisten=\"$TLS_PORT\",permitlisten=\"$HTTP_PORT\""; else options="restrict"; fi
118
+ # ------------------------------- the homes -------------------------------
119
+ # The list, read into four arrays (H_NAMES, H_TLS, H_HTTP, H_KEY), index 0 the principal.
120
+ H_NAMES=(); H_TLS=(); H_HTTP=(); H_KEY=()
121
+ read_homes() {
122
+ H_NAMES=(); H_TLS=(); H_HTTP=(); H_KEY=()
123
+ [ -s "$HOMES" ] || return 0
124
+ local names tls http rest
125
+ while read -r names tls http rest; do
126
+ case "$names" in ''|'#'*) continue ;; esac
127
+ [[ "$tls" =~ ^[0-9]{1,5}$ && "$http" =~ ^[0-9]{1,5}$ ]] && key_ok "$rest" || refuse "$HOMES has a line that is not NAMES TLS-PORT HTTP-PORT KEY: not changed here"
128
+ [ "$names" = "-" ] || names_ok "$names" || refuse "$HOMES has a name that is not a host name: $names"
129
+ H_NAMES+=("$names"); H_TLS+=("$tls"); H_HTTP+=("$http"); H_KEY+=("$rest")
130
+ done < "$HOMES"
131
+ }
132
+ write_homes() {
133
+ local i
134
+ install -d -m 0755 "$(dirname "$HOMES")"
135
+ {
136
+ echo "# Clavarium relay: the homes it serves, one a line: NAMES TLS-PORT HTTP-PORT KEY."
137
+ echo "# The first is the principal. Written by clavarium-relay: change it with add-home / remove-home."
138
+ for i in "${!H_NAMES[@]}"; do printf '%s %s %s %s\n' "${H_NAMES[$i]}" "${H_TLS[$i]}" "${H_HTTP[$i]}" "${H_KEY[$i]}"; done
139
+ } > "$HOMES.new"
140
+ chmod 0644 "$HOMES.new"
141
+ mv -f -- "$HOMES.new" "$HOMES"
142
+ }
143
+ # A relay set up before 0.2.0 kept its one tunnel key alone, in the account's key file: it becomes the
144
+ # first home of the list, the principal, with no name yet, on the same ports. Nothing else changes.
145
+ migrate() {
146
+ [ ! -s "$HOMES" ] || return 0
147
+ [ -s "$ROOT$KEYS/$TUNNEL" ] || return 0
148
+ local line key
149
+ line="$(grep -m1 'ssh-ed25519 ' "$ROOT$KEYS/$TUNNEL" || true)"
150
+ key="ssh-ed25519 ${line#*ssh-ed25519 }"
151
+ key_ok "$key" || refuse "the tunnel key of this relay ($KEYS/$TUNNEL) is not in the form this program writes: not moved"
152
+ H_NAMES=("-"); H_TLS=("$TLS_PORT"); H_HTTP=("$HTTP_PORT"); H_KEY=("$key")
153
+ write_homes
154
+ echo "the home of this relay is now the first of its list ($HOMES)"
155
+ }
156
+ body_of() { local k="${1#ssh-ed25519 }"; echo "${k%% *}"; }
157
+ # index_of NAME: the home that has this name among its names (-1: none).
158
+ index_of() {
159
+ local i n list
160
+ for i in "${!H_NAMES[@]}"; do
161
+ IFS=, read -r -a list <<< "${H_NAMES[$i]}"
162
+ for n in "${list[@]}"; do [ "$n" = "$1" ] && { echo "$i"; return; }; done
163
+ done
164
+ echo -1
165
+ }
166
+ index_of_key() {
167
+ local i b; b="$(body_of "$1")"
168
+ for i in "${!H_KEY[@]}"; do [ "$(body_of "${H_KEY[$i]}")" = "$b" ] && { echo "$i"; return; }; done
169
+ echo -1
170
+ }
171
+ all_named() { local n; [ "${#H_NAMES[@]}" -gt 0 ] || return 1; for n in "${H_NAMES[@]}"; do [ "$n" != "-" ] || return 1; done; }
172
+ listening() { ss -ltnH "sport = :$1" 2>/dev/null | grep -q .; }
173
+ host_print() { ssh-keygen -l -f "$ROOT/etc/ssh/ssh_host_ed25519_key.pub" 2>/dev/null | cut -d' ' -f2; }
174
+
175
+ # The tunnel account's keys, one line per home, each bounded to its own ports. The principal's line is
176
+ # the one written before 0.2.0, word for word (its tunnel asks for its ports on every address).
177
+ write_tunnel_keys() {
178
+ local i
75
179
  install -d -m 0755 "$ROOT$KEYS"
76
- printf '%s %s\n' "$options" "$key" > "$ROOT$KEYS/$account.new"
77
- chmod 0644 "$ROOT$KEYS/$account.new"
78
- mv -f -- "$ROOT$KEYS/$account.new" "$ROOT$KEYS/$account"
180
+ {
181
+ for i in "${!H_KEY[@]}"; do
182
+ if [ "$i" -eq 0 ]; then printf 'restrict,port-forwarding,permitlisten="%s",permitlisten="%s" %s\n' "${H_TLS[0]}" "${H_HTTP[0]}" "${H_KEY[0]}"
183
+ else printf 'restrict,port-forwarding,permitlisten="127.0.0.1:%s",permitlisten="127.0.0.1:%s" %s\n' "${H_TLS[$i]}" "${H_HTTP[$i]}" "${H_KEY[$i]}"; fi
184
+ done
185
+ } > "$ROOT$KEYS/$TUNNEL.new"
186
+ chmod 0644 "$ROOT$KEYS/$TUNNEL.new"
187
+ mv -f -- "$ROOT$KEYS/$TUNNEL.new" "$ROOT$KEYS/$TUNNEL"
79
188
  }
80
189
 
81
- do_repair() {
82
- as_root
83
- [ -s "$ROOT$KEYS/$TUNNEL" ] || refuse "this machine has no tunnel key: set it up with relay.sh --key ..."
84
- export DEBIAN_FRONTEND=noninteractive NEEDRESTART_SUSPEND=1
85
- local missing=""
86
- command -v nft >/dev/null 2>&1 || missing="nftables"
87
- [ -e "$ROOT/usr/bin/unattended-upgrade" ] || missing="$missing unattended-upgrades"
88
- if [ -n "$missing" ]; then
89
- # shellcheck disable=SC2086
90
- apt-get update -qq && apt-get install -y -qq $missing >/dev/null || refuse "could not install:$missing"
91
- fi
92
- id "$TUNNEL" >/dev/null 2>&1 || useradd --system --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin "$TUNNEL"
93
- # The keeper's commands go through its shell to "door": it needs one, and
94
- # has no password and no other command.
95
- if [ -s "$ROOT$KEYS/$KEEPER" ]; then id "$KEEPER" >/dev/null 2>&1 || useradd --system --home-dir /nonexistent --no-create-home --shell /bin/sh "$KEEPER"; fi
190
+ write_keeper_key() {
191
+ key_ok "$1" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
192
+ install -d -m 0755 "$ROOT$KEYS"
193
+ printf 'restrict %s\n' "$1" > "$ROOT$KEYS/$KEEPER.new"
194
+ chmod 0644 "$ROOT$KEYS/$KEEPER.new"
195
+ mv -f -- "$ROOT$KEYS/$KEEPER.new" "$ROOT$KEYS/$KEEPER"
196
+ }
197
+
198
+ # -------------------------------- the router --------------------------------
199
+ # WHY A SMALL NODE.JS PROGRAM, and not HAProxy or nginx: it is about 150 lines with no dependency, it
200
+ # comes INSIDE this file (so update-relay brings it with the same sha512 check, and the package stays
201
+ # one program), it reads the list of homes itself (no configuration language written and reloaded at
202
+ # each add-home), and the very same file is tried on real sockets by Clavarium's tests. Node.js itself
203
+ # comes from Ubuntu's own packages, so Ubuntu's security updates keep it patched. Written for the oldest
204
+ # Node.js an Ubuntu LTS still ships (12): CommonJS, nothing newer.
205
+ write_router() {
206
+ install -d -m 0755 "$(dirname "$ROOT$ROUTER_JS")"
207
+ cat > "$ROOT$ROUTER_JS.new" <<'ROUTER'
208
+ 'use strict';
209
+ /* THE ROUTER OF A CLAVARIUM RELAY, written by clavarium-relay (do not change it here: repair writes it
210
+ * again). Several homes behind one relay, each found by the name the visitor asks for:
211
+ *
212
+ * 443: the first TLS message (ClientHello) carries the name in the clear (SNI). It is read WITHOUT
213
+ * decrypting anything, then every byte, that message included, goes untouched to that home's
214
+ * port: TLS still ends at home, the relay holds no certificate and sees no page.
215
+ * 80: the Host line of the request; the same, to that home's HTTP port (certificate challenges,
216
+ * and the home's own redirect to https).
217
+ *
218
+ * A name not in the list: 443 closed, 80 answered "421 Misdirected Request". NEVER a default home.
219
+ * The list (/etc/clavarium-relay/homes) is read again when it changes: adding a home restarts nothing.
220
+ * It logs no visitor: only why it could not start.
221
+ *
222
+ * node router.js [--homes FILE] [--tls PORT] [--http PORT] [--bind ADDRESS]
223
+ */
224
+ var net = require('net');
225
+ var fs = require('fs');
226
+
227
+ var args = process.argv.slice(2);
228
+ function opt(name, value) { var i = args.indexOf(name); return i >= 0 && i + 1 < args.length ? args[i + 1] : value; }
229
+ var LIST = opt('--homes', '/etc/clavarium-relay/homes');
230
+ var TLS_PORT = Number(opt('--tls', '443'));
231
+ var HTTP_PORT = Number(opt('--http', '80'));
232
+ var BIND = opt('--bind', '');
233
+ var HOME_ADDRESS = '127.0.0.1';
234
+ var HELLO_MAX = 65536; /* a ClientHello larger than this is not one */
235
+ var HEAD_MAX = 16384; /* the same for the head of an HTTP request */
236
+ var WAIT_MS = 10000; /* the first bytes must come within this */
237
+ var WAITING_MAX = 4096; /* connections not yet sent anywhere, at most */
238
+ var waiting = 0;
239
+
240
+ var seen = ''; var table = {};
241
+ function homes() {
242
+ var st;
243
+ try { st = fs.statSync(LIST); } catch (e) { seen = ''; table = {}; return table; }
244
+ var stamp = st.ino + ':' + st.size + ':' + st.mtimeMs;
245
+ if (stamp === seen) return table;
246
+ var next = {};
247
+ fs.readFileSync(LIST, 'utf8').split('\n').forEach(function (line) {
248
+ var w = line.trim().split(/\s+/);
249
+ if (w.length < 3 || w[0].charAt(0) === '#' || w[0] === '-') return;
250
+ var tls = Number(w[1]); var http = Number(w[2]);
251
+ if (!(tls > 0 && tls < 65536 && http > 0 && http < 65536)) return;
252
+ w[0].split(',').forEach(function (n) { if (n) next[n.toLowerCase()] = { tls: tls, http: http }; });
253
+ });
254
+ table = next; seen = stamp;
255
+ return table;
256
+ }
257
+ function clean(name) {
258
+ var n = String(name).toLowerCase().replace(/\.$/, '');
259
+ return /^[a-z0-9.-]{1,253}$/.test(n) ? n : null;
260
+ }
96
261
 
262
+ /* The name in a ClientHello. undefined: more bytes needed; null: not a ClientHello, or no name. */
263
+ function sniOf(buf) {
264
+ var at = 0; var parts = []; var size = 0; var need = -1;
265
+ for (;;) {
266
+ if (buf.length < at + 5) return undefined;
267
+ if (buf[at] !== 22 || buf[at + 1] !== 3) return null;
268
+ var len = buf.readUInt16BE(at + 3);
269
+ if (len === 0 || len > 18432) return null;
270
+ if (buf.length < at + 5 + len) return undefined;
271
+ parts.push(buf.slice(at + 5, at + 5 + len)); size += len; at += 5 + len;
272
+ var hs = Buffer.concat(parts, size);
273
+ if (need < 0 && hs.length >= 4) {
274
+ if (hs[0] !== 1) return null;
275
+ need = 4 + hs.readUIntBE(1, 3);
276
+ if (need > HELLO_MAX) return null;
277
+ }
278
+ if (need >= 0 && hs.length >= need) return nameInHello(hs.slice(4, need));
279
+ }
280
+ }
281
+ function nameInHello(h) {
282
+ var p = 34; /* version, random */
283
+ if (h.length < p + 1) return null;
284
+ p += 1 + h[p]; /* session id */
285
+ if (h.length < p + 2) return null;
286
+ p += 2 + h.readUInt16BE(p); /* cipher suites */
287
+ if (h.length < p + 1) return null;
288
+ p += 1 + h[p]; /* compression methods */
289
+ if (h.length < p + 2) return null;
290
+ var end = Math.min(h.length, p + 2 + h.readUInt16BE(p)); p += 2;
291
+ while (p + 4 <= end) {
292
+ var type = h.readUInt16BE(p); var len = h.readUInt16BE(p + 2); p += 4;
293
+ if (p + len > end) return null;
294
+ if (type === 0 && len >= 2) { /* server_name: a list of (kind, length, name) */
295
+ var stop = Math.min(p + len, p + 2 + h.readUInt16BE(p)); var q = p + 2;
296
+ while (q + 3 <= stop) {
297
+ var kind = h[q]; var n = h.readUInt16BE(q + 1); q += 3;
298
+ if (q + n > stop) return null;
299
+ if (kind === 0) return clean(h.slice(q, q + n).toString('latin1'));
300
+ q += n;
301
+ }
302
+ return null;
303
+ }
304
+ p += len;
305
+ }
306
+ return null;
307
+ }
308
+ /* The Host of an HTTP request. undefined: its head is not whole yet; null: none, or not HTTP. */
309
+ function hostOf(buf) {
310
+ var end = buf.indexOf('\r\n\r\n');
311
+ if (end < 0) return buf.length > HEAD_MAX ? null : undefined;
312
+ var lines = buf.slice(0, end).toString('latin1').split('\r\n');
313
+ if (!/^[A-Z]{3,10} \S+ HTTP\/1\.[01]$/.test(lines[0])) return null;
314
+ for (var i = 1; i < lines.length; i++) {
315
+ var m = /^host:[ \t]*([^ \t]*)[ \t]*$/i.exec(lines[i]);
316
+ if (m) return clean(m[1].replace(/:[0-9]{1,5}$/, ''));
317
+ }
318
+ return null;
319
+ }
320
+ function answer(socket, status, text) {
321
+ socket.end('HTTP/1.1 ' + status + '\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: ' + Buffer.byteLength(text)
322
+ + '\r\nConnection: close\r\nCache-Control: no-store\r\n\r\n' + text);
323
+ }
324
+
325
+ function serve(port, kind) {
326
+ var server = net.createServer(function (client) {
327
+ client.on('error', function () {});
328
+ if (waiting >= WAITING_MAX) { client.destroy(); return; }
329
+ waiting += 1;
330
+ var buf = Buffer.alloc(0); var decided = false;
331
+ var timer = setTimeout(function () { decide(null, false); }, WAIT_MS);
332
+ function onData(piece) {
333
+ buf = Buffer.concat([buf, piece]);
334
+ var name = kind === 'tls' ? sniOf(buf) : hostOf(buf);
335
+ if (name === undefined) { if (buf.length > HELLO_MAX) decide(null, false); return; }
336
+ decide(name, true);
337
+ }
338
+ function decide(name, whole) {
339
+ if (decided) return;
340
+ decided = true; waiting -= 1; clearTimeout(timer);
341
+ client.removeListener('data', onData);
342
+ client.pause();
343
+ var home = name ? homes()[name] : null;
344
+ if (!home) {
345
+ if (kind === 'http' && whole) answer(client, '421 Misdirected Request', 'No home here has this name.\n');
346
+ else client.destroy();
347
+ return;
348
+ }
349
+ var upstream = net.connect(kind === 'tls' ? home.tls : home.http, HOME_ADDRESS);
350
+ var joined = false;
351
+ upstream.on('connect', function () {
352
+ joined = true;
353
+ upstream.write(buf);
354
+ client.pipe(upstream); upstream.pipe(client);
355
+ client.resume();
356
+ });
357
+ upstream.on('error', function () {
358
+ if (!joined && kind === 'http') answer(client, '502 Bad Gateway', 'This home is not connected to its relay just now.\n');
359
+ else client.destroy();
360
+ });
361
+ client.on('close', function () { upstream.destroy(); });
362
+ upstream.on('close', function () { if (joined) client.destroy(); });
363
+ }
364
+ client.on('data', onData);
365
+ client.on('end', function () { decide(null, false); });
366
+ });
367
+ server.on('error', function (e) { console.error('clavarium-relay router: port ' + port + ': ' + e.message); process.exit(1); });
368
+ return new Promise(function (resolve) {
369
+ server.listen(BIND ? { port: port, host: BIND } : { port: port }, function () { resolve(server.address().port); });
370
+ });
371
+ }
372
+
373
+ homes();
374
+ Promise.all([serve(TLS_PORT, 'tls'), serve(HTTP_PORT, 'http')]).then(function (ports) {
375
+ console.log('clavarium-relay router: tls ' + ports[0] + ', http ' + ports[1]);
376
+ });
377
+ ROUTER
378
+ chmod 0644 "$ROOT$ROUTER_JS.new"
379
+ if [ -f "$ROOT$ROUTER_JS" ] && cmp -s "$ROOT$ROUTER_JS.new" "$ROOT$ROUTER_JS"; then rm -f -- "$ROOT$ROUTER_JS.new"; return 1; fi
380
+ mv -f -- "$ROOT$ROUTER_JS.new" "$ROOT$ROUTER_JS"
381
+ }
382
+
383
+ # --------------------------- writing it all down ---------------------------
384
+ # The SSH rules, the tunnel keys, and what takes 443 and 80: the kernel rule (a home without a name)
385
+ # or the router (every home named). Run after any change of the list; changes nothing that holds.
386
+ apply() {
387
+ read_homes
388
+ write_tunnel_keys
389
+ local listens="" i
390
+ for i in "${!H_TLS[@]}"; do
391
+ if [ "$i" -eq 0 ]; then listens="${H_TLS[0]} ${H_HTTP[0]}"; else listens="$listens 127.0.0.1:${H_TLS[$i]} 127.0.0.1:${H_HTTP[$i]}"; fi
392
+ done
97
393
  install -d "$(dirname "$SSHD_FILE")"
98
- cat > "$SSHD_FILE" <<SSHD
394
+ cat > "$SSHD_FILE.new" <<SSHD
99
395
  # Clavarium relay: what its two accounts may do. Written by clavarium-relay.
100
396
  Match User $TUNNEL
101
397
  AuthorizedKeysFile $KEYS/%u
@@ -103,7 +399,8 @@ Match User $TUNNEL
103
399
  KbdInteractiveAuthentication no
104
400
  AllowTcpForwarding remote
105
401
  GatewayPorts clientspecified
106
- PermitListen $TLS_PORT $HTTP_PORT
402
+ # Each key may hold only its own two ports (its line in $KEYS/$TUNNEL says which).
403
+ PermitListen $listens
107
404
  PermitOpen none
108
405
  PermitTTY no
109
406
  X11Forwarding no
@@ -127,15 +424,62 @@ Match User $KEEPER
127
424
  # What follows this file in the server's settings is for everybody again.
128
425
  Match all
129
426
  SSHD
130
- if ! sshd -t; then rm -f -- "$SSHD_FILE"; refuse "the SSH server refused these settings; they were taken out again"; fi
131
- systemctl reload ssh.service 2>/dev/null || systemctl reload sshd.service
427
+ if ! cmp -s "$SSHD_FILE.new" "$SSHD_FILE" 2>/dev/null; then
428
+ local before=""; [ -f "$SSHD_FILE" ] && before="$(cat "$SSHD_FILE")"
429
+ mv -f -- "$SSHD_FILE.new" "$SSHD_FILE"
430
+ if ! sshd -t; then
431
+ if [ -n "$before" ]; then printf '%s\n' "$before" > "$SSHD_FILE"; else rm -f -- "$SSHD_FILE"; fi
432
+ refuse "the SSH server refused these settings; the ones before were put back"
433
+ fi
434
+ systemctl reload ssh.service 2>/dev/null || systemctl reload sshd.service
435
+ else rm -f -- "$SSHD_FILE.new"; fi
132
436
 
133
- install -d "$(dirname "$SUDOERS")"
134
- printf '# Clavarium relay: the maintenance account may run ONE program as root.\n%s ALL=(root) NOPASSWD: %s\n' "$KEEPER" "$PROGRAM" > "$SUDOERS.new"
135
- visudo -cf "$SUDOERS.new" >/dev/null || { rm -f -- "$SUDOERS.new"; refuse "the sudo rule did not pass its check"; }
136
- chmod 0440 "$SUDOERS.new"; mv -f -- "$SUDOERS.new" "$SUDOERS"
437
+ install -d "$(dirname "$UNIT")"
438
+ if all_named; then
439
+ command -v node >/dev/null 2>&1 || { apt-get update -qq && apt-get install -y -qq nodejs >/dev/null; } || refuse "could not install nodejs (the router)"
440
+ local changed="no"
441
+ write_router && changed="yes"
442
+ cat > "$ROUTER_UNIT.new" <<UNITFILE
443
+ [Unit]
444
+ Description=Clavarium relay - each name to its home (443 and 80)
445
+ After=network-online.target
446
+ Wants=network-online.target
447
+
448
+ [Service]
449
+ ExecStart=$(command -v node || echo /usr/bin/node) $ROUTER_JS --homes /etc/clavarium-relay/homes
450
+ DynamicUser=yes
451
+ AmbientCapabilities=CAP_NET_BIND_SERVICE
452
+ CapabilityBoundingSet=CAP_NET_BIND_SERVICE
453
+ NoNewPrivileges=yes
454
+ ProtectSystem=strict
455
+ ProtectHome=yes
456
+ PrivateTmp=yes
457
+ PrivateDevices=yes
458
+ LimitNOFILE=65536
459
+ Restart=always
460
+ RestartSec=2
137
461
 
138
- cat > "$RULES" <<NFT
462
+ [Install]
463
+ WantedBy=multi-user.target
464
+ UNITFILE
465
+ cmp -s "$ROUTER_UNIT.new" "$ROUTER_UNIT" 2>/dev/null || changed="yes"
466
+ mv -f -- "$ROUTER_UNIT.new" "$ROUTER_UNIT"
467
+ # The kernel rule out first: it would send 443 past the router.
468
+ if [ -f "$UNIT" ] || [ -f "$RULES" ]; then
469
+ systemctl disable --quiet --now clavarium-relay-ports.service 2>/dev/null || true
470
+ rm -f -- "$UNIT" "$RULES"
471
+ fi
472
+ systemctl daemon-reload
473
+ systemctl enable --quiet clavarium-relay-router.service
474
+ # Restarted only when it changed: a restart cuts every visitor of every home.
475
+ if [ "$changed" = "yes" ]; then systemctl restart clavarium-relay-router.service; else systemctl start clavarium-relay-router.service; fi
476
+ systemctl is-active --quiet clavarium-relay-router.service || refuse "the router did not start: journalctl -u clavarium-relay-router"
477
+ else
478
+ if [ -f "$ROUTER_UNIT" ]; then
479
+ systemctl disable --quiet --now clavarium-relay-router.service 2>/dev/null || true
480
+ rm -f -- "$ROUTER_UNIT"
481
+ fi
482
+ cat > "$RULES" <<NFT
139
483
  # Clavarium relay: what arrives on 443 and 80 goes to the tunnel. Written by clavarium-relay.
140
484
  table inet clavarium_relay
141
485
  delete table inet clavarium_relay
@@ -147,8 +491,7 @@ table inet clavarium_relay {
147
491
  }
148
492
  }
149
493
  NFT
150
- install -d "$(dirname "$UNIT")"
151
- cat > "$UNIT" <<UNITFILE
494
+ cat > "$UNIT" <<UNITFILE
152
495
  [Unit]
153
496
  Description=Clavarium relay - 443 and 80 go to the tunnel
154
497
  After=network-pre.target
@@ -163,10 +506,37 @@ ExecStop=/usr/sbin/nft delete table inet clavarium_relay
163
506
  [Install]
164
507
  WantedBy=multi-user.target
165
508
  UNITFILE
166
- systemctl daemon-reload
167
- systemctl enable --quiet clavarium-relay-ports.service
168
- systemctl restart clavarium-relay-ports.service
169
- systemctl is-active --quiet clavarium-relay-ports.service || refuse "the port rule could not be loaded: journalctl -u clavarium-relay-ports"
509
+ systemctl daemon-reload
510
+ systemctl enable --quiet clavarium-relay-ports.service
511
+ systemctl restart clavarium-relay-ports.service
512
+ systemctl is-active --quiet clavarium-relay-ports.service || refuse "the port rule could not be loaded: journalctl -u clavarium-relay-ports"
513
+ fi
514
+ }
515
+
516
+ do_repair() {
517
+ as_root
518
+ migrate
519
+ read_homes
520
+ [ "${#H_KEY[@]}" -gt 0 ] || refuse "this machine has no tunnel key: set it up with relay.sh --key ..."
521
+ export DEBIAN_FRONTEND=noninteractive NEEDRESTART_SUSPEND=1
522
+ local missing=""
523
+ command -v nft >/dev/null 2>&1 || missing="nftables"
524
+ [ -e "$ROOT/usr/bin/unattended-upgrade" ] || missing="$missing unattended-upgrades"
525
+ if [ -n "$missing" ]; then
526
+ # shellcheck disable=SC2086
527
+ apt-get update -qq && apt-get install -y -qq $missing >/dev/null || refuse "could not install:$missing"
528
+ fi
529
+ id "$TUNNEL" >/dev/null 2>&1 || useradd --system --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin "$TUNNEL"
530
+ # The keeper's commands go through its shell to "door": it needs one, and
531
+ # has no password and no other command.
532
+ if [ -s "$ROOT$KEYS/$KEEPER" ]; then id "$KEEPER" >/dev/null 2>&1 || useradd --system --home-dir /nonexistent --no-create-home --shell /bin/sh "$KEEPER"; fi
533
+
534
+ install -d "$(dirname "$SUDOERS")"
535
+ printf '# Clavarium relay: the maintenance account may run ONE program as root.\n%s ALL=(root) NOPASSWD: %s\n' "$KEEPER" "$PROGRAM" > "$SUDOERS.new"
536
+ visudo -cf "$SUDOERS.new" >/dev/null || { rm -f -- "$SUDOERS.new"; refuse "the sudo rule did not pass its check"; }
537
+ chmod 0440 "$SUDOERS.new"; mv -f -- "$SUDOERS.new" "$SUDOERS"
538
+
539
+ apply
170
540
 
171
541
  # Ubuntu's security updates, by themselves, every day.
172
542
  install -d "$(dirname "$UPGRADES")"
@@ -177,7 +547,16 @@ APT::Periodic::Unattended-Upgrade "1";
177
547
  APT::Periodic::AutocleanInterval "7";
178
548
  APT
179
549
  systemctl enable --quiet --now unattended-upgrades.service 2>/dev/null || true
180
- echo "repaired: ssh rules, sudo rule, port rule, automatic updates"
550
+ echo "repaired: ssh rules, sudo rule, $(all_named && echo "the router" || echo "port rule"), automatic updates"
551
+ }
552
+
553
+ do_homes() {
554
+ read_homes
555
+ local i state
556
+ for i in "${!H_NAMES[@]}"; do
557
+ state="waiting"; listening "${H_TLS[$i]}" && state="connected"
558
+ printf '%-8s %-40s tls %-5s http %-5s %s\n' "$([ "$i" -eq 0 ] && echo principal || echo home)" "${H_NAMES[$i]}" "${H_TLS[$i]}" "${H_HTTP[$i]}" "$state"
559
+ done
181
560
  }
182
561
 
183
562
  do_status() {
@@ -190,12 +569,14 @@ do_status() {
190
569
  check keeper-account "id $KEEPER >/dev/null 2>&1"
191
570
  check ssh-rules "[ -s '$SSHD_FILE' ]"
192
571
  check sudo-rule "[ -s '$SUDOERS' ]"
193
- check port-rules "systemctl is-active --quiet clavarium-relay-ports.service" ok "not loaded"
572
+ if [ -f "$ROUTER_UNIT" ]; then check router "systemctl is-active --quiet clavarium-relay-router.service" ok "not running"
573
+ else check port-rules "systemctl is-active --quiet clavarium-relay-ports.service" ok "not loaded"; fi
194
574
  check os-updates "[ -s '$UPGRADES' ]" automatic off
195
575
  line="waiting for home"
196
576
  ss -ltnH "sport = :$TLS_PORT" 2>/dev/null | grep -q . && line="connected"
197
577
  say tunnel "$line"
198
578
  if [ -e "$ROOT/var/run/reboot-required" ]; then say reboot needed; else say reboot no; fi
579
+ if [ -s "$HOMES" ]; then echo "homes:"; do_homes | sed 's/^/ /'; fi
199
580
  [ "$bad_count" -eq 0 ]
200
581
  }
201
582
 
@@ -220,31 +601,128 @@ do_update_relay() {
220
601
  mv -f -- "$ROOT$PROGRAM.new" "$ROOT$PROGRAM"
221
602
  rm -rf -- "$dir"
222
603
  echo "clavarium-relay $version installed"
223
- # The new version writes its own rules.
604
+ # The new version writes its own rules (and moves an older relay to its list of homes).
224
605
  exec "$ROOT$PROGRAM" repair
225
606
  }
226
607
 
227
608
  do_rotate_key() {
228
609
  as_root
229
610
  local who="${1:-}"; shift || true
230
- local key="$*"
231
- case "$who" in tunnel) who="$TUNNEL" ;; keeper) who="$KEEPER" ;; *) bad "rotate-key tunnel|keeper KEY" ;; esac
232
- write_key "$who" "$key"
233
- echo "the key of $who was replaced"
611
+ local key="$*" at
612
+ case "$who" in
613
+ tunnel)
614
+ # The principal's tunnel key; another home's key is changed by removing it and adding it again.
615
+ key_ok "$key" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
616
+ migrate; read_homes
617
+ [ "${#H_KEY[@]}" -gt 0 ] || refuse "this machine has no tunnel key yet: set it up with relay.sh --key ..."
618
+ at="$(index_of_key "$key")"; [ "$at" -le 0 ] || refuse "this key is the key of another home"
619
+ H_KEY[0]="$key"; write_homes; apply >/dev/null
620
+ echo "the key of $TUNNEL was replaced" ;;
621
+ keeper) write_keeper_key "$key"; echo "the key of $KEEPER was replaced" ;;
622
+ *) bad "rotate-key tunnel|keeper KEY" ;;
623
+ esac
624
+ }
625
+
626
+ do_add_home() {
627
+ as_root
628
+ local names="${1:-}" key="" n at i tls http
629
+ shift || true
630
+ while [ $# -gt 0 ]; do
631
+ case "$1" in --key) key="${2:-}"; shift 2 || bad "--key needs a value" ;; *) bad "unknown parameter: $1" ;; esac
632
+ done
633
+ names_ok "$names" || bad "add-home NAME --key KEY: NAME is a host name, lower case (several: separated by commas)"
634
+ key_ok "$key" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
635
+ migrate; read_homes
636
+ [ "${#H_KEY[@]}" -gt 0 ] || refuse "this relay has no first home yet: set it up with relay.sh --key ..."
637
+ [ "${H_NAMES[0]}" != "-" ] || refuse "the first home of this relay has no name yet: name-principal NAME first (its own Clavarium does it)"
638
+ at="$(index_of_key "$key")"
639
+ if [ "$at" -ge 0 ]; then
640
+ # The same home asking again (a join tried twice): its ports again, nothing changed.
641
+ [ "$at" -gt 0 ] && [ "${H_NAMES[$at]}" = "$names" ] || refuse "this key is the key of another home of this relay"
642
+ tls="${H_TLS[$at]}"; http="${H_HTTP[$at]}"
643
+ else
644
+ IFS=, read -r -a list <<< "$names"
645
+ for n in "${list[@]}"; do [ "$(index_of "$n")" -lt 0 ] || refuse "$n is served by this relay already (remove-home $n first)"; done
646
+ [ "${#H_NAMES[@]}" -le "$HOMES_MAX" ] || refuse "this relay serves $HOMES_MAX homes already"
647
+ tls=""; http=""
648
+ for ((i = 1; i <= HOMES_MAX; i++)); do
649
+ local t=$((TLS_PORT + i)) h=$((HTTP_PORT + i)) used="no" j
650
+ for j in "${!H_TLS[@]}"; do
651
+ case " ${H_TLS[$j]} ${H_HTTP[$j]} " in *" $t "*|*" $h "*) used="yes" ;; esac
652
+ done
653
+ [ "$used" = "no" ] || continue
654
+ # Something else of this machine on that port: the next pair.
655
+ listening "$t" || listening "$h" || { tls="$t"; http="$h"; break; }
656
+ done
657
+ [ -n "$tls" ] || refuse "no free pair of ports left"
658
+ H_NAMES+=("$names"); H_TLS+=("$tls"); H_HTTP+=("$http"); H_KEY+=("$key")
659
+ write_homes
660
+ apply >/dev/null
661
+ fi
662
+ printf 'home %s\ntls-port %s\nhttp-port %s\nfingerprint %s\n' "$names" "$tls" "$http" "$(host_print)"
663
+ }
664
+
665
+ do_remove_home() {
666
+ as_root
667
+ local name="${1:-}" at pid
668
+ [[ "$name" =~ $NAME_RE ]] && [ $# -eq 1 ] || bad "remove-home NAME"
669
+ migrate; read_homes
670
+ at="$(index_of "$name")"
671
+ [ "$at" -ge 0 ] || refuse "no home of this relay has the name $name"
672
+ [ "$at" -gt 0 ] || refuse "$name is the first home of this relay, its principal: it is not removed here"
673
+ local tls="${H_TLS[$at]}" http="${H_HTTP[$at]}"
674
+ H_NAMES=("${H_NAMES[@]:0:$at}" "${H_NAMES[@]:$((at + 1))}")
675
+ H_TLS=("${H_TLS[@]:0:$at}" "${H_TLS[@]:$((at + 1))}")
676
+ H_HTTP=("${H_HTTP[@]:0:$at}" "${H_HTTP[@]:$((at + 1))}")
677
+ H_KEY=("${H_KEY[@]:0:$at}" "${H_KEY[@]:$((at + 1))}")
678
+ write_homes
679
+ apply >/dev/null
680
+ # Its key no longer opens the account; a tunnel already open is cut (the SSH session holding its ports).
681
+ for pid in $(ss -ltnpH "( sport = :$tls or sport = :$http )" 2>/dev/null | grep -o 'pid=[0-9]*' | cut -d= -f2 | sort -u); do kill "$pid" 2>/dev/null || true; done
682
+ echo "the home $name was removed: its key, its ports ($tls, $http)"
683
+ }
684
+
685
+ do_name_principal() {
686
+ as_root
687
+ local names="${1:-}" at
688
+ [ $# -eq 1 ] && names_ok "$names" || bad "name-principal NAME"
689
+ migrate; read_homes
690
+ [ "${#H_KEY[@]}" -gt 0 ] || refuse "this relay has no first home yet: set it up with relay.sh --key ..."
691
+ if [ "${H_NAMES[0]}" = "$names" ]; then echo "the principal is $names already"; return; fi
692
+ IFS=, read -r -a list <<< "$names"
693
+ for n in "${list[@]}"; do at="$(index_of "$n")"; [ "$at" -le 0 ] || refuse "$n is the name of another home of this relay"; done
694
+ H_NAMES[0]="$names"
695
+ write_homes
696
+ apply >/dev/null
697
+ echo "the principal is $names"
234
698
  }
235
699
 
236
700
  do_setup() {
237
701
  as_root
238
- local key="" keeper=""
702
+ local key="" keeper="" name="" at
239
703
  while [ $# -gt 0 ]; do
240
704
  case "$1" in
241
705
  --key) key="${2:-}"; shift 2 || bad "--key needs a value" ;;
242
706
  --keeper-key) keeper="${2:-}"; shift 2 || bad "--keeper-key needs a value" ;;
707
+ --name) name="${2:-}"; shift 2 || bad "--name needs a value" ;;
243
708
  *) bad "unknown parameter: $1" ;;
244
709
  esac
245
710
  done
246
- write_key "$TUNNEL" "$key"
247
- [ -z "$keeper" ] || write_key "$KEEPER" "$keeper"
711
+ key_ok "$key" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
712
+ [ -z "$name" ] || names_ok "$name" || bad "--name is a host name, lower case"
713
+ [ -z "$keeper" ] || key_ok "$keeper" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
714
+ # Whoever runs this as root sets up the principal: its key and name, the same ports as ever.
715
+ # The other homes, if any, stay as they are.
716
+ migrate; read_homes
717
+ if [ "${#H_KEY[@]}" -eq 0 ]; then
718
+ H_NAMES=("${name:--}"); H_TLS=("$TLS_PORT"); H_HTTP=("$HTTP_PORT"); H_KEY=("$key")
719
+ else
720
+ at="$(index_of_key "$key")"; [ "$at" -le 0 ] || refuse "this key is the key of another home of this relay"
721
+ if [ -n "$name" ]; then at="$(index_of "$name")"; [ "$at" -le 0 ] || refuse "$name is served by this relay as another home"; H_NAMES[0]="$name"; fi
722
+ H_KEY[0]="$key"
723
+ fi
724
+ write_homes
725
+ [ -z "$keeper" ] || write_keeper_key "$keeper"
248
726
  do_repair
249
727
  }
250
728
 
@@ -257,7 +735,7 @@ do_door() {
257
735
  local verb="${words[0]:-}"
258
736
  local sudo=(sudo -n "$PROGRAM")
259
737
  case "$verb" in
260
- status|repair|version) [ "${#words[@]}" -eq 1 ] || bad "$verb takes nothing"; exec "${sudo[@]}" "$verb" ;;
738
+ status|repair|version|homes) [ "${#words[@]}" -eq 1 ] || bad "$verb takes nothing"; exec "${sudo[@]}" "$verb" ;;
261
739
  update-os) [ "${#words[@]}" -eq 1 ] || { [ "${#words[@]}" -eq 2 ] && [ "${words[1]}" = "--reboot" ]; } || bad "update-os [--reboot]"; exec "${sudo[@]}" "${words[@]}" ;;
262
740
  update-relay) [ "${#words[@]}" -eq 2 ] && [[ "${words[1]}" =~ $VERSION_RE ]] || bad "update-relay X.Y.Z"; exec "${sudo[@]}" update-relay "${words[1]}" ;;
263
741
  rotate-key)
@@ -265,6 +743,14 @@ do_door() {
265
743
  local key="${words[*]:2}"
266
744
  key_ok "$key" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
267
745
  exec "${sudo[@]}" rotate-key "${words[1]}" "$key" ;;
746
+ add-home)
747
+ names_ok "${words[1]:-}" || bad "add-home NAME KEY"
748
+ local key="${words[*]:2}"
749
+ key_ok "$key" || bad "a key is one line: ssh-ed25519, the key, and optionally a name"
750
+ exec "${sudo[@]}" add-home "${words[1]}" --key "$key" ;;
751
+ remove-home|name-principal)
752
+ [ "${#words[@]}" -eq 2 ] && names_ok "${words[1]}" || bad "$verb NAME"
753
+ exec "${sudo[@]}" "$verb" "${words[1]}" ;;
268
754
  *) bad "not a verb of this door: ${verb:-nothing}" ;;
269
755
  esac
270
756
  }
@@ -276,6 +762,10 @@ case "$verb" in
276
762
  update-os) do_update_os "$@" ;;
277
763
  update-relay) do_update_relay "$@" ;;
278
764
  rotate-key) do_rotate_key "$@" ;;
765
+ homes) do_homes ;;
766
+ add-home) do_add_home "$@" ;;
767
+ remove-home) do_remove_home "$@" ;;
768
+ name-principal) do_name_principal "$@" ;;
279
769
  setup) do_setup "$@" ;;
280
770
  door) do_door ;;
281
771
  version|--version) echo "$VERSION_OF_THIS" ;;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clavarium-relay",
3
- "version": "0.1.1",
4
- "description": "The maintenance program of a Clavarium relay: status, repair, updates, key rotation.",
3
+ "version": "0.2.0-beta.1",
4
+ "description": "The maintenance program of a Clavarium relay: status, repair, updates, key rotation, several homes routed by name.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://clavarium.org",
7
7
  "repository": { "type": "git", "url": "git+https://github.com/clavarium/clavarium.git", "directory": "relay" },