fix(bootstrap): open the nano port to the proxy alone
For a non-loopback bind, configure_nano_firewall opened the nano port in firewalld to every source, while the summary told the operator to restrict it to the proxy. hasJoined takes no token, so on a public host that port is an auth relay anyone can point a proxy at, spending this host's Mojang egress until Mojang rate-limits it and the operator's own players stop getting in. A new FELIS_NANO_PROXY_CIDR names the proxy. With it, firewalld gets one rich rule that admits the port from that source only, ipv4 or ipv6 by the address given. Without it, no port is opened and the summary prints the rule to add. A re-run closes the port an earlier installer opened to every source. A rule for a previous FELIS_NANO_PROXY_CIDR is not tracked and stays until removed by hand. Hosts without firewalld are handled as before. The value goes into the rule text, so it is checked up front for an address with one prefix length and nothing else. firewalld's own parser accepts both rule forms and refuses an ipv6 address under the ipv4 family. The harness covers the rule for each family, the closed-by-default case, the re-run cleanup, the loopback case and the CIDR check.
This commit is contained in:
2 files changed
+117
-10
No files matched your search
+41
-8
@@ -35,6 +35,9 @@
|
||||
# FELIS_NANO_LISTEN listen addr for `felis nano` (default: the address an installed
|
||||
# felis-nano already uses, else 127.0.0.1:8081 — loopback only; set a
|
||||
# private-network IP to serve an off-host proxy)
|
||||
# FELIS_NANO_PROXY_CIDR the proxy allowed to reach a non-loopback nano bind, as an address
|
||||
# with a prefix length (for example 10.0.0.7/32). firewalld opens the
|
||||
# port to that source only; unset, it opens nothing
|
||||
# FELIS_LEGACY_FORWARDING_SERVERS comma-separated backends that receive their identity
|
||||
# through the handshake address instead of modern forwarding
|
||||
# (default: legacy18). Read once at Velocity start, so changing it
|
||||
@@ -112,6 +115,7 @@ INSTALL_MODE="${FELIS_INSTALL_MODE:-}"
|
||||
# Left empty here: resolve_nano_listen applies that default only after an existing unit's
|
||||
# address has had its say.
|
||||
FELIS_NANO_LISTEN="${FELIS_NANO_LISTEN:-}"
|
||||
FELIS_NANO_PROXY_CIDR="${FELIS_NANO_PROXY_CIDR:-}"
|
||||
# Backends that take their forwarded identity through the handshake address instead of
|
||||
# proxy-wide modern forwarding. See write_velocity_service for why a protocol-47 backend
|
||||
# needs this. Overridable because adding a second 1.8 backend otherwise means editing this
|
||||
@@ -465,11 +469,23 @@ validate_listen() {
|
||||
esac
|
||||
}
|
||||
|
||||
# The value lands inside a firewalld rich rule, so anything but address characters and one
|
||||
# prefix length is refused here rather than handed to firewall-cmd.
|
||||
validate_cidr() {
|
||||
case "$2" in
|
||||
"") return 0 ;;
|
||||
*[!0-9A-Fa-f.:/]*|*/*/*|*/|/*) ;;
|
||||
*/[0-9]*) return 0 ;;
|
||||
esac
|
||||
die "$1 must be an address with a prefix length (for example 10.0.0.7/32 or fd00::7/128), got: $2"
|
||||
}
|
||||
|
||||
validate_settings() {
|
||||
validate_timeout PKG_LOCK_TIMEOUT "$PKG_LOCK_TIMEOUT"
|
||||
validate_timeout APT_LOCK_TIMEOUT "$APT_LOCK_TIMEOUT"
|
||||
validate_nodeport FELIS_PANEL_NODEPORT "$FELIS_PANEL_NODEPORT"
|
||||
validate_listen FELIS_NANO_LISTEN "$FELIS_NANO_LISTEN"
|
||||
validate_cidr FELIS_NANO_PROXY_CIDR "$FELIS_NANO_PROXY_CIDR"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2428,9 +2444,21 @@ configure_nano_firewall() {
|
||||
fi
|
||||
command -v firewall-cmd >/dev/null 2>&1 || return 0
|
||||
systemctl is-active --quiet firewalld || return 0
|
||||
local port="${FELIS_NANO_LISTEN##*:}"
|
||||
log "opening firewalld port ${port}/tcp for felis-nano"
|
||||
firewall-cmd --permanent --add-port="${port}/tcp"
|
||||
local port="${FELIS_NANO_LISTEN##*:}" family=ipv4
|
||||
# hasJoined takes no token, so the port is opened to the proxy alone. Earlier installers
|
||||
# opened it to every source, and a re-run must not leave that behind. A rule for a previous
|
||||
# FELIS_NANO_PROXY_CIDR is not tracked; it stays until removed by hand.
|
||||
if firewall-cmd --permanent --query-port="${port}/tcp" >/dev/null 2>&1; then
|
||||
log "closing firewalld port ${port}/tcp, which an earlier install opened to every source"
|
||||
firewall-cmd --permanent --remove-port="${port}/tcp"
|
||||
fi
|
||||
if [ -n "$FELIS_NANO_PROXY_CIDR" ]; then
|
||||
case "$FELIS_NANO_PROXY_CIDR" in *:*) family=ipv6 ;; esac
|
||||
log "opening firewalld port ${port}/tcp to ${FELIS_NANO_PROXY_CIDR} only"
|
||||
firewall-cmd --permanent --add-rich-rule="rule family=\"${family}\" source address=\"${FELIS_NANO_PROXY_CIDR}\" port port=\"${port}\" protocol=\"tcp\" accept"
|
||||
else
|
||||
warn "no FELIS_NANO_PROXY_CIDR, so firewalld keeps ${port}/tcp closed; the summary shows how to admit your proxy"
|
||||
fi
|
||||
firewall-cmd --reload
|
||||
}
|
||||
|
||||
@@ -2489,12 +2517,17 @@ summary_nano() {
|
||||
log "Bound to loopback: reachable from Velocity on THIS host, and from nowhere else."
|
||||
log "Proxy on another machine? Re-run with the address on the sudo line (sudo drops"
|
||||
log "exported variables):"
|
||||
log " curl -fsSL <raw-url>/deploy/bootstrap.sh | sudo FELIS_NANO_LISTEN=<private-ip>:${port} bash"
|
||||
log "and allow ${port}/tcp ONLY from that proxy — hasJoined takes no auth token, so an"
|
||||
log "internet-facing one is a free auth relay burning your Mojang egress IP."
|
||||
log " curl -fsSL <raw-url>/deploy/bootstrap.sh | sudo FELIS_NANO_LISTEN=<private-ip>:${port} FELIS_NANO_PROXY_CIDR=<proxy-ip>/32 bash"
|
||||
log "firewalld then admits ${port}/tcp ONLY from that proxy — hasJoined takes no auth token,"
|
||||
log "so an internet-facing one is a free auth relay burning your Mojang egress IP."
|
||||
elif [ -n "$FELIS_NANO_PROXY_CIDR" ]; then
|
||||
log "Bound to ${FELIS_NANO_LISTEN}. firewalld, where it runs, admits ${port}/tcp only from"
|
||||
log "${FELIS_NANO_PROXY_CIDR}; any other firewall in front of this host must do the same."
|
||||
else
|
||||
log "WARNING: bound to ${FELIS_NANO_LISTEN} — hasJoined takes no auth token, so restrict"
|
||||
log "${port}/tcp to your proxy's source IP or anyone can relay their logins through you."
|
||||
log "WARNING: bound to ${FELIS_NANO_LISTEN} with no FELIS_NANO_PROXY_CIDR. hasJoined takes no auth"
|
||||
log "token, so admit ${port}/tcp from your proxy alone, or anyone can relay their logins"
|
||||
log "through you. firewalld, where it runs, keeps the port closed until you add:"
|
||||
log " firewall-cmd --permanent --add-rich-rule='rule family=\"ipv4\" source address=\"<proxy-ip>/32\" port port=\"${port}\" protocol=\"tcp\" accept' && firewall-cmd --reload"
|
||||
fi
|
||||
log "Then edit ${STATE_DIR}/felis.toml to add your [[auth_source]] roots and run:"
|
||||
log " sudo systemctl restart felis-nano"
|
||||
|
||||
@@ -313,8 +313,8 @@ kblock="$(awk '/^nano_listen_is_loopback\(\) \{/,/^}/' "$BS")"
|
||||
[ "$(printf '%s\n' "$kblock" | wc -l)" -lt 10 ] \
|
||||
|| { echo "FAIL: the extracted block is not the function -- did its closing brace move?"; exit 1; }
|
||||
|
||||
run_summary() { # listen
|
||||
FELIS_NANO_LISTEN="$1" NODE_IP=203.0.113.9 STATE_DIR=/etc/felis bash -c '
|
||||
run_summary() { # listen [proxy-cidr]
|
||||
FELIS_NANO_LISTEN="$1" FELIS_NANO_PROXY_CIDR="${2:-}" NODE_IP=203.0.113.9 STATE_DIR=/etc/felis bash -c '
|
||||
ok() { printf "OK: %s\n" "$*"; }
|
||||
log() { printf "LOG: %s\n" "$*"; }
|
||||
systemctl() { :; }
|
||||
@@ -352,6 +352,80 @@ ndefault="$(run_listen '' "$sdir/absent.service")"
|
||||
ndefault="${ndefault#LISTEN: }"
|
||||
expect "the default listen address (${ndefault:-empty}) is loopback" LOOPBACK "$(run_loopback "$ndefault")"
|
||||
|
||||
# --- firewalld admits the proxy alone ---------------------------------------------------
|
||||
# hasJoined takes no token, so a routable bind is opened only to FELIS_NANO_PROXY_CIDR, never
|
||||
# to every source, and a re-run closes the port an earlier installer opened to everyone.
|
||||
|
||||
cblock="$(awk '/^validate_cidr\(\) \{/,/^}/' "$BS")"
|
||||
[ -n "$cblock" ] || { echo "FAIL: no validate_cidr found in $BS"; exit 1; }
|
||||
[ "$(printf '%s\n' "$cblock" | wc -l)" -lt 15 ] \
|
||||
|| { echo "FAIL: the extracted block is not the function -- did its closing brace move?"; exit 1; }
|
||||
|
||||
check_cidr() { # value
|
||||
bash -c 'die() { printf "DIE: %s\n" "$*"; exit 1; }
|
||||
'"$cblock"'
|
||||
validate_cidr FELIS_NANO_PROXY_CIDR "$1" && echo VALID' _ "$1" 2>&1
|
||||
}
|
||||
|
||||
for v in 10.0.0.7 10.0.0.7/ /32 10.0.0.0/8/9 10.0.0.7/x '10.0.0.7/32 port' '10.0.0.7/32"'; do
|
||||
expect "proxy CIDR <$v> is refused" "DIE: FELIS_NANO_PROXY_CIDR" "$(check_cidr "$v")"
|
||||
done
|
||||
for v in '' 10.0.0.7/32 192.168.0.0/24 fd00::7/128; do
|
||||
expect "proxy CIDR <$v> is accepted" VALID "$(check_cidr "$v")"
|
||||
done
|
||||
|
||||
fblock="$(awk '/^configure_nano_firewall\(\) \{/,/^}/' "$BS")"
|
||||
[ -n "$fblock" ] || { echo "FAIL: no configure_nano_firewall found in $BS"; exit 1; }
|
||||
[ "$(printf '%s\n' "$fblock" | wc -l)" -lt 40 ] \
|
||||
|| { echo "FAIL: the extracted block is not the function -- did its closing brace move?"; exit 1; }
|
||||
|
||||
run_fw() { # listen proxy-cidr port-already-open(0|1)
|
||||
FELIS_NANO_LISTEN="$1" FELIS_NANO_PROXY_CIDR="$2" OPEN="$3" bash -c '
|
||||
ok() { printf "OK: %s\n" "$*"; }
|
||||
log() { printf "LOG: %s\n" "$*"; }
|
||||
warn() { printf "WARN: %s\n" "$*"; }
|
||||
systemctl() { return 0; }
|
||||
firewall-cmd() {
|
||||
case "$*" in *--query-port=*) [ "$OPEN" = 1 ]; return ;; esac
|
||||
printf "FW: %s\n" "$*"
|
||||
}
|
||||
'"$kblock"'
|
||||
'"$fblock"'
|
||||
configure_nano_firewall' 2>&1
|
||||
}
|
||||
|
||||
no_blanket_port() { # label output
|
||||
case "$2" in
|
||||
*--add-port*) echo "FAIL $1: the port was opened to every source:"; echo "$2"; fails=$((fails + 1)) ;;
|
||||
*) echo "PASS $1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
out="$(run_fw 0.0.0.0:8081 10.0.0.7/32 0)"
|
||||
expect "a proxy CIDR opens the port to that source alone" \
|
||||
'FW: --permanent --add-rich-rule=rule family="ipv4" source address="10.0.0.7/32" port port="8081" protocol="tcp" accept' "$out"
|
||||
no_blanket_port "a proxy CIDR never opens the port to every source" "$out"
|
||||
expect "an IPv6 proxy CIDR gets an ipv6 rule" 'rule family="ipv6" source address="fd00::7/128"' \
|
||||
"$(run_fw '[::]:8081' fd00::7/128 0)"
|
||||
out="$(run_fw 0.0.0.0:8081 '' 0)"
|
||||
expect "no proxy CIDR says the port stays closed" "WARN: no FELIS_NANO_PROXY_CIDR" "$out"
|
||||
no_blanket_port "no proxy CIDR opens nothing" "$out"
|
||||
case "$out" in
|
||||
*--add-rich-rule*) echo "FAIL no proxy CIDR must add no rule:"; echo "$out"; fails=$((fails + 1)) ;;
|
||||
*) echo "PASS no proxy CIDR adds no rule" ;;
|
||||
esac
|
||||
expect "a re-run closes the port an earlier install opened to everyone" "FW: --permanent --remove-port=8081/tcp" \
|
||||
"$(run_fw 0.0.0.0:8081 10.0.0.7/32 1)"
|
||||
case "$(run_fw 127.0.0.1:8081 10.0.0.7/32 1)" in
|
||||
*FW:*) echo "FAIL a loopback bind must leave firewalld alone"; fails=$((fails + 1)) ;;
|
||||
*) echo "PASS a loopback bind leaves firewalld alone" ;;
|
||||
esac
|
||||
|
||||
expect "a routable bind with no proxy CIDR is warned about" "WARNING: bound to 10.0.0.5:8081 with no FELIS_NANO_PROXY_CIDR" \
|
||||
"$(run_summary 10.0.0.5:8081)"
|
||||
expect "a routable bind with a proxy CIDR names it" "admits 8081/tcp only from" \
|
||||
"$(run_summary 10.0.0.5:8081 10.0.0.7/32)"
|
||||
|
||||
pblock="$(awk '/^prompt_install_mode\(\) \{/,/^}/' "$BS")"
|
||||
[ -n "$pblock" ] || { echo "FAIL: no prompt_install_mode found in $BS"; exit 1; }
|
||||
[ "$(printf '%s\n' "$pblock" | wc -l)" -lt 60 ] \
|
||||
|
||||
Reference in new issue
Block a user