@kvbox/server 0.5.8 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kvbox/server",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "kvbox host daemon: owns Firecracker microVMs on a Linux host and serves the kvbox API. Managed with the @kvbox/cli CLI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bash
2
2
  # Host networking for kvbox: bridge kvbox0 (172.30.0.1/24), IP forwarding, NAT to the
3
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.
4
+ # to DROP, so NAT alone is not enough), and the guard around boxes: no box to box, box to
5
+ # host, box to private networks or outbound mail. Needs root; re-execs itself with sudo.
5
6
  # Idempotent. The bridge and the rules vanish on host reboot, so this runs from
6
7
  # install-server.sh once and from kvbox-server at start whenever the bridge is missing.
7
8
  #
@@ -21,6 +22,7 @@
21
22
  # keep the tunnel to the relay up, now and after reboots (wg-quick@kvbox), and
22
23
  # let the control plane's HTTPS port (default 7777) in on the tunnel interface
23
24
  # (a host firewall such as ufw would otherwise drop it; box ports are forwarded)
25
+ # net-setup.sh relay restart redial the tunnel (kvbox-server does it when the relay stops answering)
24
26
  # net-setup.sh relay down take the tunnel down and remove the conf
25
27
  # net-setup.sh relay status is the tunnel up, when did the relay last answer
26
28
  #
@@ -51,10 +53,10 @@ case "$action" in
51
53
  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
54
  && "$px_hport" =~ ^[0-9]{1,5}$ && "$px_gport" =~ ^[0-9]{1,5}$ && "$px_hport" -ne 22 && "$px_proto" =~ ^(tcp|udp)$ ]] \
53
55
  || 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
+ relay) if [[ ! "$relay_op" =~ ^(up|restart|down|status)$ ]] || { [[ "$relay_op" == up ]] && [[ -z "$relay_conf" ]]; } || [[ ! "$relay_port" =~ ^[0-9]{1,5}$ ]]; then
57
+ fail "usage: $0 relay up <conf> [port] | relay restart | relay down | relay status"
56
58
  fi ;;
57
- *) fail "usage: $0 [up|down|status|tap add|attach|del <name>|proxy add|rm ...|relay up|down|status]" ;;
59
+ *) fail "usage: $0 [up|down|status|tap add|attach|del <name>|proxy add|rm ...|relay up|restart|down|status]" ;;
58
60
  esac
59
61
  for bin in ip iptables sysctl; do command -v "$bin" >/dev/null 2>&1 || fail "$bin not found"; done
60
62
  if [[ "$action" == relay ]]; then
@@ -78,6 +80,11 @@ fi
78
80
  # ── rule set ─────────────────────────────────────────────────────────────────
79
81
  # RULE_DESC[i] is what we tell the user; RULE_SPEC[i] is "<table> <chain> <rule...>".
80
82
  # Added with -I (ahead of Docker's chains), checked with -C, found again by the comment tag.
83
+ # Each rule is inserted at the top, so a later entry ends up above an earlier one: the replies
84
+ # accept comes last so it sits above the drops, and the drops sit above the general accept.
85
+ # Boxes are strangers to each other and to this host (box to box is cut at the bridge, see
86
+ # do_tap_attach): a box reaches the internet only, never the host, its LAN or cloud metadata.
87
+ PRIVATE_NETS=(10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 100.64.0.0/10 169.254.0.0/16)
81
88
  RULE_DESC=(
82
89
  "NAT: boxes reach the internet using this host's address"
83
90
  "firewall: traffic from boxes may leave through $BRIDGE"
@@ -88,6 +95,22 @@ RULE_SPEC=(
88
95
  "filter FORWARD -i $BRIDGE -m comment --comment $TAG -j ACCEPT"
89
96
  "filter FORWARD -o $BRIDGE -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment $TAG -j ACCEPT"
90
97
  )
98
+ for net in "${PRIVATE_NETS[@]}"; do
99
+ RULE_DESC+=("firewall: boxes cannot reach private network $net (this host's LAN, provider networks, cloud metadata)")
100
+ RULE_SPEC+=("filter FORWARD -i $BRIDGE -d $net -m comment --comment $TAG -j DROP")
101
+ done
102
+ RULE_DESC+=(
103
+ "firewall: boxes cannot send mail directly (port 25), so spam never gets this host's address blocked"
104
+ "firewall: boxes may answer connections made to them (published ports, the relay)"
105
+ "firewall: boxes cannot reach this host itself"
106
+ "firewall: this host may still talk to its boxes"
107
+ )
108
+ RULE_SPEC+=(
109
+ "filter FORWARD -i $BRIDGE -p tcp --dport 25 -m comment --comment $TAG -j DROP"
110
+ "filter FORWARD -i $BRIDGE -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment $TAG -j ACCEPT"
111
+ "filter INPUT -i $BRIDGE -m comment --comment $TAG -j DROP"
112
+ "filter INPUT -i $BRIDGE -m conntrack --ctstate RELATED,ESTABLISHED -m comment --comment $TAG -j ACCEPT"
113
+ )
91
114
  has_rule() { local t=$1 c=$2; shift 2; iptables -t "$t" -C "$c" "$@" 2>/dev/null; }
92
115
  add_rule() { local t=$1 c=$2; shift 2; iptables -t "$t" -I "$c" 1 "$@"; }
93
116
  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; }
@@ -104,11 +127,24 @@ do_up() {
104
127
  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
128
  if bridge_is_up; then skip "$BRIDGE is already up"; else ip link set "$BRIDGE" up; ok "brought $BRIDGE up"; fi
106
129
  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
130
+ local i missing=0 present=()
108
131
  for i in "${!RULE_SPEC[@]}"; do
109
132
  # 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
133
+ if has_rule ${RULE_SPEC[$i]}; then present[i]=1; else missing=1; fi
111
134
  done
135
+ # Order matters (accepts above drops), and a rule inserted alone would land on top of the others:
136
+ # when anything is missing, take the set out and put it all back in order.
137
+ if [[ $missing -eq 1 ]]; then
138
+ # shellcheck disable=SC2086
139
+ for i in "${!RULE_SPEC[@]}"; do del_rule ${RULE_SPEC[$i]}; done
140
+ fi
141
+ for i in "${!RULE_SPEC[@]}"; do
142
+ # shellcheck disable=SC2086
143
+ [[ $missing -eq 1 ]] && add_rule ${RULE_SPEC[$i]}
144
+ if [[ -n "${present[i]:-}" ]]; then skip "${RULE_DESC[$i]} (already set)"; else ok "${RULE_DESC[$i]}"; fi
145
+ done
146
+ local tap
147
+ for tap in $(taps_on_bridge); do isolate_tap "$tap"; done
112
148
  echo
113
149
  ok "host network ready: boxes get addresses in $SUBNET on $BRIDGE, gateway $GATEWAY, internet via NAT"
114
150
  }
@@ -152,6 +188,8 @@ do_status() {
152
188
  done
153
189
  local taps; taps="$(taps_on_bridge)"
154
190
  if [[ -z "$taps" ]]; then skip "no boxes attached to $BRIDGE yet"; else ok "boxes attached to $BRIDGE: $taps"; fi
191
+ local tap
192
+ for tap in $taps; do tap_isolated "$tap" || bad "box on $tap can reach other boxes (tap not isolated)"; done
155
193
  local fwds; fwds="$(proxy_summary)"
156
194
  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
195
  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
@@ -180,8 +218,13 @@ do_tap_attach() {
180
218
  local tap=$1
181
219
  [[ -d "/sys/class/net/$tap" ]] || fail "tap $tap does not exist"
182
220
  ip link set "$tap" master "$BRIDGE" up
221
+ isolate_tap "$tap"
183
222
  ok "tap $tap plugged into $BRIDGE"
184
223
  }
224
+ # An isolated bridge port talks to the host (the gateway) but never to another isolated port:
225
+ # one box cannot see another box's ports, whoever owns them.
226
+ isolate_tap() { ip link set dev "$1" type bridge_slave isolated on; }
227
+ tap_isolated() { ip -d link show dev "$1" 2>/dev/null | grep -q "isolated on"; }
185
228
  do_tap_del() {
186
229
  local tap=$1
187
230
  if [[ -d "/sys/class/net/$tap" ]]; then ip link del "$tap"; ok "deleted tap $tap"; else skip "tap $tap does not exist"; fi
@@ -268,6 +311,13 @@ do_relay_up() {
268
311
  eval "iptables -t filter -D $rule" && ok "firewall: closed the tunnel's old port $(sed -E 's/.*--dport ([0-9]+) .*/\1/' <<<"$rule")"
269
312
  done < <(relay_input_rules)
270
313
  }
314
+ # A new process dials from a new UDP port, which a home router maps afresh: the way out of a mapping
315
+ # that went stale while the relay rebooted (the relay hears us but cannot answer).
316
+ do_relay_restart() {
317
+ [[ -f "$relay_target" ]] || fail "no relay tunnel configured on this host"
318
+ systemctl restart "$relay_unit" || fail "could not restart the tunnel (journalctl -u $relay_unit)"
319
+ ok "tunnel $RELAY_IF restarted"
320
+ }
271
321
  do_relay_down() {
272
322
  local rule n=0
273
323
  while IFS= read -r rule; do
@@ -312,6 +362,7 @@ case "$action" in
312
362
  esac ;;
313
363
  relay) case "$relay_op" in
314
364
  up) do_relay_up "$relay_conf" "$relay_port" ;;
365
+ restart) do_relay_restart ;;
315
366
  down) do_relay_down ;;
316
367
  status) do_relay_status ;;
317
368
  esac ;;