Four ways traffic could reach the sensor and never be inspected. All four are closed in this release.
Nothing to do on upgrade — unlike 3.2, this one needs no configuration change.
Traffic that was not being looked at
Each of these was confirmed by replay before it was fixed: the packet arrived, and no event came out.
- IPv6 extension headers. A packet carrying a Hop-by-Hop, Routing or Destination-Options header reported protocol 0/43/60, so the transport header was never located — no port, no DNS name, no HTTP host, no TLS SNI. Eight bytes hid a packet from every payload-derived trail. The chain is walked now.
- Tunnels. A SPAN feed at a datacentre or corporate border is usually an overlay: the addresses on the mirror port are your own tunnel endpoints, and every host worth watching is inside. A DNS query for a listed domain inside VXLAN, GENEVE, GRE or ERSPAN produced zero events. Those, plus IP-in-IP, are decapsulated now, and outer-header matching is unchanged.
- Stacked VLAN tags. The Ethernet path skipped exactly one
0x8100tag and dropped anything else — QinQ frames never reached detection at all. - Multi-worker capture.
CAPTURE_WORKERSdefaulted to 1 because scaling out cost detections:PACKET_FANOUT_HASHsplits by flow, the scan heuristics count by source, and a scanner walking ephemeral ports is a new flow per probe, so its evidence scattered across workers until none reached the threshold. Measured against the corpus, of the heuristic alerts one worker raises, 91% survived at 2 workers, 87% at 4, 66% at 8. Source-affine fanout is now the default whenever more than one worker is configured, and retains 100%.
The dashboard
Profiled in a browser against a real server holding a busy day — 200k events, 120k distinct threats. Interleaved A/B, medians:
| before | after | |
|---|---|---|
/events over the wire
| 24.3 MB | 6.1 MB |
| Cold load | 2979 ms | 1465 ms |
| Opening a threat's detail panel | 39.9 ms | 6.2 ms |
| Sorting by severity | 390 ms | 101 ms |
| Showing 100 rows | 159 ms | 84 ms |
| Typing a word into search (main thread blocked) | 571 ms | 75 ms |
| Scrolling the page behind the open panel | 46% of frames dropped | 0% |
| The panel's open animation | 83% of frames over 20 ms | 13% |
The event log is compressed while streaming — only when the client asks for it, and a 100 MB day still never has to fit in memory. Behind the numbers: risk scores and IP sort keys were being re-derived inside sort comparators millions of times per sort, the grid re-filtered and re-sorted every threat merely to turn a page, and the detail panel walked all 120k threats to produce two integers.
Three detail-panel bugs went with it: the panel was sized in vh so the text-size control left it short at the smallest step and 1200 px tall in an 857 px viewport at the largest, with its own buttons off-screen; scrolling past its end scrolled the table behind it by 600 px; and closing it left keyboard focus on a button in an off-screen panel.
Also in this release
- Multiple days at once (#4, open since November 2015). Shift-click a second day; the arrows then step a whole window.
/geowas taking only the first date in a range and quietly mapping one day's worth under a heading covering seven. LOCAL_LOG_FORMAT text|json(#19130). Every reader — server, sensor and the browser — understands both, and a single day's file may hold both if the option is flipped mid-day.- Ignore list: networks, ranges and port ranges (#19142).
192.168.1.0/24,2001:db8::/64,10.0.0.1-10.0.0.50,1024-65535. Previously exact string equality on all four fields, so silencing a subnet meant writing out every address in it. - DNS tunnelling detection, built to be hard to trigger: a tunnel and an antivirus reputation lookup look nearly identical from query shape alone, so a plain entropy-and-length detector is a machine for generating tickets about your own antivirus.
/trailsanswers an unchanged set with 304 instead of 87 MB. Every sensor pollingUPDATE_SERVERre-downloaded the whole set every cycle.- A redistribution check. Nine integrations in our README are marked "trails only" — FireHOL, oisd, NextDNS, pfBlockerNG and the rest turn our lists into firewall DROP rules and DNS blocklists without ever running
update_trails(), so none of our filtering reaches them. Every check we had asked "would Maltrail match this?", which says nothing about what those consumers get.
Also: constant-time auth comparison and tests for three auth surfaces that had none; the shipped fail2ban regex no longer bans your own hosts; an event-log handle that leaked on some request paths; two platforms the suspicious lists exist to name were being whitelisted; and a batch of allocation removals on the DNS, TLS and QUIC paths.
Removed: the Python sensor and the differential harness that existed to drive it. It was kept after the 3.0 cutover as an oracle — replay the corpus through both and diff — which was right while the port was being proved. Three releases later, "agrees with a retired implementation" stopped being a property worth a 45-minute gate step.
See the CHANGELOG for the full list.
Verifying what you downloaded
sha256sum -c maltrail-sensor-3.3-x86_64-unknown-linux-gnu.tar.gz.sha256
gh attestation verify oci://ghcr.io/stamparm/maltrail:3.3 --repo stamparm/maltrailBinaries are built inside AlmaLinux 8 with libpcap linked statically, so they start on RHEL 8/9, Debian 12, Ubuntu and openSUSE Leap 15 whether or not libpcap is installed. Alpine/musl has no prebuilt sensor; build from source there.
Container: ghcr.io/stamparm/maltrail:3.3
Full Changelog: 3.2...3.3