github stamparm/maltrail 3.3

9 hours ago

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

Don't miss a new maltrail release

NewReleases is sending notifications on new releases.