diff --git a/README.md b/README.md index 2fe1655..b9ab473 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Simply add `VIRTUAL_HOST=myapp.local` to any container or use native Traefik lab - [HTTPS Support](#https-support) - [Automatic HTTP and HTTPS Routes](#automatic-http-and-https-routes) - [Self-Signed Certificates](#self-signed-certificates) + - [HSTS Headers Disabled for Development](#hsts-headers-disabled-for-development) + - [Disabling HTTP/2 (workaround for stuck HTTPS sessions on macOS)](#disabling-http2-workaround-for-stuck-https-sessions-on-macos) - [Trusted Local Certificates with mkcert](#trusted-local-certificates-with-mkcert) - [Manual Certificate Generation (Alternative)](#manual-certificate-generation-alternative) - [Start the proxy](#start-the-proxy) @@ -420,6 +422,27 @@ Traefik automatically generates self-signed certificates for HTTPS routes. For t This is implemented using Traefik's `disable-hsts` middleware applied to the HTTPS entrypoint, ensuring **all HTTPS traffic** (both dinghy-layer and native Traefik routes) benefits from this development-friendly configuration. This is essential for development environments where certificates may frequently change, expire, or be regenerated. +### Disabling HTTP/2 (workaround for stuck HTTPS sessions on macOS) + +On macOS with Docker Desktop, long-lived HTTP/2 sessions between browsers and the proxy can become "zombies": the host-side TCP socket through vpnkit stays alive while the container-side socket has already closed, and no FIN/RST propagates across the loopback shim. The browser keeps multiplexing new requests into a dead session, and **all** `.loc` hosts time out (`ERR_TIMED_OUT` in Chrome, equivalent in Firefox) until the browser is fully restarted. Command-line clients (curl, wget) are unaffected because they open fresh connections per request. + +Setting `HTTP_PROXY_DISABLE_HTTP2=true` on the `traefik` service restricts TLS ALPN negotiation to `http/1.1`. Browsers fall back to HTTP/1.1 over TLS, which detects dead connections aggressively at the socket layer and avoids the zombie-session class of bugs. HTTPS keeps working normally; only the wire protocol version changes. + +```yaml +services: + traefik: + environment: + - HTTP_PROXY_DISABLE_HTTP2=true +``` + +Or via a top-level `.env`: + +``` +HTTP_PROXY_DISABLE_HTTP2=true +``` + +The default is `false`, so existing installations are unaffected. To verify the option is active, load any HTTPS site through the proxy and check the protocol column in your browser's network panel — it should report `HTTP/1.1` instead of `h2`. + ### Trusted Local Certificates with mkcert For browser-trusted certificates without warnings, use the `spark-http-proxy generate-mkcert` command. This command automatically handles the entire certificate generation process: diff --git a/build/traefik/entrypoint.sh b/build/traefik/entrypoint.sh index 864ac74..52fe871 100644 --- a/build/traefik/entrypoint.sh +++ b/build/traefik/entrypoint.sh @@ -5,6 +5,7 @@ CERTS_DIR="/traefik/certs" DYNAMIC_DIR="/traefik/dynamic" TLS_CONFIG_FILE="${DYNAMIC_DIR}/auto-tls.yml" +DISABLE_HTTP2_FILE="${DYNAMIC_DIR}/disable-http2.yml" generate_tls_config() { echo "Scanning for certificates in ${CERTS_DIR}..." @@ -88,6 +89,34 @@ EOF # Generate TLS configuration from user certificates generate_tls_config +# Optionally restrict TLS ALPN to HTTP/1.1 only. Useful on macOS with Docker +# Desktop, where long-lived HTTP/2 sessions across vpnkit can become stuck +# (kernel-side socket alive, container-side closed, no FIN/RST propagated) +# and cause browser timeouts on all .loc hosts until the browser is killed. +# When set, clients fall back to HTTP/1.1 over TLS, which detects dead +# connections aggressively at the socket layer. +generate_disable_http2_config() { + if [ "${HTTP_PROXY_DISABLE_HTTP2:-false}" = "true" ]; then + echo "Disabling HTTP/2 (HTTP_PROXY_DISABLE_HTTP2=true): forcing ALPN to http/1.1" + cat > "${DISABLE_HTTP2_FILE}" << 'EOF' +# Auto-generated by entrypoint.sh when HTTP_PROXY_DISABLE_HTTP2=true. +# Restricts the TLS ALPN list to http/1.1, preventing HTTP/2 negotiation +# on TLS entrypoints. +tls: + options: + default: + alpnProtocols: + - http/1.1 +EOF + else + # Make sure we don't keep a stale file from a previous run with the + # flag enabled. + rm -f "${DISABLE_HTTP2_FILE}" + fi +} + +generate_disable_http2_config + # Start Traefik with the original arguments echo "Starting Traefik..." exec traefik "$@" diff --git a/compose.yml b/compose.yml index e6de067..b008b0e 100644 --- a/compose.yml +++ b/compose.yml @@ -69,6 +69,7 @@ services: - "${LOCAL_HOME:-$HOME}/.local/spark/http-proxy/certs:/traefik/certs:ro" environment: - LOG_LEVEL=${LOG_LEVEL:-info} + - HTTP_PROXY_DISABLE_HTTP2=${HTTP_PROXY_DISABLE_HTTP2:-false} labels: - "traefik.enable=false" restart: unless-stopped