Data-plane key functions
Internals live
Heimdall's code carries no explanatory comments by design — behavior is documented
here instead. This page maps the request flow to the functions that carry it,
so a contributor can find the right place fast. It lists the load-bearing entry
points per stage, not every helper.
All paths are under dataplane/security-engine/. The connection loop lives in
internal/proxy/pipeline.go; the entry binary is cmd/security-engine/main.go,
which parses flags, builds the telemetry sink and plugin host, and calls
proxy.New(...).Run(ctx).
Ingress · internal/proxy, internal/httpengine, internal/proxyproto
| Function |
Location |
Role |
Proxy.Run |
proxy/proxy.go:188 |
Accept loop; MaxConns admission gate; spawns serve per connection |
Proxy.serve |
proxy/pipeline.go:36 |
Per-connection state machine: PROXY header → geo/anon → conn-enrich → H2 sniff → keep-alive loop → forward |
proxyproto.ReadV2WithTLVs |
proxyproto/proxyproto.go:55 |
Parse PROXY v2 + TLVs into a fingerprint (when a plugin enricher is present) |
proxyproto.parseTLVs |
proxyproto/proxyproto.go:78 |
TLV walk: 0x02 SNI, 0xE0 JA3, 0xE1 JA4, 0x20 nested SSL |
Proxy.fillHead |
proxy/pipeline.go:372 |
Read until the head is framable; else errDegrade → raw splice |
Parser.Parse |
httpengine/httpengine.go:49 |
H1 head parser → fills canonical.Request, sets anomaly flags |
httpengine.resolveFraming |
httpengine/httpengine.go:198 |
Content-Length vs Transfer-Encoding resolution; sets smuggling anomalies |
Proxy.sniffH2 / serveH2 |
proxy/h2.go:30 / :62 |
HTTP/2 detection (h2c/ALPN) and serving (own HPACK) |
Proxy.relayWebSocket |
proxy/websocket.go:33 |
Inspect frames after a 101 upgrade |
Normalization · internal/normalize
| Function |
Location |
Role |
Normalizer.Request |
normalize/normalize.go:23 |
Normalize path + every param raw → norm, backed by the request scratch arena |
Normalizer.apply |
normalize/normalize.go:49 |
The multi-pass loop: percent → entity → NFKC + homoglyph, then whitespace + lowercase |
percentDecode |
normalize/normalize.go:155 |
%XX decode, optional + → space |
decodeEntities |
normalize/normalize.go:191 |
Named (generated table) + numeric/hex HTML entities |
foldUnicode |
normalize/normalize.go:85 |
NFKC then confusable/homoglyph → ASCII |
StripSQLComments |
normalize/normalize.go:333 |
Engine-local comment strip (used by the SQL engine) |
Normalizer.TrackRawSpan |
normalize/track.go:11 |
Norm→Raw offset map for evidence capture |
Parsers · internal/parsers
Orchestrated by Proxy.analyze (proxy/pipeline.go:416), content-type gated. Each
calls canonical.AddParam(src, name, raw).
| Function |
Location |
Source |
Query / Form |
parsers/query.go:12 / :16 |
Query / Form |
Headers |
parsers/headers.go:22 |
Header / Cookie |
JSON |
parsers/json.go:30 |
JSON (recursive flatten) |
Multipart |
parsers/multipart.go:20 |
Multipart |
GraphQL / XML / SOAP |
parsers/graphql.go:21 / xml.go:28 / soap.go:20 |
GraphQL / XML / SOAP |
JWT |
parsers/jwt.go:21 |
JWT (feeds auth alg:none) |
WebSocketHandshake / IsWebSocketUpgrade |
parsers/websocket.go:219 / :237 |
WebSocket |
Canonical model · internal/canonical
| Symbol |
Location |
Role |
Request |
canonical/canonical.go:88 |
The model; two arenas: scratch (normalize) + arena (parser output) |
Source enum |
canonical/canonical.go:31 |
Param origin tags |
AnomalyFlags |
canonical/canonical.go:50 |
13-flag bitset (smuggling + H2 misuse) |
Request.Reset |
canonical/canonical.go:114 |
Pooled reuse (reqPool) |
AddParam |
canonical/canonical.go:163 |
Append a flattened param |
Features · internal/features
| Function |
Location |
Role |
features.Extract |
features/features.go:27 |
Per-request maxima: depth, entropy, tokens, non-ASCII, param count |
shannon / log2fixed |
features/features.go:90 / :108 |
Fixed-point Shannon entropy |
Tokenize |
features/features.go:132 |
Token-run callback shared with SQL/XSS engines |
Detection · internal/detect
| Symbol |
Location |
Role |
Registry.Analyze |
detect/detect.go:101 |
Iterate engines sequentially over the params |
EngineID / Category |
detect/detect.go:9 / :28 |
Engine + category enums; EnginePluginBase = 128 |
| engines |
detect/{sql,xss,ssrf,cmdi,traversal,header,protocol,api,rate,auth}/… |
One package per engine (see Detection) |
custom.Analyze |
detect/custom/custom.go |
Operator + signed-feed rules |
Registry wiring: proxy/proxy.go:127. Custom-rule and plugin findings are added in
Proxy.analyze (proxy/pipeline.go:441 / :445).
Risk & decision · internal/risk, internal/decision
| Function |
Location |
Role |
risk.Engine.Assess |
risk/risk.go:65 |
Risk × weight/256 × conf/100, saturating; adds anomaly weights → Assessment |
decision.Engine.Decide |
decision/decision.go:86 |
Map total → Action via thresholds; clamp to Monitor in shadow |
actionFor |
decision/decision.go:109 |
The threshold ladder |
Telemetry · internal/telemetry
| Function |
Location |
Role |
Proxy.buildEvent |
proxy/pipeline.go:587 |
Build the event from the analyzed request |
AsyncSink.Emit |
telemetry/async.go:36 |
Non-blocking enqueue (drops + counts if full) |
encode |
telemetry/async.go:65 |
Hand-rolled JSONL encoder |
Anonymizer.HashIP |
telemetry/anonymize.go:39 |
HMAC-SHA256(IP, key) → uint64; IP never stored |
The one-screen sequence
Proxy.Run → Proxy.serve
ReadV2WithTLVs (IP + JA3/JA4/SNI)
HashIP + geo lookup
EnrichConnection (plugins) ── conn findings cached
keep-alive loop:
fillHead → Parser.Parse → canonical.Request (+anomalies)
router.match(Host) → site bundle
access.Check ──deny──► emitAccess → writeBlock
Proxy.analyze:
parsers.* → Params
Normalizer.Request → Param.Norm
Registry.Analyze + custom.Analyze + plugin findings
features.Extract
risk.Assess → decision.Decide (shadow clamps to Monitor)
buildEvent → AsyncSink.Emit
Action → challenge | block | throttle | forward → origin