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.
- package/README.md +25 -2
- package/clavarium-relay +546 -56
- 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
|
|
9
|
-
account `clavarium-keeper`, which may run
|
|
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
|
|
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
|
|
14
|
-
# account "clavarium-keeper": that account's SSH key leads to
|
|
15
|
-
# and nowhere else, and sudo lets it run this one program,
|
|
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.
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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 !
|
|
131
|
-
|
|
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 "$
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
247
|
-
[ -z "$
|
|
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.
|
|
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" },
|