Releases: stamparm/maltrail
Release list
3.4
3.3
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
3.2
The static trails now live in their own repository, stamparm/trails, and reach a deployment as one assembled file instead of shipping in the source tree.
Everything else in this release is secondary to that, so read the upgrade note first.
Upgrading from 3.1.x â do this
A maltrail.conf written before 3.2 has no STATIC_TRAILS_URL, and without it the static trail set is not loaded at all. Add:
STATIC_TRAILS_URL https://github.com/stamparm/trails/releases/latest/download/trails.csv.gz
âĶor re-run install.sh, which writes it for you.
Maltrail will not let this pass quietly. The updater prints a banner across the terminal, server --doctor fails with exit 1, and maltrail-sensor -T fails its preflight â so the systemd unit's ExecStartPre catches it before the sensor ever runs. Losing the trail set is indistinguishable from a quiet network, and that is not a thing to discover in a month.
There is deliberately no silent fallback to a default URL. A deployment that fetches its detection content from an address the operator never configured is a worse answer than one that says what is missing.
Why the split
trails/static was 98.7% of every blob byte in this repository's history and 9,169 of 9,549 commits. Two consequences: git log, git blame and git bisect were useless on the code, and updating detection content meant pulling a new version of the software.
The repository went from 9,578 commits and a 110 MiB pack to 353 commits and 3.3 MiB. Content history was not deleted â it moved, with authorship intact.
What changes for a deployment
| Static trails | fetched from STATIC_TRAILS_URL, cached next to TRAILS_FILE |
| Update cost | ~11 MB gzipped, or 65 bytes when the published digest says nothing changed |
| Content freshness | published up to 4Ã/day; new indicators reach you within hours instead of on your next git pull |
| Pinning | point STATIC_TRAILS_URL at a dated content-YYYYMMDD-HHMM release so a bad publish is not immediately global |
| Offline / air-gapped | trails-bootstrap.csv.gz ships with this release and install.sh seeds the cache from it, so a first start detects immediately even with no connectivity |
| Integrity | a payload that does not match its published sha256 is refused in favour of the cache |
| Feeds | unchanged â still fetched by your deployment directly from each publisher |
| Custom trails | unchanged, but CUSTOM_TRAILS_DIR no longer defaults inside the installation directory |
trails/feeds/ moved to feeds/. trails/static and trails/custom stopped pretending to be feed plugins â they only ever were so update_trails() could run one loop, which is why DISABLED_FEEDS static used to silently disable 1.6M trails.
Also in this release
Everything that accumulated since 3.1.1:
- Alerting:
ALERT_WEBHOOK_URLposts events at or above a severity threshold, with throttling. Slack, Mattermost, Discord, a SIEM collector, or your own endpoint. - Event index: a per-day SQLite sidecar.
/countsis exact rather than estimated, and/huntskips non-matching lines instead of reading every one. - JA3/JA4 client fingerprints matched as trails, byte-identical to the Python implementation and pinned by generated vectors.
- Beaconing heuristic â timer-regular reconnects to one destination, rated suspicious, never malware, because uptime monitors beacon too.
- Trail confidence: how strongly sources agree, surfaced in
/checkand the trail drawer. server --doctorpreflight, and both config parsers now flag an unknown option instead of ignoring a typo.- Longest-match whitelist precedence â an exact static trail on a more specific name than its whitelisted ancestor now fires. 3,082 trails on shared platforms were loaded, counted, and could never match.
- Source citations survive the split: each release of the trail set carries a provenance sidecar, so the trail drawer still shows the report an indicator was extracted from.
Fixes worth naming: UDP event intake was losing 24% at 10k events/s (a thread and an open() per datagram â now lossless to 20k/s); /hunt's IP search was 9Ã slower than a plain substring hunt; --detect-test told a healthy install its detection was broken; --smoke-test printed nothing and exited 1; every IPv6 event was unmapped on the attack map. The server also refuses to start with the private key Maltrail used to ship, and RIPEstat is proxied through /ripe so script-src is back to 'self'.
See the CHANGELOG for the full list.
Verifying what you downloaded
sha256sum -c maltrail-sensor-3.2-x86_64-unknown-linux-gnu.tar.gz.sha256
gh attestation verify oci://ghcr.io/stamparm/maltrail:3.2 --repo stamparm/maltrailBinaries are built inside AlmaLinux 8 with libpcap linked statically. Checked on the published x86_64 binary: highest glibc symbol required is 2.28, and libpcap is not among its dynamic dependencies â so it starts 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.2
3.1.1
3.1
3.0.1
3.0
What's Changed
- build(deps): bump fancy-regex from 0.16.2 to 0.19.0 in /sensor by @dependabot[bot] in #19587
- build(deps): bump the actions group with 12 updates by @dependabot[bot] in #19592
New Contributors
- @dependabot[bot] made their first contribution in #19587
Full Changelog: 2.1...3.0
v3.0-rc1
What's Changed
- build(deps): bump fancy-regex from 0.16.2 to 0.19.0 in /sensor by @dependabot[bot] in #19587
- build(deps): bump the actions group with 12 updates by @dependabot[bot] in #19592
New Contributors
- @dependabot[bot] made their first contribution in #19587
Full Changelog: 2.1...v3.0-rc1