Auto-scan for a free web admin port instead of hardcoding one

The 8080->8081 fix from the last commit just moved the collision
risk, not removed it — any hardcoded port can eventually collide with
something else on a box running several services. Both services now
scan for the first genuinely free port starting at 8081 (ss -tlnH
"sport = :$PORT", capped at 100 ports checked) and use whatever they
find — .env, UFW, the DO Cloud Firewall rule, and the Caddy proxy
target all follow the actual chosen port, not a fixed number.

asterisk-do.sh's self-contained Caddy block (unquoted heredoc) reads
the port live. asterisk.sh's README heredoc is quoted (no expansion),
so its generated docs keep the static "8081" default with an added
note to check .env for the real value if it differed — the summary
echo outside that heredoc still reports the live value correctly.

Verified: normal case still lands on 8081; with 8081 deliberately
occupied by another process, both services correctly detect the
collision and fall through to 8082 instead, confirmed via the actual
generated .env in each case.
This commit is contained in:
Claude
2026-07-19 17:20:56 +00:00
parent 2483365141
commit 4af9cfeae3
2 changed files with 69 additions and 18 deletions
+35 -11
View File
@@ -210,7 +210,8 @@ install_asterisk-do() {
echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env"
echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk"
echo "[DRY-RUN] Would detect droplet public IP via DO metadata service"
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8081, 8088, 8089, 3478, 10000-20000, 49152-49252"
echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)"
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, <web admin port>, 8088, 8089, 3478, 10000-20000, 49152-49252"
echo "[DRY-RUN] Would open 51820/udp (not 51821) if wg-easy was selected"
echo "[DRY-RUN] Would offer to create a DigitalOcean Cloud Firewall via doctl"
echo "[DRY-RUN] Would reverse-proxy the web admin on the SAME FQDN used for SIP (needed for cert sync)"
@@ -487,6 +488,25 @@ EOF
sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml
fi
# ── Pick a free port for the web admin ─────────────────────────────────────
# Hardcoding a single number gets fragile fast once several services share
# a host — CrowdSec's own LAPI already collides with 8080 by default (its
# own upstream default, confirmed against its real config.yaml). Scan
# instead: start at 8081 and take the first port nothing is listening on,
# capped so a pathological box can't spin this forever.
local WEB_ADMIN_PORT_VAL=8081
local _port_scan_limit=$((WEB_ADMIN_PORT_VAL + 100))
while ss -tlnH "sport = :${WEB_ADMIN_PORT_VAL}" 2>/dev/null | grep -q . \
&& [[ "$WEB_ADMIN_PORT_VAL" -lt "$_port_scan_limit" ]]; do
WEB_ADMIN_PORT_VAL=$((WEB_ADMIN_PORT_VAL + 1))
done
if [[ "$WEB_ADMIN_PORT_VAL" -ge "$_port_scan_limit" ]]; then
log_warning "No free port found in 8081-${_port_scan_limit} — falling back to 8081 anyway."
WEB_ADMIN_PORT_VAL=8081
elif [[ "$WEB_ADMIN_PORT_VAL" != 8081 ]]; then
log_info "Port 8081 was already taken — web admin will use ${WEB_ADMIN_PORT_VAL} instead."
fi
# ── .env ──────────────────────────────────────────────────────────────────
cat > .env << ENV
# ── Domain ────────────────────────────────────────────────────
@@ -512,7 +532,11 @@ HAS_VLANS=n
VLAN_SUBNETS=
# ── Web admin ─────────────────────────────────────────────────
WEB_ADMIN_PORT=8081
# Picked automatically at install time (first free port starting at 8081) —
# see WEB_ADMIN_PORT_VAL in services/asterisk-do.sh if this ever needs to
# change again; don't hand-edit without also updating Caddy's Caddyfile and
# both firewall layers to match.
WEB_ADMIN_PORT=${WEB_ADMIN_PORT_VAL}
WEB_ADMIN_AUTH_DISABLED=false
ENV
chmod 600 .env
@@ -523,7 +547,7 @@ ENV
ufw allow 5060/udp
ufw allow 5060/tcp
ufw allow 5061/tcp
ufw allow 8081/tcp
ufw allow "${WEB_ADMIN_PORT_VAL}/tcp"
ufw allow 8088/tcp
ufw allow 8089/tcp
ufw allow 3478/udp
@@ -555,7 +579,7 @@ ENV
"protocol:tcp,ports:5060,address:0.0.0.0/0,address:::/0"
"protocol:udp,ports:5060,address:0.0.0.0/0,address:::/0"
"protocol:tcp,ports:5061,address:0.0.0.0/0,address:::/0"
"protocol:tcp,ports:8081,address:0.0.0.0/0,address:::/0"
"protocol:tcp,ports:${WEB_ADMIN_PORT_VAL},address:0.0.0.0/0,address:::/0"
"protocol:tcp,ports:8088-8089,address:0.0.0.0/0,address:::/0"
"protocol:tcp,ports:3478,address:0.0.0.0/0,address:::/0"
"protocol:udp,ports:3478,address:0.0.0.0/0,address:::/0"
@@ -604,9 +628,9 @@ ENV
# matching $DOMAIN_NAME, and SIP TLS would silently stay self-signed. So
# there's no separate domain prompt: this always targets $DOMAIN_NAME.
if [[ -z "$DOMAIN_NAME" ]]; then
log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:8081 (nothing for Caddy to do)."
log_info "No FQDN set — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL} (nothing for Caddy to do)."
elif [[ ! -d "$DOCKER_DIR/caddy" ]] && [[ -z "${CADDY_REMOTE_HOST:-}" ]]; then
log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:8081, SIP TLS stays self-signed."
log_info "Caddy not installed — web admin stays on http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL}, SIP TLS stays self-signed."
else
local EXTRA_BLOCK=""
if [ -d "$DOCKER_DIR/authelia" ]; then
@@ -665,7 +689,7 @@ ENV
# Asterisk Web Admin
${DOMAIN_NAME} {
reverse_proxy localhost:8081
reverse_proxy localhost:${WEB_ADMIN_PORT_VAL}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
@@ -847,7 +871,7 @@ plan for the admin panel.
| 22 | TCP | SSH (keep this open or you're locked out) |
| 5060 | UDP/TCP | SIP signalling (unencrypted) |
| 5061 | TCP | SIP over TLS |
| 8081 | TCP | Easy Asterisk web admin |
| ${WEB_ADMIN_PORT_VAL} | TCP | Easy Asterisk web admin (auto-picked — see \`.env\`) |
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
| 3478 | UDP/TCP | TURN/STUN (coturn) |
| 1000020000 | UDP | RTP media streams |
@@ -881,7 +905,7 @@ be added later by running \`sudo ./setup.sh <name>\` from the repo.
the public firewall; the web UI (51821) is deliberately **not** exposed —
reach it via SSH tunnel: \`ssh -L 51821:localhost:51821 user@<droplet-ip>\`,
then browse \`http://localhost:51821\`. A natural next step once it's
installed: restrict the web admin (8081) to the VPN subnet only, on both
installed: restrict the web admin (${WEB_ADMIN_PORT_VAL}) to the VPN subnet only, on both
firewall layers, so reconfiguring the PBX requires being on the VPN —
done manually, not automatically, since a firewall mistake there can lock
you out.
@@ -965,7 +989,7 @@ accept it manually).
## Web admin
Access the Easy Asterisk web interface at http://<droplet-ip>:8081
Access the Easy Asterisk web interface at http://<droplet-ip>:${WEB_ADMIN_PORT_VAL}
or via your configured reverse-proxy domain.
## Data directories (all inside ~/docker/asterisk-do/, included in backup)
@@ -1001,7 +1025,7 @@ MD
fi
echo " Public IP: ${PUBLIC_IP:-unknown}"
echo " SIP port: 5061 (TLS) / 5060 (UDP)"
echo " Web admin: http://${PUBLIC_IP:-localhost}:8081"
echo " Web admin: http://${PUBLIC_IP:-localhost}:${WEB_ADMIN_PORT_VAL}"
echo " Manage: docker compose -f $EA_DIR/docker-compose.yml <up|down|logs>"
echo " Script: docker exec -it easy-asterisk-do easy-asterisk --help"
if [[ -n "$DOMAIN_NAME" ]] && [[ -d "$DOCKER_DIR/caddy" ]]; then
+34 -7
View File
@@ -204,7 +204,8 @@ install_asterisk() {
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $EA_DIR with Dockerfile, docker-compose.yml, .env"
echo "[DRY-RUN] Would copy/download vendor files from easy-asterisk"
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, 8081, 8088, 8089, 3478, 10000-20000, 49152-49252"
echo "[DRY-RUN] Would scan for a free web admin port starting at 8081 (avoids e.g. CrowdSec's 8080)"
echo "[DRY-RUN] Would open UFW ports: 5060, 5061, <web admin port>, 8088, 8089, 3478, 10000-20000, 49152-49252"
return 0
fi
@@ -359,6 +360,25 @@ EOF
sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml
fi
# ── Pick a free port for the web admin ─────────────────────────────────────
# Hardcoding a single number gets fragile fast once several services share
# a host — CrowdSec's own LAPI already collides with 8080 by default (its
# own upstream default, confirmed against its real config.yaml). Scan
# instead: start at 8081 and take the first port nothing is listening on,
# capped so a pathological box can't spin this forever.
local WEB_ADMIN_PORT_VAL=8081
local _port_scan_limit=$((WEB_ADMIN_PORT_VAL + 100))
while ss -tlnH "sport = :${WEB_ADMIN_PORT_VAL}" 2>/dev/null | grep -q . \
&& [[ "$WEB_ADMIN_PORT_VAL" -lt "$_port_scan_limit" ]]; do
WEB_ADMIN_PORT_VAL=$((WEB_ADMIN_PORT_VAL + 1))
done
if [[ "$WEB_ADMIN_PORT_VAL" -ge "$_port_scan_limit" ]]; then
log_warning "No free port found in 8081-${_port_scan_limit} — falling back to 8081 anyway."
WEB_ADMIN_PORT_VAL=8081
elif [[ "$WEB_ADMIN_PORT_VAL" != 8081 ]]; then
log_info "Port 8081 was already taken — web admin will use ${WEB_ADMIN_PORT_VAL} instead."
fi
# ── .env ──────────────────────────────────────────────────────────────────
cat > .env << ENV
# ── Domain ────────────────────────────────────────────────────
@@ -383,7 +403,11 @@ HAS_VLANS=${HAS_VLANS_VAL}
VLAN_SUBNETS=${VLAN_SUBNETS_VAL}
# ── Web admin ─────────────────────────────────────────────────
WEB_ADMIN_PORT=8081
# Picked automatically at install time (first free port starting at 8081) —
# see WEB_ADMIN_PORT_VAL in services/asterisk.sh if this ever needs to
# change again; don't hand-edit without also updating Caddy's Caddyfile and
# any firewall rules to match.
WEB_ADMIN_PORT=${WEB_ADMIN_PORT_VAL}
WEB_ADMIN_AUTH_DISABLED=false
ENV
chmod 600 .env
@@ -394,7 +418,7 @@ ENV
ufw allow 5060/udp
ufw allow 5060/tcp
ufw allow 5061/tcp
ufw allow 8081/tcp
ufw allow "${WEB_ADMIN_PORT_VAL}/tcp"
ufw allow 8088/tcp
ufw allow 8089/tcp
ufw allow 3478/udp
@@ -415,7 +439,7 @@ ENV
sed -i "s/^WEB_ADMIN_AUTH_DISABLED=.*/WEB_ADMIN_AUTH_DISABLED=true/" .env
fi
fi
configure_caddy_for_service "Asterisk Web Admin" "8081" "asterisk" "$EXTRA_BLOCK"
configure_caddy_for_service "Asterisk Web Admin" "${WEB_ADMIN_PORT_VAL}" "asterisk" "$EXTRA_BLOCK"
# ── README ────────────────────────────────────────────────────────────────
write_readme "$EA_DIR" << 'MD'
@@ -484,7 +508,10 @@ to accept it).
## Web admin
Access the Easy Asterisk web interface at http://<host-ip>:8081
or via your configured reverse-proxy domain.
or via your configured reverse-proxy domain. (8081 is the default; if that
port was already taken by something else on this box, the installer picked
the next free one instead — check WEB_ADMIN_PORT in .env for the actual
value.)
## Data directories (all inside ~/docker/asterisk/, included in backup)
@@ -502,7 +529,7 @@ or via your configured reverse-proxy domain.
|---------------|----------|----------------------------------|
| 5060 | UDP/TCP | SIP signalling (unencrypted) |
| 5061 | TCP | SIP over TLS |
| 8081 | TCP | Easy Asterisk web admin |
| 8081 | TCP | Easy Asterisk web admin (default — see .env) |
| 8088/8089 | TCP | Asterisk HTTP/WS (ARI/AMI) |
| 3478 | UDP/TCP | TURN/STUN (coturn) |
| 1000020000 | UDP | RTP media streams |
@@ -530,7 +557,7 @@ MD
echo " TURN server: (none — LAN/VPN only)"
fi
echo " SIP port: 5061 (TLS) / 5060 (UDP)"
echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):8081"
echo " Web admin: http://$(hostname -I 2>/dev/null | awk '{print $1}' || echo localhost):${WEB_ADMIN_PORT_VAL}"
echo " Manage: docker compose -f $EA_DIR/docker-compose.yml <up|down|logs>"
echo " Script: docker exec -it easy-asterisk easy-asterisk --help"
echo ""