diff --git a/services/gitea.sh b/services/gitea.sh index 350abe7..9847af6 100644 --- a/services/gitea.sh +++ b/services/gitea.sh @@ -529,6 +529,8 @@ install_gitea() { if [ "$DRY_RUN" = true ]; then 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 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] 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" @@ -738,6 +740,25 @@ ENV # replacement requiring Caddy involvement. ───────────────────────────── 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_reverse_proxy_auth "$DIR" _gitea_offer_actions_runner "$DIR" diff --git a/services/wolf-pair.sh b/services/wolf-pair.sh index a802d2f..c7a8894 100644 --- a/services/wolf-pair.sh +++ b/services/wolf-pair.sh @@ -174,6 +174,19 @@ CBLOCK [ -f "$_file" ] || return 0 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 # 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 " - Run the container with network_mode: host (for localhost:47989 access)" 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" return 0 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" ensure_docker_dir_ownership "$WOLFPAIR_DIR" 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 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 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 # secret in here is dead — show the waiting page instead of re-offering it. @@ -390,7 +422,7 @@ class Handler(BaseHTTPRequestHandler): if __name__ == '__main__': 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 log_success "server.py written" @@ -420,12 +452,27 @@ services: dockerfile: Dockerfile container_name: wolf-pair network_mode: host + environment: + - WOLFPAIR_PORT=${WOLFPAIR_PORT:-8090} volumes: - /var/run/docker.sock:/var/run/docker.sock:ro restart: unless-stopped COMPOSE 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" # ── 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 subdomain is for the PIN form only. MD + [ "$WOLFPAIR_PORT" != "8090" ] && \ + sed -i "s/localhost:8090/localhost:${WOLFPAIR_PORT}/g" "$WOLFPAIR_DIR/README.md" # ── 7. Build & start ────────────────────────────────────────────────────── echo ""