fix(caddy+syslog): add UnmarshalCaddyfile to H2FP/FP handlers; add start_fp_socket_reader to syslog_bridge
This commit is contained in:
@@ -31,6 +31,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/caddyserver/caddy/v2"
|
"github.com/caddyserver/caddy/v2"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
|
||||||
|
"github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile"
|
||||||
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
"github.com/caddyserver/caddy/v2/modules/caddyhttp"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -39,6 +41,12 @@ func init() {
|
|||||||
caddy.RegisterModule(H2FPListenerWrapper{})
|
caddy.RegisterModule(H2FPListenerWrapper{})
|
||||||
caddy.RegisterModule(FPHandler{})
|
caddy.RegisterModule(FPHandler{})
|
||||||
caddy.RegisterModule(DecnetJSONLEncoder{})
|
caddy.RegisterModule(DecnetJSONLEncoder{})
|
||||||
|
httpcaddyfile.RegisterHandlerDirective("decnet_fp", parseFPHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFPHandler(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, error) {
|
||||||
|
var fp FPHandler
|
||||||
|
return &fp, fp.UnmarshalCaddyfile(h.Dispenser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sockPath() string {
|
func sockPath() string {
|
||||||
@@ -103,6 +111,10 @@ func (w *H2FPListenerWrapper) WrapListener(ln net.Listener) net.Listener {
|
|||||||
return &h2FPListener{Listener: ln, logger: w.logger}
|
return &h2FPListener{Listener: ln, logger: w.logger}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *H2FPListenerWrapper) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type h2FPListener struct {
|
type h2FPListener struct {
|
||||||
net.Listener
|
net.Listener
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
@@ -230,6 +242,10 @@ func (h *FPHandler) Provision(ctx caddy.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *FPHandler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *FPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
func (h *FPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
|
||||||
// Collect ordered header names. Go's http.Header is a map so we cannot
|
// Collect ordered header names. Go's http.Header is a map so we cannot
|
||||||
// recover arrival order from it directly. We read the raw wire order via
|
// recover arrival order from it directly. We read the raw wire order via
|
||||||
@@ -279,10 +295,12 @@ func (h *FPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddy
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ caddy.Provisioner = (*H2FPListenerWrapper)(nil)
|
_ caddy.Provisioner = (*H2FPListenerWrapper)(nil)
|
||||||
_ caddy.ListenerWrapper = (*H2FPListenerWrapper)(nil)
|
_ caddy.ListenerWrapper = (*H2FPListenerWrapper)(nil)
|
||||||
_ caddy.Provisioner = (*FPHandler)(nil)
|
_ caddyfile.Unmarshaler = (*H2FPListenerWrapper)(nil)
|
||||||
|
_ caddy.Provisioner = (*FPHandler)(nil)
|
||||||
_ caddyhttp.MiddlewareHandler = (*FPHandler)(nil)
|
_ caddyhttp.MiddlewareHandler = (*FPHandler)(nil)
|
||||||
|
_ caddyfile.Unmarshaler = (*FPHandler)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── caddy.logging.encoders.decnet_jsonl ──────────────────────────────────────
|
// ── caddy.logging.encoders.decnet_jsonl ──────────────────────────────────────
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ Facility: local0 (16). SD element ID uses PEN 55555.
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import hashlib as _hashlib
|
|
||||||
import json as _json
|
import json as _json
|
||||||
|
import os as _os
|
||||||
import re
|
import re
|
||||||
import socket as _socket
|
import socket as _socket
|
||||||
import threading as _threading
|
import threading as _threading
|
||||||
@@ -266,105 +266,54 @@ def forward_syslog(line: str, log_target: str) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# ─── Caddy fp-socket reader ───────────────────────────────────────────────────
|
# ─── Caddy fingerprint socket reader ─────────────────────────────────────────
|
||||||
|
|
||||||
_FP_SOCK_SIZE = 65536
|
_FP_BUF = 65536
|
||||||
|
|
||||||
|
|
||||||
def _ja4h_from_record(rec: dict) -> str:
|
def _fp_socket_reader(node_name: str, service_name: str, log_target: str) -> None:
|
||||||
method = rec.get("method", "")[:2].upper() or "UN"
|
sock_path = _os.environ.get("DECNET_FP_SOCK", "/run/decnet/fp.sock")
|
||||||
proto = rec.get("proto", "")
|
|
||||||
ver_map = {
|
|
||||||
"HTTP/1.0": "10", "HTTP/1.1": "11", "HTTP/2.0": "20", "HTTP/3.0": "30",
|
|
||||||
}
|
|
||||||
ver_tag = ver_map.get(proto.upper(), "00")
|
|
||||||
headers: list[str] = rec.get("headers_ordered", [])
|
|
||||||
has_cookie = "c" if any(h.lower() == "cookie" for h in headers) else "n"
|
|
||||||
has_referer = "r" if any(h.lower() == "referer" for h in headers) else "n"
|
|
||||||
lang = rec.get("accept_language", "") or ""
|
|
||||||
lang_tag = (lang[:4].ljust(4, "0") if lang else "0000")
|
|
||||||
filtered = [h for h in headers if h.lower() not in ("cookie", "referer")]
|
|
||||||
count_tag = f"{min(len(filtered), 99):02d}"
|
|
||||||
header_hash = _hashlib.sha256(",".join(h.lower() for h in filtered).encode()).hexdigest()[:12]
|
|
||||||
cookie_val = rec.get("cookie", "") or ""
|
|
||||||
if cookie_val:
|
|
||||||
pairs = sorted(p.strip() for p in cookie_val.split(";") if "=" in p.strip())
|
|
||||||
cookie_hash = _hashlib.sha256(";".join(pairs).encode()).hexdigest()[:12]
|
|
||||||
else:
|
|
||||||
cookie_hash = "000000000000"
|
|
||||||
return f"{method}{ver_tag}{has_cookie}{has_referer}{lang_tag}_{count_tag}_{header_hash}_{cookie_hash}"
|
|
||||||
|
|
||||||
|
|
||||||
def _fp_socket_reader(
|
|
||||||
node_name: str,
|
|
||||||
service_name: str,
|
|
||||||
log_target: str,
|
|
||||||
sock_path: str = "/run/decnet/fp.sock",
|
|
||||||
) -> None:
|
|
||||||
import os as _os
|
|
||||||
try:
|
try:
|
||||||
sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_DGRAM)
|
sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_DGRAM)
|
||||||
_os.makedirs(_os.path.dirname(sock_path), exist_ok=True)
|
|
||||||
try:
|
|
||||||
_os.unlink(sock_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
sock.bind(sock_path)
|
sock.bind(sock_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
return
|
return
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = sock.recv(_FP_SOCK_SIZE)
|
data = sock.recv(_FP_BUF)
|
||||||
rec = _json.loads(data.decode("utf-8", errors="replace"))
|
record = _json.loads(data)
|
||||||
kind = rec.get("kind", "")
|
except (OSError, ValueError):
|
||||||
remote = rec.get("remote_addr", "").split(":")[0]
|
continue
|
||||||
|
kind = record.get("kind", "")
|
||||||
if kind == "http_request":
|
remote = record.get("remote_addr", "-")
|
||||||
ja4h = _ja4h_from_record(rec)
|
if kind == "h2_settings":
|
||||||
proto_tag = rec.get("proto_tag", "h1")
|
ln = syslog_line(
|
||||||
line = syslog_line(
|
service_name, node_name, "http2_settings", SEVERITY_INFO,
|
||||||
service_name, node_name, "http_request_fingerprint",
|
remote_addr=remote,
|
||||||
attacker_ip=remote,
|
settings=_json.dumps(record.get("settings", {})),
|
||||||
ja4h=ja4h,
|
frame_order=_json.dumps(record.get("frame_order", [])),
|
||||||
protocol=proto_tag,
|
)
|
||||||
method=rec.get("method", ""),
|
write_syslog_file(ln)
|
||||||
path=rec.get("path", ""),
|
if log_target:
|
||||||
)
|
forward_syslog(ln, log_target)
|
||||||
write_syslog_file(line)
|
elif kind == "http_request":
|
||||||
forward_syslog(line, log_target)
|
ln = syslog_line(
|
||||||
|
service_name, node_name, "http_request_fingerprint", SEVERITY_INFO,
|
||||||
elif kind == "h2_settings":
|
remote_addr=remote,
|
||||||
settings_hash = _hashlib.sha256(
|
proto=record.get("proto_tag", "-"),
|
||||||
_json.dumps(rec.get("settings", {}), sort_keys=True).encode()
|
headers_ordered=_json.dumps(record.get("headers_ordered", [])),
|
||||||
).hexdigest()[:12]
|
cookie=record.get("cookie", ""),
|
||||||
line = syslog_line(
|
accept_language=record.get("accept_language", ""),
|
||||||
service_name, node_name, "http2_settings",
|
)
|
||||||
attacker_ip=remote,
|
write_syslog_file(ln)
|
||||||
settings=_json.dumps(rec.get("settings", {})),
|
if log_target:
|
||||||
frame_order=_json.dumps(rec.get("frame_order", [])),
|
forward_syslog(ln, log_target)
|
||||||
settings_hash=settings_hash,
|
|
||||||
)
|
|
||||||
write_syslog_file(line)
|
|
||||||
forward_syslog(line, log_target)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def start_fp_socket_reader(
|
def start_fp_socket_reader(node_name: str, service_name: str, log_target: str) -> None:
|
||||||
node_name: str,
|
|
||||||
service_name: str,
|
|
||||||
log_target: str = "",
|
|
||||||
sock_path: str = "/run/decnet/fp.sock",
|
|
||||||
) -> None:
|
|
||||||
import os as _os
|
|
||||||
if not _os.path.isdir(_os.path.dirname(sock_path) or "."):
|
|
||||||
return
|
|
||||||
t = _threading.Thread(
|
t = _threading.Thread(
|
||||||
target=_fp_socket_reader,
|
target=_fp_socket_reader,
|
||||||
args=(node_name, service_name, log_target, sock_path),
|
args=(node_name, service_name, log_target),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name="fp-socket-reader",
|
|
||||||
)
|
)
|
||||||
t.start()
|
t.start()
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ Facility: local0 (16). SD element ID uses PEN 55555.
|
|||||||
|
|
||||||
import base64
|
import base64
|
||||||
import binascii
|
import binascii
|
||||||
import hashlib as _hashlib
|
|
||||||
import json as _json
|
import json as _json
|
||||||
|
import os as _os
|
||||||
import re
|
import re
|
||||||
import socket as _socket
|
import socket as _socket
|
||||||
import threading as _threading
|
import threading as _threading
|
||||||
@@ -266,120 +266,54 @@ def forward_syslog(line: str, log_target: str) -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# ─── Caddy fp-socket reader ───────────────────────────────────────────────────
|
# ─── Caddy fingerprint socket reader ─────────────────────────────────────────
|
||||||
|
|
||||||
_FP_SOCK_SIZE = 65536 # max unix datagram payload
|
_FP_BUF = 65536
|
||||||
|
|
||||||
|
|
||||||
def _ja4h_from_record(rec: dict) -> str:
|
def _fp_socket_reader(node_name: str, service_name: str, log_target: str) -> None:
|
||||||
"""Compute JA4H from a Caddy decnet_fp 'http_request' record."""
|
sock_path = _os.environ.get("DECNET_FP_SOCK", "/run/decnet/fp.sock")
|
||||||
method = rec.get("method", "")[:2].upper() or "UN"
|
|
||||||
proto = rec.get("proto", "")
|
|
||||||
ver_map = {
|
|
||||||
"HTTP/1.0": "10", "HTTP/1.1": "11", "HTTP/2.0": "20", "HTTP/3.0": "30",
|
|
||||||
}
|
|
||||||
ver_tag = ver_map.get(proto.upper(), "00")
|
|
||||||
headers: list[str] = rec.get("headers_ordered", [])
|
|
||||||
has_cookie = "c" if any(h.lower() == "cookie" for h in headers) else "n"
|
|
||||||
has_referer = "r" if any(h.lower() == "referer" for h in headers) else "n"
|
|
||||||
lang = rec.get("accept_language", "") or ""
|
|
||||||
lang_tag = (lang[:4].ljust(4, "0") if lang else "0000")
|
|
||||||
filtered = [h for h in headers if h.lower() not in ("cookie", "referer")]
|
|
||||||
count_tag = f"{min(len(filtered), 99):02d}"
|
|
||||||
header_str = ",".join(h.lower() for h in filtered)
|
|
||||||
header_hash = _hashlib.sha256(header_str.encode()).hexdigest()[:12]
|
|
||||||
cookie_val = rec.get("cookie", "") or ""
|
|
||||||
if cookie_val:
|
|
||||||
pairs = sorted(p.strip() for p in cookie_val.split(";") if "=" in p.strip())
|
|
||||||
cookie_hash = _hashlib.sha256(";".join(pairs).encode()).hexdigest()[:12]
|
|
||||||
else:
|
|
||||||
cookie_hash = "000000000000"
|
|
||||||
return f"{method}{ver_tag}{has_cookie}{has_referer}{lang_tag}_{count_tag}_{header_hash}_{cookie_hash}"
|
|
||||||
|
|
||||||
|
|
||||||
def _fp_socket_reader(
|
|
||||||
node_name: str,
|
|
||||||
service_name: str,
|
|
||||||
log_target: str,
|
|
||||||
sock_path: str = "/run/decnet/fp.sock",
|
|
||||||
) -> None:
|
|
||||||
"""Read JSON fingerprint records from the Caddy fp unix datagram socket."""
|
|
||||||
import os as _os
|
|
||||||
# Create the socket as the receiver (we bind, Caddy writes)
|
|
||||||
try:
|
try:
|
||||||
sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_DGRAM)
|
sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_DGRAM)
|
||||||
_os.makedirs(_os.path.dirname(sock_path), exist_ok=True)
|
|
||||||
try:
|
|
||||||
_os.unlink(sock_path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
sock.bind(sock_path)
|
sock.bind(sock_path)
|
||||||
except Exception:
|
except OSError:
|
||||||
return
|
return
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data = sock.recv(_FP_SOCK_SIZE)
|
data = sock.recv(_FP_BUF)
|
||||||
rec = _json.loads(data.decode("utf-8", errors="replace"))
|
record = _json.loads(data)
|
||||||
kind = rec.get("kind", "")
|
except (OSError, ValueError):
|
||||||
remote = rec.get("remote_addr", "").split(":")[0] # strip port
|
continue
|
||||||
|
kind = record.get("kind", "")
|
||||||
if kind == "http_request":
|
remote = record.get("remote_addr", "-")
|
||||||
ja4h = _ja4h_from_record(rec)
|
if kind == "h2_settings":
|
||||||
proto_tag = rec.get("proto_tag", "h1")
|
ln = syslog_line(
|
||||||
line = syslog_line(
|
service_name, node_name, "http2_settings", SEVERITY_INFO,
|
||||||
service_name, node_name, "http_request_fingerprint",
|
remote_addr=remote,
|
||||||
attacker_ip=remote,
|
settings=_json.dumps(record.get("settings", {})),
|
||||||
ja4h=ja4h,
|
frame_order=_json.dumps(record.get("frame_order", [])),
|
||||||
protocol=proto_tag,
|
)
|
||||||
method=rec.get("method", ""),
|
write_syslog_file(ln)
|
||||||
path=rec.get("path", ""),
|
if log_target:
|
||||||
)
|
forward_syslog(ln, log_target)
|
||||||
write_syslog_file(line)
|
elif kind == "http_request":
|
||||||
forward_syslog(line, log_target)
|
ln = syslog_line(
|
||||||
|
service_name, node_name, "http_request_fingerprint", SEVERITY_INFO,
|
||||||
elif kind == "h2_settings":
|
remote_addr=remote,
|
||||||
settings_hash = _hashlib.sha256(
|
proto=record.get("proto_tag", "-"),
|
||||||
_json.dumps(rec.get("settings", {}), sort_keys=True).encode()
|
headers_ordered=_json.dumps(record.get("headers_ordered", [])),
|
||||||
).hexdigest()[:12]
|
cookie=record.get("cookie", ""),
|
||||||
line = syslog_line(
|
accept_language=record.get("accept_language", ""),
|
||||||
service_name, node_name, "http2_settings",
|
)
|
||||||
attacker_ip=remote,
|
write_syslog_file(ln)
|
||||||
settings=_json.dumps(rec.get("settings", {})),
|
if log_target:
|
||||||
frame_order=_json.dumps(rec.get("frame_order", [])),
|
forward_syslog(ln, log_target)
|
||||||
settings_hash=settings_hash,
|
|
||||||
)
|
|
||||||
write_syslog_file(line)
|
|
||||||
forward_syslog(line, log_target)
|
|
||||||
|
|
||||||
elif kind == "h3_settings":
|
|
||||||
line = syslog_line(
|
|
||||||
service_name, node_name, "http3_settings",
|
|
||||||
attacker_ip=remote,
|
|
||||||
settings=_json.dumps(rec.get("settings", {})),
|
|
||||||
frame_order=_json.dumps(rec.get("frame_order", [])),
|
|
||||||
)
|
|
||||||
write_syslog_file(line)
|
|
||||||
forward_syslog(line, log_target)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def start_fp_socket_reader(
|
def start_fp_socket_reader(node_name: str, service_name: str, log_target: str) -> None:
|
||||||
node_name: str,
|
|
||||||
service_name: str,
|
|
||||||
log_target: str = "",
|
|
||||||
sock_path: str = "/run/decnet/fp.sock",
|
|
||||||
) -> None:
|
|
||||||
"""Start the Caddy fp-socket reader in a daemon thread."""
|
|
||||||
import os as _os
|
|
||||||
if not _os.path.isdir(_os.path.dirname(sock_path) or "."):
|
|
||||||
return
|
|
||||||
t = _threading.Thread(
|
t = _threading.Thread(
|
||||||
target=_fp_socket_reader,
|
target=_fp_socket_reader,
|
||||||
args=(node_name, service_name, log_target, sock_path),
|
args=(node_name, service_name, log_target),
|
||||||
daemon=True,
|
daemon=True,
|
||||||
name="fp-socket-reader",
|
|
||||||
)
|
)
|
||||||
t.start()
|
t.start()
|
||||||
|
|||||||
Reference in New Issue
Block a user