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:
@@ -1,6 +1,10 @@
|
||||
FROM caddy:2 AS caddy-bin
|
||||
|
||||
ARG BASE_IMAGE=debian:bookworm-slim@sha256:f9c6a2fd2ddbc23e336b6257a5245e31f996953ef06cd13a59fa0a1df2d5c252
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
COPY --from=caddy-bin /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -17,12 +21,18 @@ RUN chmod +x /entrypoint.sh
|
||||
RUN mkdir -p /opt/tls
|
||||
|
||||
EXPOSE 443
|
||||
|
||||
RUN useradd -r -s /bin/false -d /opt logrelay \
|
||||
&& chown -R logrelay:logrelay /opt/tls \
|
||||
&& mkdir -p /etc/caddy /opt/.local/share/caddy /opt/.config/caddy \
|
||||
&& chown -R logrelay:logrelay /etc/caddy /opt/.local /opt/.config \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends libcap2-bin \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& setcap 'cap_net_bind_service+eip' /usr/bin/caddy \
|
||||
&& (find /usr/bin/ -maxdepth 1 -name 'python3*' -type f -exec setcap 'cap_net_bind_service+eip' {} \; 2>/dev/null || true)
|
||||
|
||||
ENV XDG_DATA_HOME=/opt/.local/share XDG_CONFIG_HOME=/opt/.config
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD kill -0 1 || exit 1
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -9,10 +9,10 @@ with configurable pages. Forwards events as JSON to LOG_TARGET if set.
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import ssl
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, request, send_from_directory
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from werkzeug.serving import make_server, WSGIRequestHandler
|
||||
|
||||
import instance_seed as _seed
|
||||
@@ -29,7 +29,7 @@ logging.getLogger("werkzeug").setLevel(logging.ERROR)
|
||||
NODE_NAME = os.environ.get("NODE_NAME", "webserver")
|
||||
SERVICE_NAME = "https"
|
||||
LOG_TARGET = os.environ.get("LOG_TARGET", "")
|
||||
PORT = int(os.environ.get("PORT", "443"))
|
||||
PORT = int(os.environ.get("PORT", "8080"))
|
||||
|
||||
_SERVER_CHOICES = [
|
||||
"Apache/2.4.41 (Ubuntu)",
|
||||
@@ -50,8 +50,6 @@ FAKE_APP = os.environ.get("FAKE_APP", "")
|
||||
EXTRA_HEADERS = json.loads(os.environ.get("EXTRA_HEADERS", "{}"))
|
||||
CUSTOM_BODY = os.environ.get("CUSTOM_BODY", "")
|
||||
FILES_DIR = os.environ.get("FILES_DIR", "")
|
||||
TLS_CERT = os.environ.get("TLS_CERT", "/opt/tls/cert.pem")
|
||||
TLS_KEY = os.environ.get("TLS_KEY", "/opt/tls/key.pem")
|
||||
|
||||
_FAKE_APP_BODIES: dict[str, str] = {
|
||||
"apache_default": (
|
||||
@@ -86,6 +84,7 @@ _FAKE_APP_BODIES: dict[str, str] = {
|
||||
}
|
||||
|
||||
app = Flask(__name__)
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) # type: ignore[method-assign]
|
||||
|
||||
@app.after_request
|
||||
def _fix_server_header(response):
|
||||
@@ -155,10 +154,5 @@ class _SilentHandler(WSGIRequestHandler):
|
||||
|
||||
if __name__ == "__main__":
|
||||
_log("startup", msg=f"HTTPS server starting as {NODE_NAME}")
|
||||
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ctx.load_cert_chain(TLS_CERT, TLS_KEY)
|
||||
|
||||
srv = make_server("0.0.0.0", PORT, app, request_handler=_SilentHandler) # nosec B104
|
||||
srv.socket = ctx.wrap_socket(srv.socket, server_side=True)
|
||||
srv = make_server("127.0.0.1", PORT, app, request_handler=_SilentHandler)
|
||||
srv.serve_forever()
|
||||
|
||||
Reference in New Issue
Block a user