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:
@@ -11,7 +11,7 @@ from typing import Any, Literal
|
||||
# submitters (PUT /…/services/{svc}/config) keep working with raw strings.
|
||||
TEXTAREA_B64_PREFIX = "b64:"
|
||||
|
||||
FieldType = Literal["string", "password", "int", "bool", "textarea", "enum"]
|
||||
FieldType = Literal["string", "password", "int", "bool", "textarea", "enum", "multi_enum"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -146,4 +146,23 @@ def _coerce(spec: ServiceConfigField, raw: Any) -> Any:
|
||||
f"{spec.key}: {s!r} not in allowed values {spec.enum}"
|
||||
)
|
||||
return s
|
||||
if t == "multi_enum":
|
||||
if not isinstance(raw, list):
|
||||
raise ConfigValidationError(
|
||||
f"{spec.key}: expected list, got {type(raw).__name__}"
|
||||
)
|
||||
if not raw:
|
||||
raise ConfigValidationError(f"{spec.key}: list must not be empty")
|
||||
seen: set[str] = set()
|
||||
result: list[str] = []
|
||||
for item in raw:
|
||||
s = str(item)
|
||||
if spec.enum and s not in spec.enum:
|
||||
raise ConfigValidationError(
|
||||
f"{spec.key}: {s!r} not in allowed values {spec.enum}"
|
||||
)
|
||||
if s not in seen:
|
||||
seen.add(s)
|
||||
result.append(s)
|
||||
return result
|
||||
raise ConfigValidationError(f"{spec.key}: unknown field type {t!r}")
|
||||
|
||||
@@ -43,6 +43,14 @@ class HTTPService(BaseService):
|
||||
label="Custom response body",
|
||||
type="textarea",
|
||||
),
|
||||
ServiceConfigField(
|
||||
key="http_versions",
|
||||
label="Supported HTTP versions",
|
||||
type="multi_enum",
|
||||
enum=["http/1.1", "http/2"],
|
||||
default=["http/1.1"],
|
||||
help="Protocol versions Caddy advertises. HTTP/3 requires TLS — use the https service.",
|
||||
),
|
||||
]
|
||||
|
||||
def compose_fragment(
|
||||
@@ -77,6 +85,8 @@ class HTTPService(BaseService):
|
||||
)
|
||||
if "custom_body" in cfg:
|
||||
fragment["environment"]["CUSTOM_BODY"] = cfg["custom_body"]
|
||||
if "http_versions" in cfg:
|
||||
fragment["environment"]["HTTP_VERSIONS"] = json.dumps(cfg["http_versions"])
|
||||
if "files" in cfg:
|
||||
files_path = str(Path(cfg["files"]).resolve())
|
||||
fragment["environment"]["FILES_DIR"] = "/opt/html_files"
|
||||
|
||||
@@ -59,6 +59,14 @@ class HTTPSService(BaseService):
|
||||
type="textarea",
|
||||
secret=True,
|
||||
),
|
||||
ServiceConfigField(
|
||||
key="http_versions",
|
||||
label="Supported HTTP versions",
|
||||
type="multi_enum",
|
||||
enum=["http/1.1", "http/2", "http/3"],
|
||||
default=["http/1.1"],
|
||||
help="Protocol versions Caddy advertises. HTTP/3 uses QUIC over UDP/443.",
|
||||
),
|
||||
]
|
||||
|
||||
def compose_fragment(
|
||||
@@ -103,6 +111,10 @@ class HTTPSService(BaseService):
|
||||
fragment["environment"]["TLS_KEY"] = cfg["tls_key"]
|
||||
if "tls_cn" in cfg:
|
||||
fragment["environment"]["TLS_CN"] = cfg["tls_cn"]
|
||||
if "http_versions" in cfg:
|
||||
fragment["environment"]["HTTP_VERSIONS"] = json.dumps(cfg["http_versions"])
|
||||
if "http/3" in cfg["http_versions"]:
|
||||
fragment.setdefault("ports", []).append("443:443/udp")
|
||||
|
||||
return fragment
|
||||
|
||||
|
||||
Reference in New Issue
Block a user