feat(services): HTTP/2 + HTTP/3 support via Caddy reverse-proxy

Swap Werkzeug for Caddy as the protocol layer for http and https decoy
services. Flask keeps owning app logic (fake_app, custom_body, headers,
syslog) on 127.0.0.1:8080; Caddy terminates h1/h2/h2c/h3 on the wire
with real-world TLS/QUIC fingerprints.

- Add `multi_enum` FieldType to ServiceConfigField + _coerce
- Add `http_versions` field to HTTPService (h1/h2c) and HTTPSService
  (h1/h2/h3); selecting h3 emits UDP/443 port mapping in compose
- Rewrite both Dockerfiles with multi-stage Caddy binary copy +
  setcap for port binding as the logrelay user
- Entrypoints parse HTTP_VERSIONS JSON, render a Caddyfile, start
  Flask in background, wait for it, then exec Caddy
- https/server.py drops direct TLS handling; Caddy owns the cert
- Add ProxyFix to both server.py so Flask sees real attacker IPs
- Frontend: multi_enum checkbox-group renderer in ServiceConfigFields;
  FormValue union extended to string[]; compactPayload skips []
- Fix stale test_smtp_relay_schema_matches_smtp: relay schema is a
  superset of smtp, not equal; update assertions accordingly
This commit is contained in:
2026-05-10 00:04:37 -04:00
parent ec5b49144e
commit 0653e500b5
14 changed files with 435 additions and 31 deletions

View File

@@ -4,10 +4,8 @@ set -e
TLS_DIR="/opt/tls"
mkdir -p "$TLS_DIR"
# TLS_CERT/TLS_KEY may arrive as either a host-side path OR raw PEM
# content (the wizard ships PEM textareas as decoded strings). Detect by
# looking for a PEM header; if present, write to disk and rebind the var
# to the on-disk path.
# TLS_CERT/TLS_KEY may arrive as either a host-side path OR raw PEM content.
# Detect by looking for a PEM header; if present, write to disk.
if [ -n "$TLS_CERT" ] && printf '%s' "$TLS_CERT" | grep -q 'BEGIN '; then
printf '%s' "$TLS_CERT" > "$TLS_DIR/cert.pem"
CERT="$TLS_DIR/cert.pem"
@@ -31,8 +29,46 @@ if [ ! -f "$CERT" ] || [ ! -f "$KEY" ]; then
2>/dev/null
fi
# server.py reads TLS_CERT/TLS_KEY as filesystem paths.
export TLS_CERT="$CERT"
export TLS_KEY="$KEY"
# Parse HTTP_VERSIONS JSON → Caddy protocol tokens (h1 / h2 / h3)
CADDY_PROTOCOLS=$(python3 -c "
import json, os
versions = json.loads(os.environ.get('HTTP_VERSIONS', '[\"http/1.1\"]'))
tokens = []
if 'http/1.1' in versions:
tokens.append('h1')
if 'http/2' in versions:
tokens.append('h2')
if 'http/3' in versions:
tokens.append('h3')
print(' '.join(tokens) if tokens else 'h1')
")
exec python3 /opt/server.py
cat > /etc/caddy/Caddyfile <<EOF
{
admin off
servers :443 {
protocols ${CADDY_PROTOCOLS}
}
}
:443 {
tls ${CERT} ${KEY}
reverse_proxy 127.0.0.1:8080
}
EOF
python3 /opt/server.py &
# Wait for Flask to be ready before handing off to Caddy
python3 -c "
import socket, time
for _ in range(40):
try:
s = socket.create_connection(('127.0.0.1', 8080), timeout=0.25)
s.close()
break
except OSError:
time.sleep(0.1)
"
exec caddy run --config /etc/caddy/Caddyfile