Skip to content

Releases: stamparm/maltrail

3.4

Choose a tag to compare

@github-actions github-actions released this 04 Sep 22:39

Full Changelog: 3.3...3.4

3.3

3.3

Choose a tag to compare

@stamparm stamparm released this 31 Aug 22:11

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 0x8100 tag and dropped anything else — QinQ frames never reached detection at all.
  • Multi-worker capture. CAPTURE_WORKERS defaulted to 1 because scaling out cost detections: PACKET_FANOUT_HASH splits 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. /geo was 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.
  • /trails answers an unchanged set with 304 instead of 87 MB. Every sensor polling UPDATE_SERVER re-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/maltrail

Binaries 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

3.2

Choose a tag to compare

@github-actions github-actions released this 27 Aug 18:09

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_URL posts 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. /counts is exact rather than estimated, and /hunt skips 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 /check and the trail drawer.
  • server --doctor preflight, 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/maltrail

Binaries 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

Choose a tag to compare

@github-actions github-actions released this 11 Aug 20:25

Full Changelog: 3.1...3.1.1

3.1

3.1

Choose a tag to compare

@github-actions github-actions released this 11 Aug 08:46

Full Changelog: 3.0.1...3.1

3.0.1

Choose a tag to compare

@github-actions github-actions released this 10 Aug 14:18

Full Changelog: 3.0...3.0.1

3.0

3.0

Choose a tag to compare

@github-actions github-actions released this 08 Aug 06:42

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

Full Changelog: 2.1...3.0

v3.0-rc1

v3.0-rc1 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 08 Aug 01:10

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

Full Changelog: 2.1...v3.0-rc1

2.1

2.1

Choose a tag to compare

@stamparm stamparm released this 30 Jun 22:11

Start-of-month release

1.5

1.5

Choose a tag to compare

@stamparm stamparm released this 31 May 22:11

Start-of-month release