network: allocate ports the host isn't already listening on
portFindNextAvailablePort consulted LibrePortal's own network_resources table plus a hardcoded list (8080, 6060) and CFG_RESERVED_PORTS_EXTRA — while the comment above it claimed a picked port "can never collide with a host service at compose-up time". It can: the list only covers what someone thought to write down. Ask the kernel instead, via ss, read once per allocation rather than per candidate. No ss => empty set => exactly the old behaviour. Found while restoring 13 apps onto a desktop, though not the cause there: stoat's livekit publishes a FIXED udp range (50000-50100, which it advertises to clients and so cannot be re-rolled), and kdeconnectd held 50016. That collision needs its own answer; this fixes the randomly-allocated ports, which had the same exposure with no reason to. Also make the bulk restore stop reporting a half-running app as a clean success. continue-on-error lets a failed compose-up log and carry on, so restoreAppStart returns 0 either way — which is how that run printed "13 apps restored" while four of stoat's containers had exited 101. checkSuccess already appends every failure to error_report.log, so watch it grow across each app and name the ones that were noisy. scripts/dev/lp-port-host-test binds a real socket and asserts the allocator refuses that port; verified it fails when the check is removed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5c8876428b
commit
089ac8827c
71
scripts/dev/lp-port-host-test
Executable file
71
scripts/dev/lp-port-host-test
Executable file
@ -0,0 +1,71 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Does port allocation avoid a port the HOST is already listening on?
|
||||||
|
#
|
||||||
|
# scripts/dev/lp-port-host-test
|
||||||
|
#
|
||||||
|
# It used to avoid only a hardcoded list (8080, 6060) plus whatever an admin had
|
||||||
|
# written into CFG_RESERVED_PORTS_EXTRA, while the comment above it claimed a
|
||||||
|
# picked port "can never collide with a host service at compose-up time". On a
|
||||||
|
# desktop, a first-run restore handed stoat's livekit a UDP port kdeconnectd
|
||||||
|
# already held; compose failed with "address already in use", four dependent
|
||||||
|
# containers exited 101, and the app came back half-running under a restore that
|
||||||
|
# reported success.
|
||||||
|
#
|
||||||
|
# The test binds a real socket and asserts the allocator refuses that port.
|
||||||
|
|
||||||
|
REPO="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||||
|
fail=0
|
||||||
|
chk(){ if [[ "$2" == "$3" ]]; then echo " ok $1"; else echo " FAIL $1: got '$2' want '$3'"; fail=1; fi; }
|
||||||
|
|
||||||
|
command -v ss >/dev/null 2>&1 || { echo " SKIP ss not available"; exit 0; }
|
||||||
|
|
||||||
|
# Hold a real port for the duration, so this tests the kernel's answer and not a
|
||||||
|
# fixture's idea of one.
|
||||||
|
python3 -c '
|
||||||
|
import socket, sys, time
|
||||||
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
|
s.bind(("127.0.0.1", 0)); s.listen(1)
|
||||||
|
print(s.getsockname()[1], flush=True)
|
||||||
|
time.sleep(30)
|
||||||
|
' > /tmp/lp-port-host-test.$$ &
|
||||||
|
holder=$!
|
||||||
|
trap 'kill $holder 2>/dev/null; rm -f /tmp/lp-port-host-test.$$' EXIT
|
||||||
|
for _ in $(seq 1 50); do [[ -s /tmp/lp-port-host-test.$$ ]] && break; sleep 0.1; done
|
||||||
|
BOUND=$(cat /tmp/lp-port-host-test.$$)
|
||||||
|
[[ -n "$BOUND" ]] || { echo " FAIL could not bind a test port"; exit 1; }
|
||||||
|
echo "--- holding tcp/$BOUND ---"
|
||||||
|
|
||||||
|
source "$REPO/scripts/network/ports/core/port_find_next_available.sh"
|
||||||
|
|
||||||
|
echo "--- the kernel's view includes it ---"
|
||||||
|
chk "in host set" "$(portHostBoundPorts | grep -cx "$BOUND")" "1"
|
||||||
|
|
||||||
|
echo "--- allocation refuses it ---"
|
||||||
|
# A one-port range containing only the bound port: with no other candidate, a
|
||||||
|
# correct allocator returns nothing rather than handing back a port in use.
|
||||||
|
isNotice(){ :; }; isError(){ :; }
|
||||||
|
runInstallOp(){ :; } # empty => the port table claims nothing
|
||||||
|
docker_dir="/tmp"; db_file="lp-port-host-test.db.$$"; : > "$docker_dir/$db_file"
|
||||||
|
CFG_PORT_RANGE="$BOUND-$BOUND"
|
||||||
|
CFG_RESERVED_PORTS_EXTRA=""
|
||||||
|
allocated_port="sentinel"
|
||||||
|
portFindNextAvailablePort
|
||||||
|
rm -f "$docker_dir/$db_file"
|
||||||
|
chk "refused the bound port" "$allocated_port" ""
|
||||||
|
|
||||||
|
echo "--- a free range still allocates ---"
|
||||||
|
CFG_PORT_RANGE="49000-49010"
|
||||||
|
: > "$docker_dir/$db_file"
|
||||||
|
allocated_port=""
|
||||||
|
portFindNextAvailablePort
|
||||||
|
rm -f "$docker_dir/$db_file"
|
||||||
|
if [[ "$allocated_port" =~ ^[0-9]+$ ]] && (( allocated_port >= 49000 && allocated_port <= 49010 )); then
|
||||||
|
echo " ok allocated $allocated_port"
|
||||||
|
else
|
||||||
|
echo " FAIL free range gave '$allocated_port'"; fail=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if (( fail )); then echo "FAILED"; exit 1; fi
|
||||||
|
echo "All port checks passed."
|
||||||
@ -4,8 +4,6 @@
|
|||||||
#
|
#
|
||||||
# Host-bound ports that aren't tracked in the docker port table — extend
|
# Host-bound ports that aren't tracked in the docker port table — extend
|
||||||
# CFG_RESERVED_PORTS_EXTRA in your config to add more (comma-separated).
|
# CFG_RESERVED_PORTS_EXTRA in your config to add more (comma-separated).
|
||||||
# These are skipped during allocation so a randomly-picked app port can
|
|
||||||
# never collide with a host service at compose-up time.
|
|
||||||
portIsReservedHostPort()
|
portIsReservedHostPort()
|
||||||
{
|
{
|
||||||
local p="$1"
|
local p="$1"
|
||||||
@ -18,12 +16,38 @@ portIsReservedHostPort()
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Every port the host is ACTUALLY listening on, one per line.
|
||||||
|
#
|
||||||
|
# The reserved list above only covers what someone thought to write down, and
|
||||||
|
# this used to claim that was enough — that a picked port "can never collide
|
||||||
|
# with a host service at compose-up time". It can. A first-run restore on a
|
||||||
|
# desktop handed stoat's livekit a UDP port that kdeconnectd already had, and
|
||||||
|
# compose failed with "address already in use"; four containers that depended
|
||||||
|
# on it exited 101 and the app came back half-running, while the restore
|
||||||
|
# reported success.
|
||||||
|
#
|
||||||
|
# So ask the kernel instead of a list. Read once per allocation rather than per
|
||||||
|
# candidate: the loop below runs up to fifty times and the set does not change
|
||||||
|
# meaningfully in between. If ss is missing the set is empty, which is exactly
|
||||||
|
# the old behaviour.
|
||||||
|
portHostBoundPorts()
|
||||||
|
{
|
||||||
|
command -v ss >/dev/null 2>&1 || return 0
|
||||||
|
ss -HlntuA tcp,udp 2>/dev/null \
|
||||||
|
| awk '{print $5}' | sed 's/.*://' | grep -E '^[0-9]+$' | sort -u
|
||||||
|
}
|
||||||
|
|
||||||
portFindNextAvailablePort()
|
portFindNextAvailablePort()
|
||||||
{
|
{
|
||||||
IFS='-' read -ra PORT_RANGE <<< "$CFG_PORT_RANGE"
|
IFS='-' read -ra PORT_RANGE <<< "$CFG_PORT_RANGE"
|
||||||
local port_start=${PORT_RANGE[0]}
|
local port_start=${PORT_RANGE[0]}
|
||||||
local port_end=${PORT_RANGE[1]}
|
local port_end=${PORT_RANGE[1]}
|
||||||
|
|
||||||
|
local -A host_bound=()
|
||||||
|
local _hp
|
||||||
|
while IFS= read -r _hp; do [[ -n "$_hp" ]] && host_bound[$_hp]=1; done \
|
||||||
|
< <(portHostBoundPorts)
|
||||||
|
|
||||||
if [[ ! -f "$docker_dir/$db_file" ]]; then
|
if [[ ! -f "$docker_dir/$db_file" ]]; then
|
||||||
isNotice "Database not found: $docker_dir/$db_file"
|
isNotice "Database not found: $docker_dir/$db_file"
|
||||||
allocated_port=""
|
allocated_port=""
|
||||||
@ -37,8 +61,9 @@ portFindNextAvailablePort()
|
|||||||
# Generate random port in range
|
# Generate random port in range
|
||||||
local random_port=$(( RANDOM % (port_end - port_start + 1) + port_start ))
|
local random_port=$(( RANDOM % (port_end - port_start + 1) + port_start ))
|
||||||
|
|
||||||
# Skip host-reserved ports (CrowdSec LAPI, etc.)
|
# Skip host-reserved ports (CrowdSec LAPI, etc.) and anything
|
||||||
if portIsReservedHostPort "$random_port"; then
|
# the host is already listening on.
|
||||||
|
if portIsReservedHostPort "$random_port" || [[ -n "${host_bound[$random_port]:-}" ]]; then
|
||||||
((attempts++))
|
((attempts++))
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
@ -56,7 +81,7 @@ portFindNextAvailablePort()
|
|||||||
if [[ -z "$available_port" ]]; then
|
if [[ -z "$available_port" ]]; then
|
||||||
isNotice "Random allocation failed, trying sequential..."
|
isNotice "Random allocation failed, trying sequential..."
|
||||||
for ((port=port_start; port<=port_end; port++)); do
|
for ((port=port_start; port<=port_end; port++)); do
|
||||||
if portIsReservedHostPort "$port"; then
|
if portIsReservedHostPort "$port" || [[ -n "${host_bound[$port]:-}" ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
local check_result=$(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT 1 FROM network_resources WHERE resource_value LIKE '$port:%' AND resource_type='port' AND status='active' LIMIT 1" 2>/dev/null)
|
local check_result=$(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT 1 FROM network_resources WHERE resource_value LIKE '$port:%' AND resource_type='port' AND status='active' LIMIT 1" 2>/dev/null)
|
||||||
|
|||||||
@ -58,23 +58,45 @@ restoreFirstRunBulk()
|
|||||||
# Count what actually landed. A per-app failure must not be reported as a
|
# Count what actually landed. A per-app failure must not be reported as a
|
||||||
# complete restore — that is how "4 apps restored" read as success when
|
# complete restore — that is how "4 apps restored" read as success when
|
||||||
# nine had gone missing.
|
# nine had gone missing.
|
||||||
|
#
|
||||||
|
# An app can also come back only half-running: continue-on-error (the
|
||||||
|
# default) lets a failed compose-up log and carry on, so restoreAppStart
|
||||||
|
# still returns 0. That is how a restore reported thirteen successes while
|
||||||
|
# stoat's livekit had lost a port race and four containers that depended on
|
||||||
|
# it exited 101. checkSuccess appends every such failure to error_report.log,
|
||||||
|
# so watch that file grow across each app and name the noisy ones.
|
||||||
|
local _errlog="${logs_dir%/}/error_report.log"
|
||||||
|
_restoreErrLines() { wc -l < "$_errlog" 2>/dev/null || echo 0; }
|
||||||
|
|
||||||
local app
|
local app
|
||||||
local -i ok=0 bad=0
|
local -i ok=0 bad=0 before=0 after=0
|
||||||
local -a failed=()
|
local -a failed=() noisy=()
|
||||||
for app in "${apps_to_restore[@]}"; do
|
for app in "${apps_to_restore[@]}"; do
|
||||||
|
before=$(_restoreErrLines)
|
||||||
if restoreAppStart "$app" "latest" "$idx" "$source_host"; then
|
if restoreAppStart "$app" "latest" "$idx" "$source_host"; then
|
||||||
ok=$(( ok + 1 ))
|
ok=$(( ok + 1 ))
|
||||||
|
after=$(_restoreErrLines)
|
||||||
|
(( after > before )) && noisy+=("$app")
|
||||||
else
|
else
|
||||||
bad=$(( bad + 1 )); failed+=("$app")
|
bad=$(( bad + 1 )); failed+=("$app")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
unset -f _restoreErrLines
|
||||||
|
|
||||||
if (( bad > 0 )); then
|
if (( bad > 0 )); then
|
||||||
isError "First-run restore finished with failures — $ok of ${#apps_to_restore[@]} restored"
|
isError "First-run restore finished with failures — $ok of ${#apps_to_restore[@]} restored"
|
||||||
isNotice "Failed: ${failed[*]}"
|
isNotice "Failed: ${failed[*]}"
|
||||||
|
(( ${#noisy[@]} )) && isNotice "Restored but reported errors: ${noisy[*]}"
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if (( ${#noisy[@]} )); then
|
||||||
|
isSuccessful "First-run restore complete — $ok apps restored"
|
||||||
|
isNotice "${#noisy[@]} reported errors while starting: ${noisy[*]}"
|
||||||
|
isNotice "They are restored, but check them: $_errlog"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
isSuccessful "First-run restore complete — $ok apps restored"
|
isSuccessful "First-run restore complete — $ok apps restored"
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
@ -825,6 +825,7 @@ declare -gA LP_FN_MAP=(
|
|||||||
[portGetPublicPorts]="network/ports/core/port_get_public_ports.sh"
|
[portGetPublicPorts]="network/ports/core/port_get_public_ports.sh"
|
||||||
[portGetServicePorts]="network/ports/core/port_get_service_ports.sh"
|
[portGetServicePorts]="network/ports/core/port_get_service_ports.sh"
|
||||||
[portGetServicePortsOnly]="network/ports/core/port_get_service_ports_only.sh"
|
[portGetServicePortsOnly]="network/ports/core/port_get_service_ports_only.sh"
|
||||||
|
[portHostBoundPorts]="network/ports/core/port_find_next_available.sh"
|
||||||
[portIsReservedHostPort]="network/ports/core/port_find_next_available.sh"
|
[portIsReservedHostPort]="network/ports/core/port_find_next_available.sh"
|
||||||
[portLookupExisting]="network/ports/allocation/port_allocate.sh"
|
[portLookupExisting]="network/ports/allocation/port_allocate.sh"
|
||||||
[port_show_all_network_services]="network/display/show_all_network_services.sh"
|
[port_show_all_network_services]="network/display/show_all_network_services.sh"
|
||||||
@ -2063,6 +2064,7 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[portGetPublicPorts]="scripts"
|
[portGetPublicPorts]="scripts"
|
||||||
[portGetServicePorts]="scripts"
|
[portGetServicePorts]="scripts"
|
||||||
[portGetServicePortsOnly]="scripts"
|
[portGetServicePortsOnly]="scripts"
|
||||||
|
[portHostBoundPorts]="scripts"
|
||||||
[portIsReservedHostPort]="scripts"
|
[portIsReservedHostPort]="scripts"
|
||||||
[portLookupExisting]="scripts"
|
[portLookupExisting]="scripts"
|
||||||
[port_show_all_network_services]="scripts"
|
[port_show_all_network_services]="scripts"
|
||||||
@ -3339,6 +3341,7 @@ portFindNextAvailablePort() { unset -f portFindNextAvailablePort; __lpAutoload "
|
|||||||
portGetPublicPorts() { unset -f portGetPublicPorts; __lpAutoload "${install_scripts_dir}network/ports/core/port_get_public_ports.sh"; portGetPublicPorts "$@"; }
|
portGetPublicPorts() { unset -f portGetPublicPorts; __lpAutoload "${install_scripts_dir}network/ports/core/port_get_public_ports.sh"; portGetPublicPorts "$@"; }
|
||||||
portGetServicePorts() { unset -f portGetServicePorts; __lpAutoload "${install_scripts_dir}network/ports/core/port_get_service_ports.sh"; portGetServicePorts "$@"; }
|
portGetServicePorts() { unset -f portGetServicePorts; __lpAutoload "${install_scripts_dir}network/ports/core/port_get_service_ports.sh"; portGetServicePorts "$@"; }
|
||||||
portGetServicePortsOnly() { unset -f portGetServicePortsOnly; __lpAutoload "${install_scripts_dir}network/ports/core/port_get_service_ports_only.sh"; portGetServicePortsOnly "$@"; }
|
portGetServicePortsOnly() { unset -f portGetServicePortsOnly; __lpAutoload "${install_scripts_dir}network/ports/core/port_get_service_ports_only.sh"; portGetServicePortsOnly "$@"; }
|
||||||
|
portHostBoundPorts() { unset -f portHostBoundPorts; __lpAutoload "${install_scripts_dir}network/ports/core/port_find_next_available.sh"; portHostBoundPorts "$@"; }
|
||||||
portIsReservedHostPort() { unset -f portIsReservedHostPort; __lpAutoload "${install_scripts_dir}network/ports/core/port_find_next_available.sh"; portIsReservedHostPort "$@"; }
|
portIsReservedHostPort() { unset -f portIsReservedHostPort; __lpAutoload "${install_scripts_dir}network/ports/core/port_find_next_available.sh"; portIsReservedHostPort "$@"; }
|
||||||
portLookupExisting() { unset -f portLookupExisting; __lpAutoload "${install_scripts_dir}network/ports/allocation/port_allocate.sh"; portLookupExisting "$@"; }
|
portLookupExisting() { unset -f portLookupExisting; __lpAutoload "${install_scripts_dir}network/ports/allocation/port_allocate.sh"; portLookupExisting "$@"; }
|
||||||
port_show_all_network_services() { unset -f port_show_all_network_services; __lpAutoload "${install_scripts_dir}network/display/show_all_network_services.sh"; port_show_all_network_services "$@"; }
|
port_show_all_network_services() { unset -f port_show_all_network_services; __lpAutoload "${install_scripts_dir}network/display/show_all_network_services.sh"; port_show_all_network_services "$@"; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user