Compare commits

...
3 Commits
Author SHA1 Message Date
Outis c584bc45cd Merge pull request #403 from outis1one/claude/wolf-pair-port-conflict-7nz8qg
Claude/wolf pair port conflict 7nz8qg
2026-08-31 14:19:09 -04:00
Claude 591bdd0e79 Fix gitea.sh: open the SSH clone port in UFW
install_gitea() scanned WEB_PORT/SSH_PORT and published both in
docker-compose.yml but never opened either in UFW. With UFW active,
a `git clone ssh://...` against the SSH port silently drops instead
of getting connection-refused, which just hangs forever with no
error — the exact symptom reported.

The web port can be safely left off the public rule when Caddy fronts
it locally (scoped to caddy_net instead, matching every other service
here), but SSH can't be proxied through Caddy at all, so it always
gets a direct ufw allow now.
2026-08-31 18:01:43 +00:00
Claude 8a298d161a Fix wolf-pair: scan for a free port instead of hardcoding 8090
wolf-pair runs network_mode: host, so a taken 8090 fails at container
start with "address already in use" and no ports: line in
docker-compose.yml to explain why — wordpress, ntfy, and beszel all
default to 8090 too and correctly scan for a free port; wolf-pair
hardcoded it in three places (installer var, UFW rule, server.py's
bind) with no scan at all.

Now finds a free port via find_free_port, persists it in a new .env
(read back on rerun so a live install never silently moves), and
threads it into the container via WOLFPAIR_PORT so server.py binds
the scanned port instead of a literal 8090.
2026-08-31 17:44:22 +00:00
2 changed files with 73 additions and 3 deletions
+21
View File
@@ -529,6 +529,8 @@ install_gitea() {
if [ "$DRY_RUN" = true ]; then if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $DIR with docker-compose.yml (gitea/gitea:latest)" echo "[DRY-RUN] Would create $DIR with docker-compose.yml (gitea/gitea:latest)"
echo "[DRY-RUN] Would scan for free host ports (web + SSH) to avoid collisions" echo "[DRY-RUN] Would scan for free host ports (web + SSH) to avoid collisions"
echo "[DRY-RUN] Would open the SSH clone port in UFW (web port too, or scoped to caddy_net"
echo "[DRY-RUN] if Caddy ends up fronting it locally)"
echo "[DRY-RUN] Would prompt for a Gitea admin username/password, then create that account" echo "[DRY-RUN] Would prompt for a Gitea admin username/password, then create that account"
echo "[DRY-RUN] and an API token once the container is ready (no manual web wizard)" echo "[DRY-RUN] and an API token once the container is ready (no manual web wizard)"
echo "[DRY-RUN] Would prompt for a GitHub token and copy in gitea-github-sync.sh" echo "[DRY-RUN] Would prompt for a GitHub token and copy in gitea-github-sync.sh"
@@ -738,6 +740,25 @@ ENV
# replacement requiring Caddy involvement. ───────────────────────────── # replacement requiring Caddy involvement. ─────────────────────────────
configure_caddy_for_service "Gitea" "host.docker.internal:${WEB_PORT}" "git" configure_caddy_for_service "Gitea" "host.docker.internal:${WEB_PORT}" "git"
# ── Firewall ─────────────────────────────────────────────────────────────
# SSH clone (SSH_PORT->22) is a different protocol than the web UI — Caddy
# can't front it no matter what CADDY_SERVICE_MODE came back as, so it
# always needs its own direct rule or `git clone ssh://...` hangs forever
# (a dropped SYN with UFW active, not a fast connection-refused).
if command -v ufw &>/dev/null; then
if [[ "$CADDY_SERVICE_CONFIGURED" == true && "$CADDY_SERVICE_MODE" == "local" ]]; then
ufw delete allow "${WEB_PORT}/tcp" 2>/dev/null || true
ufw_allow_from_caddy_net "${WEB_PORT}"
else
ufw allow "${WEB_PORT}/tcp" comment "Gitea web UI" >/dev/null 2>&1 || true
fi
ufw allow "${SSH_PORT}/tcp" comment "Gitea SSH clone" >/dev/null 2>&1 || true
ensure_ufw_enabled
log_success "UFW: opened SSH clone port ${SSH_PORT}/tcp"
else
log_warning "ufw not installed — if you use a firewall, open TCP ${SSH_PORT} for SSH clones."
fi
_gitea_offer_authelia_sso "$DIR" _gitea_offer_authelia_sso "$DIR"
_gitea_offer_reverse_proxy_auth "$DIR" _gitea_offer_reverse_proxy_auth "$DIR"
_gitea_offer_actions_runner "$DIR" _gitea_offer_actions_runner "$DIR"
+52 -3
View File
@@ -174,6 +174,19 @@ CBLOCK
[ -f "$_file" ] || return 0 [ -f "$_file" ] || return 0
cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null cp -p "$_file" "${_file}.bak.$(date +%Y%m%d-%H%M%S)" 2>/dev/null
} }
port_in_use() {
local _port="$1" _proto="${2:-tcp}"
local _flag="-tlnH"
[ "$_proto" = "udp" ] && _flag="-ulnH"
ss "$_flag" "sport = :${_port}" 2>/dev/null | grep -q .
}
find_free_port() {
local _varname="$1" _port="$2" _proto="${3:-tcp}"
while port_in_use "$_port" "$_proto"; do
_port=$((_port + 1))
done
eval "$_varname='$_port'"
}
fi fi
# Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR # Globals — ACTUAL_USER/ACTUAL_HOME must come before DOCKER_DIR
@@ -206,11 +219,29 @@ install_wolf-pair() {
echo " - Build the wolf-pair image (python:3.12-alpine + docker-cli)" echo " - Build the wolf-pair image (python:3.12-alpine + docker-cli)"
echo " - Run the container with network_mode: host (for localhost:47989 access)" echo " - Run the container with network_mode: host (for localhost:47989 access)"
echo " - Mount /var/run/docker.sock:ro (for docker logs wolf)" echo " - Mount /var/run/docker.sock:ro (for docker logs wolf)"
echo " - Open port $WOLFPAIR_PORT in UFW" echo " - Open port $WOLFPAIR_PORT in UFW (auto-scanned for a free host port —"
echo " other services, e.g. wordpress/ntfy/beszel, default to 8090 too)"
echo " - Optionally configure a Caddy reverse proxy" echo " - Optionally configure a Caddy reverse proxy"
return 0 return 0
fi fi
# network_mode: host means there's no HOST:CONTAINER ports: mapping to scan
# around a collision on — server.py binds 0.0.0.0 directly on the host, so a
# taken 8090 (wordpress/ntfy/beszel all default here too) fails at container
# start with "address already in use" and nothing in docker-compose.yml to
# point at. Scan once and persist in .env; on a rerun, keep the port already
# in use rather than silently moving it out from under an existing Caddy
# site block / bookmarked URL.
if [ -f "$WOLFPAIR_DIR/.env" ]; then
local _existing_port
_existing_port="$(grep '^WOLFPAIR_PORT=' "$WOLFPAIR_DIR/.env" 2>/dev/null | cut -d= -f2-)"
[ -n "$_existing_port" ] && WOLFPAIR_PORT="$_existing_port"
else
find_free_port WOLFPAIR_PORT "$WOLFPAIR_PORT"
fi
[ "$WOLFPAIR_PORT" != "8090" ] && \
log_info "Port 8090 already in use — wolf-pair will use $WOLFPAIR_PORT instead."
mkdir -p "$WOLFPAIR_DIR" mkdir -p "$WOLFPAIR_DIR"
ensure_docker_dir_ownership "$WOLFPAIR_DIR" ensure_docker_dir_ownership "$WOLFPAIR_DIR"
cd "$WOLFPAIR_DIR" || return 1 cd "$WOLFPAIR_DIR" || return 1
@@ -234,10 +265,11 @@ submitted — otherwise the user resubmits a dead secret and Wolf returns
"key not found". We track submitted secrets and fall back to the waiting page "key not found". We track submitted secrets and fall back to the waiting page
until Moonlight initiates a brand-new pairing (which mints a new secret). until Moonlight initiates a brand-new pairing (which mints a new secret).
""" """
import json, subprocess, re, urllib.request, urllib.error import json, os, subprocess, re, urllib.request, urllib.error
from http.server import HTTPServer, BaseHTTPRequestHandler from http.server import HTTPServer, BaseHTTPRequestHandler
WOLF_HTTP = "http://localhost:47989" WOLF_HTTP = "http://localhost:47989"
LISTEN_PORT = int(os.environ.get("WOLFPAIR_PORT", "8090"))
# Secrets already submitted to Wolf. Wolf erases a secret on first submit, so a # Secrets already submitted to Wolf. Wolf erases a secret on first submit, so a
# secret in here is dead — show the waiting page instead of re-offering it. # secret in here is dead — show the waiting page instead of re-offering it.
@@ -390,7 +422,7 @@ class Handler(BaseHTTPRequestHandler):
if __name__ == '__main__': if __name__ == '__main__':
HTTPServer.allow_reuse_address = True HTTPServer.allow_reuse_address = True
HTTPServer(('0.0.0.0', 8090), Handler).serve_forever() HTTPServer(('0.0.0.0', LISTEN_PORT), Handler).serve_forever()
PYEOF PYEOF
log_success "server.py written" log_success "server.py written"
@@ -420,12 +452,27 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: wolf-pair container_name: wolf-pair
network_mode: host network_mode: host
environment:
- WOLFPAIR_PORT=${WOLFPAIR_PORT:-8090}
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro - /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped restart: unless-stopped
COMPOSE COMPOSE
log_success "docker-compose.yml written" log_success "docker-compose.yml written"
# host networking means server.py binds this port directly — .env feeds it
# to the container's WOLFPAIR_PORT (above) via docker compose's own .env
# auto-load, same pattern as WOLF_STATE_DIR in services/wolf.sh.
backup_if_exists "$WOLFPAIR_DIR/.env"
cat > "$WOLFPAIR_DIR/.env" << EOF
# Port wolf-pair's pairing UI listens on (host networking — no port mapping
# to edit). Auto-scanned at install time to avoid clashing with other
# services that also default to 8090 (wordpress, ntfy, beszel).
WOLFPAIR_PORT=${WOLFPAIR_PORT}
EOF
chmod 600 "$WOLFPAIR_DIR/.env"
chown "$ACTUAL_USER:$ACTUAL_USER" "$WOLFPAIR_DIR/.env"
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WOLFPAIR_DIR" chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WOLFPAIR_DIR"
# ── 4. Caddy (optional) ─────────────────────────────────────────────────── # ── 4. Caddy (optional) ───────────────────────────────────────────────────
@@ -491,6 +538,8 @@ docker compose logs -f # follow logs
- If you set up a Caddy subdomain (e.g. `wolf-pair.yourdomain.com`), that - If you set up a Caddy subdomain (e.g. `wolf-pair.yourdomain.com`), that
subdomain is for the PIN form only. subdomain is for the PIN form only.
MD MD
[ "$WOLFPAIR_PORT" != "8090" ] && \
sed -i "s/localhost:8090/localhost:${WOLFPAIR_PORT}/g" "$WOLFPAIR_DIR/README.md"
# ── 7. Build & start ────────────────────────────────────────────────────── # ── 7. Build & start ──────────────────────────────────────────────────────
echo "" echo ""