@kvbox/server 0.0.0-stage → 0.5.5
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/dist/kvbox-server.js +2504 -0
- package/dist/kvbox-server.js.map +1 -0
- package/package.json +48 -4
- package/scripts/net-setup.sh +318 -0
- package/README.md +0 -3
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kvbox/server",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.5",
|
|
4
|
+
"description": "kvbox host daemon: owns Firecracker microVMs on a Linux host and serves the kvbox API. Managed with the @kvbox/cli CLI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kvbox-server": "dist/kvbox-server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"scripts"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ghalex/kvbox.git",
|
|
20
|
+
"directory": "packages/server"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://kvbox.sh",
|
|
23
|
+
"keywords": [
|
|
24
|
+
"firecracker",
|
|
25
|
+
"microvm",
|
|
26
|
+
"sandbox",
|
|
27
|
+
"vm",
|
|
28
|
+
"server"
|
|
29
|
+
],
|
|
30
|
+
"os": [
|
|
31
|
+
"linux"
|
|
32
|
+
],
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=22.13"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"dev": "tsx src/index.ts",
|
|
38
|
+
"build": "tsup"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@hono/node-server": "^2.1.5",
|
|
42
|
+
"commander": "^15.0.0",
|
|
43
|
+
"hono": "^4.13.13",
|
|
44
|
+
"undici": "^8.12.3",
|
|
45
|
+
"zod": "^4.6.7"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@kvbox/shared": "workspace:*"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Host networking for kvbox: bridge kvbox0 (172.30.0.1/24), IP forwarding, NAT to the
|
|
3
|
+
# default route, and FORWARD accept rules for the bridge (Docker sets FORWARD's policy
|
|
4
|
+
# to DROP, so NAT alone is not enough). Needs root; re-execs itself with sudo.
|
|
5
|
+
# Idempotent. The bridge and the rules vanish on host reboot, so this runs from
|
|
6
|
+
# install-server.sh once and from kvbox-server at start whenever the bridge is missing.
|
|
7
|
+
#
|
|
8
|
+
# net-setup.sh [up] create/repair everything
|
|
9
|
+
# net-setup.sh down remove the rules and the bridge (detaches any taps still on it)
|
|
10
|
+
# net-setup.sh status show what is in place; exit 1 if anything is missing
|
|
11
|
+
# net-setup.sh tap add <name> create tap <name> owned by the calling user, plugged into the bridge
|
|
12
|
+
# net-setup.sh tap add <name> detached the same, but up and not plugged in yet (a restored box keeps
|
|
13
|
+
# its old addresses until kvbox-agent changes them)
|
|
14
|
+
# net-setup.sh tap attach <name> plug a detached tap into the bridge
|
|
15
|
+
# net-setup.sh tap del <name> delete it
|
|
16
|
+
# net-setup.sh proxy add <box-id> <box-ip> <host-port> <guest-port> [tcp|udp]
|
|
17
|
+
# port <host-port> on this host's public side reaches <box-ip>:<guest-port>
|
|
18
|
+
# (DNAT + forward accept, rules tagged kvbox:<box-id>; tcp by default)
|
|
19
|
+
# net-setup.sh proxy rm <box-id> <box-ip> <host-port> <guest-port> [tcp|udp] remove exactly that
|
|
20
|
+
# net-setup.sh relay up <conf> [port] install <conf> (written by kvbox-server relay) as /etc/wireguard/kvbox.conf,
|
|
21
|
+
# keep the tunnel to the relay up, now and after reboots (wg-quick@kvbox), and
|
|
22
|
+
# let the control plane's HTTPS port (default 7777) in on the tunnel interface
|
|
23
|
+
# (a host firewall such as ufw would otherwise drop it; box ports are forwarded)
|
|
24
|
+
# net-setup.sh relay down take the tunnel down and remove the conf
|
|
25
|
+
# net-setup.sh relay status is the tunnel up, when did the relay last answer
|
|
26
|
+
#
|
|
27
|
+
# KVBOX_BRIDGE (default kvbox0) KVBOX_SUBNET (default 172.30.0.0/24)
|
|
28
|
+
# KVBOX_GATEWAY (default 172.30.0.1) the host's address on the bridge
|
|
29
|
+
set -euo pipefail
|
|
30
|
+
|
|
31
|
+
BRIDGE="${KVBOX_BRIDGE:-kvbox0}"
|
|
32
|
+
SUBNET="${KVBOX_SUBNET:-172.30.0.0/24}"
|
|
33
|
+
GATEWAY="${KVBOX_GATEWAY:-172.30.0.1}"
|
|
34
|
+
PREFIX="${SUBNET#*/}"
|
|
35
|
+
TAG="kvbox:$BRIDGE" # iptables comment; `down` finds our rules by it
|
|
36
|
+
action="${1:-up}"
|
|
37
|
+
tap_op="${2:-}"; tap_name="${3:-}"; tap_mode="${4:-}"
|
|
38
|
+
px_op="${2:-}"; px_box="${3:-}"; px_ip="${4:-}"; px_hport="${5:-}"; px_gport="${6:-}"; px_proto="${7:-tcp}"
|
|
39
|
+
relay_op="${2:-}"; relay_conf="${3:-}"; relay_port="${4:-7777}"
|
|
40
|
+
RELAY_IF=kvbox # wg-quick@kvbox reads /etc/wireguard/kvbox.conf
|
|
41
|
+
|
|
42
|
+
if [[ -t 1 ]]; then GREEN=$'\033[0;32m'; RED=$'\033[0;31m'; DIM=$'\033[2m'; NC=$'\033[0m'; else GREEN=""; RED=""; DIM=""; NC=""; fi
|
|
43
|
+
ok() { printf '%s✓%s %s\n' "$GREEN" "$NC" "$*"; }
|
|
44
|
+
skip() { printf '%s–%s %s\n' "$DIM" "$NC" "$*"; }
|
|
45
|
+
fail() { printf '%s✗%s %s\n' "$RED" "$NC" "$*" >&2; exit 1; }
|
|
46
|
+
|
|
47
|
+
case "$action" in
|
|
48
|
+
up|down|status) ;;
|
|
49
|
+
tap) [[ "$tap_op" =~ ^(add|attach|del)$ && "$tap_name" =~ ^[a-z0-9][a-z0-9._-]{0,14}$ && "$tap_mode" =~ ^(|detached)$ ]] \
|
|
50
|
+
|| fail "usage: $0 tap add <name> [detached] | tap attach <name> | tap del <name> (name: up to 15 chars, [a-z0-9._-])" ;;
|
|
51
|
+
proxy) [[ "$px_op" =~ ^(add|rm)$ && "$px_box" =~ ^[a-z0-9][a-z0-9._-]{0,14}$ && "$px_ip" =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ \
|
|
52
|
+
&& "$px_hport" =~ ^[0-9]{1,5}$ && "$px_gport" =~ ^[0-9]{1,5}$ && "$px_hport" -ne 22 && "$px_proto" =~ ^(tcp|udp)$ ]] \
|
|
53
|
+
|| fail "usage: $0 proxy add|rm <box-id> <box-ip> <host-port> <guest-port> [tcp|udp] (host port is never 22)" ;;
|
|
54
|
+
relay) if [[ ! "$relay_op" =~ ^(up|down|status)$ ]] || { [[ "$relay_op" == up ]] && [[ -z "$relay_conf" ]]; } || [[ ! "$relay_port" =~ ^[0-9]{1,5}$ ]]; then
|
|
55
|
+
fail "usage: $0 relay up <conf> [port] | relay down | relay status"
|
|
56
|
+
fi ;;
|
|
57
|
+
*) fail "usage: $0 [up|down|status|tap add|attach|del <name>|proxy add|rm ...|relay up|down|status]" ;;
|
|
58
|
+
esac
|
|
59
|
+
for bin in ip iptables sysctl; do command -v "$bin" >/dev/null 2>&1 || fail "$bin not found"; done
|
|
60
|
+
if [[ "$action" == relay ]]; then
|
|
61
|
+
# The conf is read before sudo drops the caller's view of its home: resolve it while we are still that user.
|
|
62
|
+
[[ "$relay_op" != up ]] || [[ -r "$relay_conf" ]] || fail "cannot read $relay_conf"
|
|
63
|
+
[[ "$relay_op" != up ]] || relay_conf="$(readlink -f "$relay_conf")"
|
|
64
|
+
command -v wg-quick >/dev/null 2>&1 || fail "WireGuard is not installed on this host (apt install wireguard-tools), so it cannot use a relay"
|
|
65
|
+
command -v systemctl >/dev/null 2>&1 || fail "systemd is needed to keep the relay tunnel up across reboots"
|
|
66
|
+
fi
|
|
67
|
+
if [[ $EUID -ne 0 ]]; then
|
|
68
|
+
# From a terminal sudo may prompt; from kvbox-server (no tty) it must not hang.
|
|
69
|
+
# KVBOX_TAP_USER tells the root side who should own a new tap (Firecracker runs as that user).
|
|
70
|
+
export KVBOX_TAP_USER="${KVBOX_TAP_USER:-$(id -un)}"
|
|
71
|
+
if [[ -t 0 ]]; then exec sudo --preserve-env=KVBOX_TAP_USER,KVBOX_BRIDGE,KVBOX_SUBNET,KVBOX_GATEWAY "$0" "$@"; fi
|
|
72
|
+
exec sudo -n --preserve-env=KVBOX_TAP_USER,KVBOX_BRIDGE,KVBOX_SUBNET,KVBOX_GATEWAY "$0" "$@"
|
|
73
|
+
fi
|
|
74
|
+
if [[ "$action" == proxy ]]; then
|
|
75
|
+
command -v iptables-save >/dev/null 2>&1 || fail "iptables-save not found"
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# ── rule set ─────────────────────────────────────────────────────────────────
|
|
79
|
+
# RULE_DESC[i] is what we tell the user; RULE_SPEC[i] is "<table> <chain> <rule...>".
|
|
80
|
+
# Added with -I (ahead of Docker's chains), checked with -C, found again by the comment tag.
|
|
81
|
+
RULE_DESC=(
|
|
82
|
+
"NAT: boxes reach the internet using this host's address"
|
|
83
|
+
"firewall: traffic from boxes may leave through $BRIDGE"
|
|
84
|
+
"firewall: replies may come back to boxes"
|
|
85
|
+
)
|
|
86
|
+
RULE_SPEC=(
|
|
87
|
+
"nat POSTROUTING -s $SUBNET ! -o $BRIDGE -m comment --comment $TAG -j MASQUERADE"
|
|
88
|
+
"filter FORWARD -i $BRIDGE -m comment --comment $TAG -j ACCEPT"
|
|
89
|
+
"filter FORWARD -o $BRIDGE -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment $TAG -j ACCEPT"
|
|
90
|
+
)
|
|
91
|
+
has_rule() { local t=$1 c=$2; shift 2; iptables -t "$t" -C "$c" "$@" 2>/dev/null; }
|
|
92
|
+
add_rule() { local t=$1 c=$2; shift 2; iptables -t "$t" -I "$c" 1 "$@"; }
|
|
93
|
+
del_rule() { local t=$1 c=$2; shift 2; while iptables -t "$t" -C "$c" "$@" 2>/dev/null; do iptables -t "$t" -D "$c" "$@"; done; }
|
|
94
|
+
|
|
95
|
+
bridge_exists() { [[ -d "/sys/class/net/$BRIDGE" ]]; }
|
|
96
|
+
taps_on_bridge() { local t out=""; for t in "/sys/class/net/$BRIDGE/brif"/*; do [[ -e "$t" ]] && out+="${t##*/} "; done; printf '%s' "$out"; }
|
|
97
|
+
bridge_has_ip() { ip -4 -o addr show dev "$BRIDGE" 2>/dev/null | grep -q " inet $GATEWAY/$PREFIX "; }
|
|
98
|
+
# Administrative UP flag only: an empty bridge reports operstate "down" (NO-CARRIER) until a tap joins.
|
|
99
|
+
bridge_is_up() { ip link show "$BRIDGE" 2>/dev/null | head -1 | grep -q "[<,]UP[,>]"; }
|
|
100
|
+
|
|
101
|
+
# ── up ───────────────────────────────────────────────────────────────────────
|
|
102
|
+
do_up() {
|
|
103
|
+
if bridge_exists; then skip "bridge $BRIDGE already exists"; else ip link add "$BRIDGE" type bridge; ok "created bridge $BRIDGE (the virtual switch boxes plug into)"; fi
|
|
104
|
+
if bridge_has_ip; then skip "$BRIDGE already has address $GATEWAY"; else ip addr add "$GATEWAY/$PREFIX" dev "$BRIDGE"; ok "gave this host address $GATEWAY on $BRIDGE (boxes use it as their gateway)"; fi
|
|
105
|
+
if bridge_is_up; then skip "$BRIDGE is already up"; else ip link set "$BRIDGE" up; ok "brought $BRIDGE up"; fi
|
|
106
|
+
if [[ "$(sysctl -n net.ipv4.ip_forward)" == 1 ]]; then skip "IP forwarding already enabled"; else sysctl -q -w net.ipv4.ip_forward=1; ok "enabled IP forwarding (this host routes for the boxes)"; fi
|
|
107
|
+
local i
|
|
108
|
+
for i in "${!RULE_SPEC[@]}"; do
|
|
109
|
+
# shellcheck disable=SC2086 # the rule is a word list by design
|
|
110
|
+
if has_rule ${RULE_SPEC[$i]}; then skip "${RULE_DESC[$i]} (already set)"; else add_rule ${RULE_SPEC[$i]}; ok "${RULE_DESC[$i]}"; fi
|
|
111
|
+
done
|
|
112
|
+
echo
|
|
113
|
+
ok "host network ready: boxes get addresses in $SUBNET on $BRIDGE, gateway $GATEWAY, internet via NAT"
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# ── down ─────────────────────────────────────────────────────────────────────
|
|
117
|
+
do_down() {
|
|
118
|
+
local i
|
|
119
|
+
for i in "${!RULE_SPEC[@]}"; do
|
|
120
|
+
# shellcheck disable=SC2086
|
|
121
|
+
if has_rule ${RULE_SPEC[$i]}; then del_rule ${RULE_SPEC[$i]}; ok "removed: ${RULE_DESC[$i]}"; else skip "not set: ${RULE_DESC[$i]}"; fi
|
|
122
|
+
done
|
|
123
|
+
if bridge_exists; then
|
|
124
|
+
local taps; taps="$(taps_on_bridge)"
|
|
125
|
+
[[ -z "$taps" ]] || printf '! detaching taps still on %s: %s\n' "$BRIDGE" "$taps"
|
|
126
|
+
ip link del "$BRIDGE"; ok "deleted bridge $BRIDGE"
|
|
127
|
+
else
|
|
128
|
+
skip "bridge $BRIDGE does not exist"
|
|
129
|
+
fi
|
|
130
|
+
# IP forwarding is left as is: other software (Docker) relies on it too.
|
|
131
|
+
local n=0 t rule
|
|
132
|
+
while IFS=$'\t' read -r t rule; do
|
|
133
|
+
[[ -n "$rule" ]] || continue
|
|
134
|
+
# shellcheck disable=SC2086 # iptables-save wrote the rule as a word list
|
|
135
|
+
eval "iptables -t $t -D $rule" && n=$((n + 1))
|
|
136
|
+
done < <(proxy_rules)
|
|
137
|
+
if [[ $n -gt 0 ]]; then ok "removed $n box port forward rule(s)"; else skip "no box port forwards"; fi
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
# ── status ───────────────────────────────────────────────────────────────────
|
|
141
|
+
do_status() {
|
|
142
|
+
local missing=0 i
|
|
143
|
+
bad() { printf '%s✗%s %s\n' "$RED" "$NC" "$*"; missing=1; }
|
|
144
|
+
if bridge_exists && bridge_has_ip && bridge_is_up; then ok "bridge $BRIDGE is up with address $GATEWAY/$PREFIX"
|
|
145
|
+
elif ! bridge_exists; then bad "bridge $BRIDGE does not exist"
|
|
146
|
+
elif ! bridge_has_ip; then bad "bridge $BRIDGE has no address $GATEWAY"
|
|
147
|
+
else bad "bridge $BRIDGE is down"; fi
|
|
148
|
+
if [[ "$(sysctl -n net.ipv4.ip_forward)" == 1 ]]; then ok "IP forwarding enabled"; else bad "IP forwarding disabled"; fi
|
|
149
|
+
for i in "${!RULE_SPEC[@]}"; do
|
|
150
|
+
# shellcheck disable=SC2086
|
|
151
|
+
if has_rule ${RULE_SPEC[$i]}; then ok "${RULE_DESC[$i]}"; else bad "missing: ${RULE_DESC[$i]}"; fi
|
|
152
|
+
done
|
|
153
|
+
local taps; taps="$(taps_on_bridge)"
|
|
154
|
+
if [[ -z "$taps" ]]; then skip "no boxes attached to $BRIDGE yet"; else ok "boxes attached to $BRIDGE: $taps"; fi
|
|
155
|
+
local fwds; fwds="$(proxy_summary)"
|
|
156
|
+
if [[ -z "$fwds" ]]; then skip "no box ports published on this host"; else ok "box ports published on this host:"; printf '%s\n' "$fwds"; fi
|
|
157
|
+
if [[ $missing -eq 0 ]]; then echo; ok "host network is ready"; else echo; printf '%s✗%s host network incomplete; run: kvbox-server net up\n' "$RED" "$NC"; fi
|
|
158
|
+
return $missing
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# ── tap add|del ──────────────────────────────────────────────────────────────
|
|
162
|
+
# One tap per box: a virtual cable with one end in the VM and the other plugged into the bridge.
|
|
163
|
+
do_tap_add() {
|
|
164
|
+
local tap=$1 mode=${2:-} owner="${KVBOX_TAP_USER:-${SUDO_USER:-root}}"
|
|
165
|
+
bridge_exists || fail "bridge $BRIDGE does not exist; run: kvbox-server net up"
|
|
166
|
+
if [[ -d "/sys/class/net/$tap" ]]; then
|
|
167
|
+
skip "tap $tap already exists"
|
|
168
|
+
else
|
|
169
|
+
ip tuntap add dev "$tap" mode tap user "$owner"
|
|
170
|
+
ok "created tap $tap (owner $owner)"
|
|
171
|
+
fi
|
|
172
|
+
if [[ "$mode" == detached ]]; then
|
|
173
|
+
ip link set "$tap" up
|
|
174
|
+
ok "tap $tap is up, not plugged into $BRIDGE yet"
|
|
175
|
+
else
|
|
176
|
+
do_tap_attach "$tap"
|
|
177
|
+
fi
|
|
178
|
+
}
|
|
179
|
+
do_tap_attach() {
|
|
180
|
+
local tap=$1
|
|
181
|
+
[[ -d "/sys/class/net/$tap" ]] || fail "tap $tap does not exist"
|
|
182
|
+
ip link set "$tap" master "$BRIDGE" up
|
|
183
|
+
ok "tap $tap plugged into $BRIDGE"
|
|
184
|
+
}
|
|
185
|
+
do_tap_del() {
|
|
186
|
+
local tap=$1
|
|
187
|
+
if [[ -d "/sys/class/net/$tap" ]]; then ip link del "$tap"; ok "deleted tap $tap"; else skip "tap $tap does not exist"; fi
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# ── proxy add|rm ─────────────────────────────────────────────────────────────
|
|
191
|
+
# A box's port on the host's public side (#49: SSH; #30: anything). Three rules per forward, all
|
|
192
|
+
# tagged kvbox:<box-id>: DNAT for traffic arriving from outside, DNAT for traffic the host itself
|
|
193
|
+
# sends to its own address (so the forward works from the host too), and a FORWARD accept so the
|
|
194
|
+
# new connection may enter the bridge (the base rules only let replies back in).
|
|
195
|
+
proxy_specs() {
|
|
196
|
+
local ip=$2 hport=$3 gport=$4 proto=${5:-tcp} tag="kvbox:$1"
|
|
197
|
+
printf '%s\n' \
|
|
198
|
+
"nat PREROUTING ! -i $BRIDGE -p $proto --dport $hport -m comment --comment $tag -j DNAT --to-destination $ip:$gport" \
|
|
199
|
+
"nat OUTPUT -p $proto --dport $hport -m addrtype --dst-type LOCAL -m comment --comment $tag -j DNAT --to-destination $ip:$gport" \
|
|
200
|
+
"filter FORWARD -o $BRIDGE -d $ip -p $proto --dport $gport -m comment --comment $tag -j ACCEPT"
|
|
201
|
+
}
|
|
202
|
+
do_proxy_add() {
|
|
203
|
+
local spec added=0
|
|
204
|
+
while read -r spec; do
|
|
205
|
+
# shellcheck disable=SC2086
|
|
206
|
+
if ! has_rule $spec; then add_rule $spec; added=1; fi
|
|
207
|
+
done < <(proxy_specs "$@")
|
|
208
|
+
if [[ $added -eq 1 ]]; then ok "$5 port $3 on this host now reaches box $1's port $4"; else skip "$5 port $3 on this host already reaches box $1's port $4"; fi
|
|
209
|
+
}
|
|
210
|
+
do_proxy_rm() {
|
|
211
|
+
local spec removed=0
|
|
212
|
+
while read -r spec; do
|
|
213
|
+
# shellcheck disable=SC2086
|
|
214
|
+
if has_rule $spec; then del_rule $spec; removed=1; fi
|
|
215
|
+
done < <(proxy_specs "$@")
|
|
216
|
+
if [[ $removed -eq 1 ]]; then ok "$5 port $3 on this host no longer reaches box $1"; else skip "$5 port $3 on this host was not forwarding to box $1"; fi
|
|
217
|
+
}
|
|
218
|
+
# Every forward rule in place, as "<table>\t<rule without -A>", found by its kvbox:<box-id> tag
|
|
219
|
+
# (the bridge's own rules carry kvbox:<bridge> and are not forwards).
|
|
220
|
+
proxy_rules() {
|
|
221
|
+
local t
|
|
222
|
+
for t in nat filter; do
|
|
223
|
+
iptables-save -t "$t" 2>/dev/null | sed -n 's/^-A //p' | sed 's/--comment "\([^"]*\)"/--comment \1/' \
|
|
224
|
+
| grep -E -- "--comment kvbox:[a-z0-9._-]+( |$)" | grep -v -F -- "--comment $TAG " | sed "s/^/$t\t/" || true
|
|
225
|
+
done
|
|
226
|
+
}
|
|
227
|
+
# One line per published port: "port 31207 -> box a3f9c1 (172.30.0.2:22)".
|
|
228
|
+
proxy_summary() {
|
|
229
|
+
proxy_rules | grep -F "PREROUTING" | sed -E 's/.*-p ([a-z]+) .*--dport ([0-9]+) .*--comment kvbox:([^ ]+) .*--to-destination ([0-9.:]+).*/ \1 port \2 -> box \3 (\4)/'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# ── relay up|down|status ─────────────────────────────────────────────────────
|
|
233
|
+
# The host's end of the tunnel to its relay (#50). kvbox-server writes the wg-quick conf (private
|
|
234
|
+
# key inside) under its state dir; root copies it to /etc/wireguard and lets systemd keep the tunnel
|
|
235
|
+
# up. Nothing here knows the relay's rules: the relay forwards its public IP down to us, and SNATs,
|
|
236
|
+
# so the only route we need is the relay's own tunnel address (in the conf's AllowedIPs).
|
|
237
|
+
relay_unit="wg-quick@$RELAY_IF"
|
|
238
|
+
relay_target="/etc/wireguard/$RELAY_IF.conf"
|
|
239
|
+
# Only the control plane's calls arrive for this host itself; box ports are DNAT'd and go through
|
|
240
|
+
# FORWARD, where the bridge rules already let them in ahead of any firewall chain.
|
|
241
|
+
relay_input_spec() { printf '%s' "filter INPUT -i $RELAY_IF -p tcp --dport $1 -m comment --comment kvbox:relay -j ACCEPT"; }
|
|
242
|
+
relay_input_rules() { iptables-save -t filter 2>/dev/null | sed -n 's/^-A //p' | sed 's/--comment "\([^"]*\)"/--comment \1/' | grep -F -- "--comment kvbox:relay " || true; }
|
|
243
|
+
do_relay_up() {
|
|
244
|
+
local src=$1 port=$2
|
|
245
|
+
install -d -m 0700 /etc/wireguard
|
|
246
|
+
if [[ -f "$relay_target" ]] && cmp -s "$src" "$relay_target"; then
|
|
247
|
+
skip "tunnel conf already installed"
|
|
248
|
+
else
|
|
249
|
+
install -m 0600 -o root -g root "$src" "$relay_target"
|
|
250
|
+
ok "installed the tunnel conf as $relay_target (root only)"
|
|
251
|
+
fi
|
|
252
|
+
if systemctl is-active --quiet "$relay_unit"; then
|
|
253
|
+
# Re-read the conf without dropping the tunnel: a changed relay key or endpoint takes effect now.
|
|
254
|
+
if wg syncconf "$RELAY_IF" <(wg-quick strip "$RELAY_IF") 2>/dev/null; then ok "tunnel $RELAY_IF reloaded"; else systemctl restart "$relay_unit"; ok "tunnel $RELAY_IF restarted"; fi
|
|
255
|
+
else
|
|
256
|
+
systemctl enable --now "$relay_unit" >/dev/null 2>&1 || fail "could not bring the tunnel up (journalctl -u $relay_unit)"
|
|
257
|
+
ok "tunnel $RELAY_IF is up and comes back after a reboot"
|
|
258
|
+
fi
|
|
259
|
+
local spec; spec="$(relay_input_spec "$port")"
|
|
260
|
+
# shellcheck disable=SC2086 # the rule is a word list by design
|
|
261
|
+
if has_rule $spec; then skip "the control plane may already reach port $port through the tunnel"; else add_rule $spec; ok "firewall: the control plane may reach port $port through the tunnel"; fi
|
|
262
|
+
# A rule for another port from an earlier `relay up` would stay open for nothing.
|
|
263
|
+
local rule
|
|
264
|
+
while IFS= read -r rule; do
|
|
265
|
+
[[ -n "$rule" ]] || continue
|
|
266
|
+
[[ "$rule" == *"--dport $port "* ]] && continue
|
|
267
|
+
# shellcheck disable=SC2086 # iptables-save wrote the rule as a word list
|
|
268
|
+
eval "iptables -t filter -D $rule" && ok "firewall: closed the tunnel's old port $(sed -E 's/.*--dport ([0-9]+) .*/\1/' <<<"$rule")"
|
|
269
|
+
done < <(relay_input_rules)
|
|
270
|
+
}
|
|
271
|
+
do_relay_down() {
|
|
272
|
+
local rule n=0
|
|
273
|
+
while IFS= read -r rule; do
|
|
274
|
+
[[ -n "$rule" ]] || continue
|
|
275
|
+
# shellcheck disable=SC2086
|
|
276
|
+
eval "iptables -t filter -D $rule" && n=$((n + 1))
|
|
277
|
+
done < <(relay_input_rules)
|
|
278
|
+
if [[ $n -gt 0 ]]; then ok "firewall: the tunnel's port is closed again"; fi
|
|
279
|
+
if systemctl is-enabled --quiet "$relay_unit" 2>/dev/null || systemctl is-active --quiet "$relay_unit"; then
|
|
280
|
+
systemctl disable --now "$relay_unit" >/dev/null 2>&1 || true
|
|
281
|
+
ok "tunnel $RELAY_IF is down"
|
|
282
|
+
else
|
|
283
|
+
skip "tunnel $RELAY_IF was not up"
|
|
284
|
+
fi
|
|
285
|
+
if [[ -f "$relay_target" ]]; then rm -f "$relay_target"; ok "removed $relay_target"; else skip "no tunnel conf installed"; fi
|
|
286
|
+
}
|
|
287
|
+
do_relay_status() {
|
|
288
|
+
[[ -f "$relay_target" ]] || { skip "no relay tunnel configured on this host"; return 1; }
|
|
289
|
+
if ! systemctl is-active --quiet "$relay_unit"; then printf '%s✗%s tunnel %s is down (journalctl -u %s)\n' "$RED" "$NC" "$RELAY_IF" "$relay_unit"; return 1; fi
|
|
290
|
+
local hs; hs="$(wg show "$RELAY_IF" latest-handshakes 2>/dev/null | awk '{print $2}' | head -1)"
|
|
291
|
+
if [[ -z "$hs" || "$hs" == 0 ]]; then
|
|
292
|
+
printf '%s!%s tunnel %s is up but the relay has not answered yet\n' "$RED" "$NC" "$RELAY_IF"; return 1
|
|
293
|
+
fi
|
|
294
|
+
ok "tunnel $RELAY_IF is up; the relay last answered $(( $(date +%s) - hs )) s ago"
|
|
295
|
+
local rules; rules="$(relay_input_rules)"
|
|
296
|
+
if [[ -z "$rules" ]]; then printf '%s✗%s the control plane cannot reach this host through the tunnel (no firewall rule); rerun kvbox server relay\n' "$RED" "$NC"; return 1; fi
|
|
297
|
+
ok "firewall: the control plane may reach port $(sed -E 's/.*--dport ([0-9]+) .*/\1/' <<<"$rules" | head -1) through the tunnel"
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case "$action" in
|
|
301
|
+
up) do_up ;;
|
|
302
|
+
down) do_down ;;
|
|
303
|
+
status) do_status ;;
|
|
304
|
+
tap) case "$tap_op" in
|
|
305
|
+
add) do_tap_add "$tap_name" "$tap_mode" ;;
|
|
306
|
+
attach) do_tap_attach "$tap_name" ;;
|
|
307
|
+
del) do_tap_del "$tap_name" ;;
|
|
308
|
+
esac ;;
|
|
309
|
+
proxy) case "$px_op" in
|
|
310
|
+
add) do_proxy_add "$px_box" "$px_ip" "$px_hport" "$px_gport" "$px_proto" ;;
|
|
311
|
+
rm) do_proxy_rm "$px_box" "$px_ip" "$px_hport" "$px_gport" "$px_proto" ;;
|
|
312
|
+
esac ;;
|
|
313
|
+
relay) case "$relay_op" in
|
|
314
|
+
up) do_relay_up "$relay_conf" "$relay_port" ;;
|
|
315
|
+
down) do_relay_down ;;
|
|
316
|
+
status) do_relay_status ;;
|
|
317
|
+
esac ;;
|
|
318
|
+
esac
|
package/README.md
DELETED