Repository navigation
Expand file tree
/
Copy pathCHANGELOG
More file actions
1065 lines (713 loc) · 46.6 KB
/
Copy pathCHANGELOG
File metadata and controls
1065 lines (713 loc) · 46.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Copyright (c) 2014-present Maltrail developers (https://github.com/stamparm/maltrail/)
# See the file 'LICENSE' for copying permission
#################################################################
# Maltrail Changelog File #
#################################################################
[+] Added functionality
[-] Deleted functionality
[!] Bug fixing
[=] Minor update or changed functionality
#################################################################
- Version 3.5 -> 3.6 (unreleased)
[!] Multi-worker capture with CAPTURE_FANOUT_MODE source sent a host's
outgoing packets to the wrong worker whenever the destination MAC began
with 0x4 or 0x6: on transmit the kernel hands the fanout program the
Ethernet header, which it mistook for an IP header. 1,000 outgoing
packets from 40 sources all went to worker 0 of 4; they now split
250/250/250/250. The program reads the kernel's protocol and network
header offset instead of guessing.
[!] A USERS entry no longer works as its own password. It stored
sha256(password), and logging in proved knowledge of exactly that, so
anyone who could read maltrail.conf could log in. Entries are now salted
PBKDF2 with a SCRAM-style proof ('python3 server.py --hash-password';
install.sh writes this form), and the login nonce is issued by the
server, so a captured login cannot be replayed after a restart. Old
sha256 entries still work, with a warning at start and in --doctor.
[!] error.log was created world-writable (0666), so any local account
could append to it. It is 0644 now, owned by the log directory's owner,
and an existing world-writable one is tightened. The sensor also prints
a parser panic once per location rather than once per packet (a crafted
packet replayed at line rate flooded journald), and its capture counters
no longer wrap at 2^32 packets (12 hours at 100k packets/s).
[!] install.sh installs the latest release by default instead of master.
The prebuilt sensor exists only for releases, so a master tree got an
older sensor, which ignored options newer than itself. '--ref master'
still works and says that the two differ.
[!] /events capped a date range at nothing: one request for
0001-01-01_9999-12-31 was 3.65 million stat() calls. It uses the same
92-day cap as every other endpoint now. Its open log handle was also
kept on the session between Range requests, so two tabs read the same
file object and a request for one day could be answered from another.
[+] DNS responses are checked for listed CNAME targets. A client asking for
an innocent-looking name the resolver aliases to a listed one never
looked the listed name up, so nothing matched. The event is reported
like the query it answers (source the client, destination the
resolver), its trail names both ("(x).evil.com (innocent.org)"), and a
response yields at most one event; targets are matched against trails
only, not judged by the heuristics.
[-] Code only the retired Python sensor used: core/log.py's event writer
and condenser (log_event, flush_condensed_events) and core/meta.py's
writer and pruner. The server keeps everything it calls (the auth
events' syslog/logstash forwarding, severity_of, the /meta reader).
[!] The published admin/changeme! login is accepted only from the server
itself; over the network it is refused with a message saying to set
USERS. The shipped maltrail.conf listens on 0.0.0.0, so every install
that kept it was open to anyone who had read the README. install.sh
now generates a password of the install's own and prints it once.
[-] The per-day event index (USE_EVENT_INDEX, LOG_DIR/index/, server.py
--rebuild-index) is gone, and its files are removed on start. It took
~10 s and twice the log's size per day to build, and opening the date
picker could build ~120 days in one request. /counts now counts lines
(44 ms per 55 MB day, exact); /hunt parses only lines that can contain
the query (0.17 s per 55 MB day instead of 6.2 s).
[!] Fragmented IPv4 traffic is inspected: the kernel reassembles it
(CAPTURE_FANOUT_DEFRAG, now on by default; one worker joins a fanout
group of its own for it). A DNS query, HTTP request or ClientHello
split into IP fragments was never seen before.
[!] The server's dashboard comes up at once instead of after the first
trail update, which runs in the background (20 s update: /ping answered
after 0.5 s instead of 20.5 s).
[=] The sensor's fast hash starts from a random per-process state, so
colliding source addresses cannot be computed in advance.
[!] With USE_SSL, one client that connected and never sent anything locked
every other user out of HTTPS, indefinitely: the TLS handshake ran on
the thread that accepts connections, with no timeout. It runs in the
request's own thread now, with a 10 s limit.
[!] /fail2ban and /blacklist returned nothing whenever today's log held a
byte the locale could not decode (any non-ASCII byte under a C locale,
the usual one for systemd units and containers), so fail2ban banned no
one. They also rescanned the whole day every 8 s (~7 s of CPU per
65 MB day); they read only the appended bytes now.
[!] Alert webhook failures (4xx, 5xx, timeouts) were reported as delivered
and lost. They are retried with backoff from a queue, on their own
thread, and logged. Downloads no longer silently retry HTTPS over
plain HTTP, which had exposed the webhook URL and the static trails'
checksum to anyone on the path.
[!] A live sensor with a trails.csv already on disk starts capturing at
once and updates in the background. It used to detect nothing until
the update finished, which can take minutes, on every start.
[=] Scan heuristics keep threshold+1 items per key instead of up to 1024,
and DNS tunneling judges distinctness on a pair's first 256 queries and
stops recording once it has reported: a flood could fill gigabytes.
[!] The server lost most remote-sensor events whenever it was busy. Its UDP
receiver shared one Python interpreter with the dashboard and the trail
update, and with one other thread busy it lost 98% of 5,000 events/s.
It runs as its own process now: 0% at 20,000/s with the rest busy.
[!] Datagrams the server refuses (wrong LOG_SERVER_SECRET, clock skew) are
counted per sender and listed under sensors on the dashboard. Such a
sensor's heartbeats are refused too, so it used to just look dead.
[!] The sensor resolved LOG_SERVER once and kept that address for good, while
heartbeats re-resolved: after the server's IP changed it showed alive
while every event went to the old address. All remote endpoints are
re-resolved every minute, off the capture threads.
[!] A failed static-trails download no longer replaces a working trails.csv
with a feeds-only build (a few percent of the set), which the server
would then hand to every sensor pulling from it. The old file is kept
and the update reports the failure.
[=] Stale trails are reported every update cycle instead of once per process
(error.log and a new maltrail_trails_age_seconds metric), and a trail
update that hangs is killed after an hour instead of blocking reloads.
[=] meta.sqlite is written by one thread, not by the capture workers: a
worker hands its window over in ~10-90 us instead of stalling capture
for the write. The write itself is one sorted upsert per row.
[=] The trail hash table is sized to its entries instead of the next power
of two: 109 -> 74 MB for the full trail set (RSS 158 -> 122 MB).
[=] The trail store is built file-backed only on hosts with under 2 GB of
RAM. Its pages are reclaimable only after being written back, so every
start and reload wrote the whole store to disk (125 MB measured on a
Pi 4, to its SD card) even where the heap holds it comfortably. A Pi 4
now writes ~5 MB in its first minute instead of 125 MB.
[=] DNS queries for new names and subdomains are 25-37% cheaper (subdomain
query 660 -> 430 ns, cold 405 -> 300 ns). The tunneling heuristic decides
a zone's exemption once per (source, zone) pair instead of per query,
the exempt-zone set no longer uses SipHash, both DNS accumulators share
one hash of each name, the domain caches hash a name once per lookup and
insert, and short-key hashing no longer calls memcpy. Events unchanged.
[=] potential remote code execution heur is improved (Issue #19646).
[=] JA3, JA4 and certificate SHA-1 digests are computed only when the trail
set holds a key of that shape. The shipped trail set holds none, so a
TLS handshake no longer pays for an MD5, two SHA-256s and a SHA-1 that
could never match (ClientHello path ~5,300 -> ~1,000 instructions).
DNS names are decoded into a reused buffer and checked in fewer passes,
and the domain caches evict with SIEVE instead of LRU, so a hit sets a
bit instead of relinking a list (warm query 230 -> 171 ns, cold 478 ->
401 ns). On the reference network
with the shipped trail shapes the packet path runs 38% fewer
instructions (757M -> 467M) and replays 31% faster, events unchanged.
[=] Faster packet path: 46% fewer instructions per packet on the reference
network (500 hosts, 208k packets), with byte-identical events. Release
builds use fat LTO (9-17% on every benchmark row); JA4's SHA-256s are
memoized per worker and a ClientHello parse reuses its buffers instead
of a dozen allocations; DNS subdomain queries no longer allocate per
name in the exhaustion/tunneling accumulators (1100 -> 730 ns); a QUIC
Initial skips server packets and cached keys, and decrypts in batches
(2980 -> 900 ns).
[!] TLS and QUIC ClientHellos longer than one packet are put back together.
Post-quantum key shares made a browser's ~2 KB, and Chrome's shuffled
extension order puts the SNI in the second TCP segment about half of the
time (122 of 240 real Chromium handshakes); every Chrome QUIC ClientHello
spans two Initials. On those captures the sensor read 118 of 240 SNIs
and no QUIC one; now all of them, and JA3/JA4 come from the whole
ClientHello. Fixed memory, 64 x 4 KB per worker; no measurable cost on
other traffic.
[!] IPv6 TCP was mostly invisible with the shipped CAPTURE_FILTER: libpcap's
tcp[] reads IPv4 only, so no IPv6 SYN (outside the HTTP/proxy ports) and
no IPv6 TLS handshake was captured, and SNI was read from IPv4 only. The
filter now spells IPv6 TCP with ip6[] offsets and also passes the second
segment of a ClientHello. An existing maltrail.conf keeps its filter
until it is updated.
[-] USE_FAST_PREFILTER, FAST_FLOW_CUTOFF, FAST_ADMIT_LEVEL and
FAST_ADMIT_ADAPTIVE are gone. They configured the retired Python sensor's
C prefilter; all that was left of them was switching SNI matching on,
which is now CHECK_TLS_SNI and on by default. With DNS over HTTPS the SNI
is where the domain is still visible, and the handshake was already
parsed for JA3/JA4, so it costs one lookup per connection.
[=] maltrail.conf cleanup. CAPTURE_FANOUT is CAPTURE_WORKERS (they were the
same number), CHECK_TLS_CERTIFICATES is CHECK_TLS_FINGERPRINTS (it also
switches JA3/JA4), and CAPTURE_BUFFER, the Python sensor's ring, is no
longer mandatory: its 10% is the default, so a stock install keeps the
same 64-256 MB libpcap ring. The old names are still read. PROCESS_COUNT
is no longer set: it only ever applied to EVENT_THROTTLE_MODE legacy.
[!] /fail2ban no longer publishes the source of 'potential iot-malware
download': since #19622 that event fires only when one of OUR hosts fetches
the dropper, so the shipped FAIL2BAN_REGEX handed fail2ban the infected
machine it is meant to protect.
[!] Dashboard: a heuristic on a bare TLD ('(label).com') from 50+ hosts no
longer folds every OTHER heuristic on that TLD into the same flood threat
(a DGA bot vanished inside 60 hosts' long-domain events), and a throttle
summary line ending in '(+N more)' joins its threat instead of opening a
second one.
[+] Heuristic 'icmp_tunneling' (icmpsh, ptunnel, ICMP exfiltration): more
than 10 distinct echo payloads from one host to one destination that match
no ping tool's pattern. Real iputils ping (default, -s, -p), mtr and
busybox ping raised nothing; a 40-request tunnel was reported.
[+] Heuristic 'idn_homograph': a queried name whose xn-- label, with its
Cyrillic/Greek/Armenian lookalike letters read as Latin, becomes a
whitelisted domain it is not (xn--pple-43d.com, "аpple.com"). Every IDN in
the Cisco and Alexa top 1M lists (3,291) raised nothing.
[=] The default CAPTURE_FILTER keeps QUIC's data packets out of the capture
ring and passes only its handshake (the long-header packets, which carry
the SNI), the way it already passed only the start of a TCP connection.
QUIC is about half of what the old filter passed on a real transit link
(MAWI, Oct 2026). One worker under a 3 Mpps QUIC-heavy load went from 11%
dropped and 8,880 of 10,000 planted detections to nothing dropped and all
10,000. An existing maltrail.conf keeps its filter until it is updated.
[!] The multi-worker startup warning "fanout hashes by flow" no longer
appears with CAPTURE_FANOUT_MODE source, the default, where it was false.
[!] DISABLED_HEURISTICS udp_scanning / web_scanning now stop the per-packet
tracking those heuristics need, not only their alerts (+3.4% packets per
second with both muted, on a UDP-heavy mix).
[=] DNS events on a subdomain throttle per zone: '(label).zone' trails (long
domains, DGA verdicts, a listed TLD or parent domain) share one budget per
host, zone and detection, the grouping the dashboard already shows. A DNS
tunnel or a DGA bot names a new label every time, so each wrote one event
per query: 220 for one tunnel and 177 for one bot in the reference
network, now 3 per minute and one '(*).zone' summary line.
[!] The beaconing case of the replay corpus asserts its detection again. Its
expectation was kept for the retired Python parity harness only, so from
27 Aug nothing would have noticed beaconing breaking.
[!] A scan (port, UDP, web, infection) or ICMP tunnel in the last second of
a replayed capture is reported. The heuristics report on the next
packet's arrival, and at the end of the file none arrived.
- Version 3.4 -> 3.5 (14 Oct 2026)
[+] Runs on a 256 MB Raspberry Pi. The trail update needed ~1.1 GB and was
OOM-killed on every small box; the sensor needed 153 MB and twice that
across a reload. Measured in a 221 MB container with no swap, against the
full 1.9M trail set: update 69 MB, sensor 32 MB, reload 58 MB. Thanks to
#19641 for the report and the idea.
- the static aggregate is streamed from its cache instead of being held
as bytes, text and a dict at once - the update is ~255 MB on every
machine now, not 1.1 GB
- below 768 MB available (cgroup limits included) the merge spills to a
SQLite file; same code, byte-identical trails.csv, ~4x slower
- 'UPDATE_SERVER' downloads stream to disk
- the sensor builds its trail store into unlinked, file-backed mappings,
so it is reclaimable page cache instead of heap; lookups measured the
same speed
- hosts under 1 GB get a capture ring of 10% of RAM, not 64 MB
[+] A static ARMv6 sensor ('arm-unknown-linux-musleabihf') for every 32-bit
Raspberry Pi, Pi 1 and Zero included; install.sh picks it on armv6l and
armv7l, and on a 64-bit kernel under a 32-bit userland. The release
replays a trail through it under an emulated Pi 1 CPU and refuses a
binary that does not detect it.
[+] Basic sensor status on the dashboard (#19627).
[+] Windows installer ('install.ps1'); NetBSD and OpenBSD join the tested
platform matrix, nine of nine checks each.
[=] Workflows and shell scripts are linted in CI; a test covers the seam
between the sensor that writes the event log and the server that reads it.
- Version 3.3 -> 3.4 (04 Sep 2026)
[+] Prebuilt sensors for musl (Alpine), FreeBSD, macOS and Windows, next to
glibc Linux. Windows is built and RUN under Wine in CI; the Windows build
found four bugs that only running it could. docs/compat publishes the
platform matrix - 19 platforms, every capability checked, none failing.
[+] 'LOG_SERVER_SECRET': event datagrams to the server are authenticated, with
a configurable replay window. Forged detections are evidence tampering.
[+] Spamhaus DROP as a '/check_ip' verdict (#19598). One severity policy shared
by the server and the dashboard (#19621, #19622); a domain-only malware
list derived for DNS filters (#19620).
[+] '--detect-test --keep DIR [--serve]' keeps the generated events and serves
them; 'DISABLED_HEURISTICS' documents all eight and warns on a typo.
[!] The released Linux sensor could not capture at all: the statically linked
libpcap refused promiscuous mode on 'any'. Found by the new captures
column of the platform matrix.
[!] The dashboard silently dropped every JA3 and JA4 detection.
[!] install.sh died on the whole RHEL 9 family (curl-minimal conflict), and on
FreeBSD and macOS (four Linux assumptions).
[!] '/fail2ban' published a field fragment instead of the attacker; '/geo'
skipped JSON lines; 'cleared.txt' did nothing on a JSON log; '/hunt' fails
closed on a netfilter it cannot parse; '(custom)' masking looks at the
reference field only.
[-] Two frozen feeds and cleantalk; Googlebot is no longer called an attacker.
Removed the retired mmap ring, core/fastfilter.py and the unused cargo-fuzz
crate (parser fuzzing moved into the test suite).
- Version 3.2 -> 3.3 (31 Aug 2026)
[-] The Python sensor ('old/') is retired, with the harness that only existed
to diff against it.
[+] Decapsulation of VXLAN, GENEVE, GRE, ERSPAN and IP-in-IP; stacked VLAN
(QinQ) tags; the full IPv6 extension-header chain. Each of these used to
drop the frame, which on such a link means a sensor that sees nothing.
[+] DNS tunneling detection, built to be hard to trigger.
[+] 'LOCAL_LOG_FORMAT text|json' (#19130), read by every server-side reader;
the ignore list accepts networks, ranges and port ranges (#19142); more
than one day of events can be selected (#4).
[+] A reference network (sensor/tools/refnet.py): a synthetic LAN with planted
ground truth, so detection rate and noise are numbers.
[=] Multi-worker capture uses source-affine fanout by default; flow hashing
split a scanner's evidence across workers and lost scan alerts.
[=] '/trails' answers an unchanged set with 304 instead of 84 MB; '/events' is
compressed and validated; the dashboard sorts and opens the drawer faster.
[!] Constant-time auth compare; the shipped fail2ban regex no longer bans our
own hosts; address trails are gated like names (#19607); trails naming a
registry instead of a host fail the build.
[=] The version lives in five files and one command now bumps all of them.
- Version 3.1.1 -> 3.2 (27 Aug 2026)
[+] Static trails moved to their own repository (stamparm/trails) and are now
distributed as one assembled, signed file instead of shipped in the source
tree. They were 98.7% of every blob byte in this repository's history and
9,169 of 9,549 commits, which made 'git log', 'blame' and 'bisect' useless
on the code and meant updating detection required pulling a new version of
the software. This repository went from 9,578 commits and a 110 MiB pack to
344 and 3.3 MiB. Set 'STATIC_TRAILS_URL' (shipped in maltrail.conf); pin it
to a dated release if you want to control when new content lands.
[!] UPGRADING FROM 3.1.x: an older maltrail.conf has no 'STATIC_TRAILS_URL', and
without it the static trail set is not loaded at all. The updater prints a
banner, 'server --doctor' fails, and 'maltrail-sensor -T' fails, rather than
letting detection quietly shrink to heuristics and feeds. Copy the option
from the shipped maltrail.conf, or re-run install.sh.
[+] Releases now carry a trail-set snapshot ('trails-bootstrap.csv.gz') and
install.sh seeds the runtime cache from it, so a first start detects
immediately - including with no connectivity at all.
[+] The trail set is fetched gzipped, and only when it has changed: the
published sha256 is checked first, so a deployment updating more often than
the content moves transfers 65 bytes instead of 11 MB. A payload that does
not match its digest is refused in favour of the cache.
[+] Every content commit is now gated (reachability, canaries, assembly) in the
content repository - the per-commit check that did not exist before.
[=] 'trails/feeds' became 'feeds/'; 'trails/static' and 'trails/custom' stopped
pretending to be feeds. They were plugin modules only so update_trails()
could run one loop, which is why 'DISABLED_FEEDS static' silently disabled
1.6M trails. Three explicit sources through one merge rule now, same order.
[=] 'CUSTOM_TRAILS_DIR' no longer defaults inside the installation directory.
[+] Alert webhook: POST events at or above a severity threshold, with dedup.
[+] Per-day sidecar event index: exact '/counts', '/hunt' skips non-matching
lines. Trail-confidence sidecar surfaced in '/check' and the trail drawer.
[+] TLS client fingerprints (JA3/JA4) matched as trails; new beaconing
heuristic; 'server --doctor' preflight; both config parsers flag unknown
options.
[!] An exact static trail now beats a whitelisted ancestor (longest match), so
3,082 trails on shared platforms that were loaded, counted and could never
fire now do.
[!] UDP event intake was losing 24% at 10k events/s - a thread and an open() per
datagram. Lossless intake went from 5,000 to 20,000 events/s.
[!] '/hunt' IP search was 9x slower than a substring hunt over the same log.
[!] '--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.
[!] Refuses to start with the private key Maltrail used to ship. RIPEstat is
proxied through '/ripe' so script-src is 'self' again.
- Version 3.1 -> 3.1.1 (11 Aug 2026)
[+] 'install.sh': one command sets up a working deployment - dependencies,
a shallow clone, the prebuilt sensor, an unprivileged user, log and
state directories, '/etc/maltrail.conf', systemd units and setcap.
Re-running it upgrades. Run from inside a checkout you already have, it
installs THAT tree and never touches its git state. Exercised in
ubuntu, debian, fedora, opensuse and alpine containers on every commit,
where the server is started and asked for '/ping'
[!] Release binaries are built against glibc 2.28 (in an AlmaLinux 8
container) instead of the runner's 2.39, so they actually start on
RHEL 8+, Debian 10+, Ubuntu 18.04+ and Leap 15.x. Before this they
failed at exec with "GLIBC_2.39 not found" on most enterprise
distributions. The release now refuses to publish a binary needing
anything newer
[!] Python 3.6 works again. The whole 3.7 floor came down to ONE call -
'str.isascii()' in core/update.py - which made the trail update die on
every distribution whose stock 'python3' is 3.6 (RHEL 8, CentOS 7,
openSUSE Leap 15 / SLE 15, Amazon Linux 2), leaving an empty trail set
and a sensor that detected nothing. There is a 3.6 path now, the whole
server suite and a full offline trail build run on 3.6.15 in CI, and the
sensor's interpreter floor follows
[!] The Docker image adapts to the storage it is given instead of demanding uid
10001 own it: 'docker/entrypoint.sh' picks the uid that can write the log
and state directories (bind-mount owner, 'PUID'/'PGID', or the image's own
user), then drops privileges before starting anything (#19596)
[!] 'cap_add: NET_RAW' had no effect on the sensor container: Docker grants a
capability in the bounding set and leaves the ambient set empty for a
non-root 'USER', so the capture socket failed with EPERM no matter what the
compose file said. The entrypoint raises the ambient capabilities, and only
for the sensor
[!] The sensor never updated its trails when started without an absolute '-c'
path - a relative configuration file resolved the repository root to '',
and chdir('') failed as 'unable to run python3.12: No such file or
directory'. That is every Docker and tarball install: the sensor ran on
with an empty trail set and detected nothing
[!] 'docker compose -f docker/docker-compose.yml up' failed outright: compose
resolves relative bind-mount paths against the compose file's directory, so
'./maltrail.conf' meant 'docker/maltrail.conf', which does not exist
- Version 3.0.1 -> 3.1 (11 Aug 2026)
[+] The sensor matches TLS server certificates against the trail set by their
SHA-1 fingerprint ('CHECK_TLS_CERTIFICATES', new 'CERT' trail type), fed by
the new 'sslblcert' feed (~10,400 C2 certificates from abuse.ch SSLBL). A
certificate outlives the address and domain in front of it, so it keeps
matching after those rotate. Rated 'suspicious' rather than malware
[+] New '/check?q=<domain|ip|url>' server endpoint: answers whether one
indicator is in the trail set and which key matched, reading through the
memory-mapped trail store so it costs no memory. Custom trails require a
session, since 'ENABLE_MASK_CUSTOM' already treats their names as private
[+] 'SYSLOG_SERVER' and 'LOGSTASH_SERVER' may each name several collectors,
separated by commas, for redundant SIEM targets
[+] New 'torprojectnodes' feed: all running Tor relays from the Tor Project's
own Onionoo API, not only exit nodes - a client connects to a guard relay
and never to an exit, so outbound Tor use was previously invisible
[!] Both sensors now parse PPPoE-encapsulated IP. Traffic mirrored from a
DSL/fibre uplink is PPPoE end to end, so a SPAN port carrying it was
detected as nothing at all while the capture host's own traffic was fine
[!] 'retrieve_content' keeps cookies across a redirect, which restores three
cybercrime-tracker feeds (~39,000 indicators) that a cookie-and-redirect
challenge had silently reduced to zero
[!] Removed six feeds whose services were retired years ago (Palevo Tracker,
Ransomware Tracker x3, ZeuS Tracker x2), and repointed 'feodotrackerip' at
the CSV that abuse.ch still fills. A feed that yields nothing is now
reported; that warning used to be suppressed for abuse.ch URLs, which is
why all seven rotted unnoticed
[!] '/blacklist' is gated like '/fail2ban' ('BLACKLIST_ALLOWLIST', falling back
to 'FAIL2BAN_ALLOWLIST'); an authenticated session also passes. It reads the
event log and returned the flagged source IPs to anyone
[!] The Docker image health-checks the process it actually runs. Its default
command is the server, but the healthcheck ran the sensor's preflight, so a
server-only container reported itself unhealthy while serving correctly
[=] Docker: 'MALTRAIL_UID'/'MALTRAIL_GID' build args, and documentation for
bind-mounted log directories, which keep the host's ownership and leave the
unprivileged container unable to write the daily log
[=] 'sensor/tools/check.sh', 'parity.py' and 'bench_compare.py' report why they
cannot run instead of failing obscurely: missing toolchain or capture
bindings, an interpreter older than 3.7, and a benchmark whose reference
sensor never started
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 3.0 -> 3.0.1 (10 Aug 2026)
[!] The sensor now verifies that the Python driving the trail updater is 3.7+
(core/update.py needs 'str.isascii()'), and prefers a newer 'python3.N' on
PATH over an older default 'python3'. On distributions whose python3 is 3.6
(openSUSE Leap 15, SLE 15) the trail set silently failed to build, leaving a
sensor that detected NOTHING while '-T' reported the interpreter as present
and the configuration test as passed
[!] '-T' fails, rather than passing with warnings, when the trail updater cannot
run at all
[=] '-T' prints the command that fixes an unwritable or missing LOG_DIR, and
names the interpreter and version it found
[=] 'sensor/tools/check.sh' checks for cargo/rustfmt/clippy/python3 before doing
any work, names the package that provides each, and states that none of them
are needed to build or run the sensor
[=] Documented prerequisites that were previously assumed: 'setcap'
(libcap2-bin / libcap / libcap-progs), Python 3.7+, and that a distribution's
packaged Rust 1.74 is sufficient, so 'rustup' should not be installed
alongside it
[!] Fixed the quick-start command that created LOG_DIR: '-g "$USER"' assumed a
group named after the user, which does not exist on every distribution
- Version 2.2 -> 3.0 (08 Aug 2026)
[+] The sensor is rewritten in Rust and becomes the sensor: 'sensor/', built with
'cargo build --release'
[+] Sensor '-T' preflight validates configuration, trails, whitelist, log directory,
capture filter and capabilities, and exits non-zero, so a broken deployment can be
caught before it runs blind
[+] Prometheus metrics endpoint for the sensor ('STATS_ADDRESS'), exposing dropped
packets, trail generation, per-worker counters and build info
[+] Trail reloads are atomic and happen without a restart, on SIGHUP or within a second
of the trail file changing
[+] Event throttling is reworked into three selectable modes: burst-then-summarize
(default), byte-exact legacy, and off
[=] The sensor captures with ONE worker by default and no longer derives its worker
count from 'PROCESS_COUNT'. The kernel flow-hashes capture while the scan
heuristics count per source, so the previous default of 16 ran every install with
diluted heuristics to buy throughput a single worker already has; scaling out is
now an explicit 'CAPTURE_FANOUT'/'CAPTURE_WORKERS' opt-in. Exact trail detection is
identical at any worker count
[+] The sensor writes the condensed observable store ('USE_CONDENSED_STORAGE',
'meta.sqlite') that feeds the server's novelty and retro-hunt views, in the same
format core/meta.py produces and the server reads
[+] Differential parity harness: a generated fixture corpus plus one sampled from the
operator's real trails.csv, replayed through both sensors, requiring byte-identical
events and an identical condensed observable store ('sensor/tools/parity.py')
[+] 'sensor/tools/check.sh' runs the whole gate: formatting, lints, tests in both debug
and release profiles, and both parity corpora
[+] systemd units run both the sensor and the server as an unprivileged user, with
systemd-managed state/log directories, a read-only filesystem, and exactly
CAP_NET_RAW + CAP_NET_ADMIN for the sensor
[-] Python 2 support is dropped; Python 3.7+ is required and 'thirdparty/' is removed
[-] Plugin support is removed from both sensors, along with 'plugins/'
[-] The sample SSL PEM is no longer shipped: a private key in a public repository is a
private key everybody already has (see the 'openssl' hint in maltrail.conf)
[-] 'requirements.txt' is removed: the server is standard library only, and the old
sensor's pcapy-ng dependency now lives in 'old/requirements.txt'
[=] The previous Python sensor moves to 'old/', kept as the reference implementation and
as the oracle the parity harness replays against
[=] Runtime data files (whitelist, ua, worst_asns, cdn_ranges, bogon_ranges,
ignore_events) move from 'misc/' to 'data/'; 'misc/' is no longer in the repository
[=] Sensor throughput: 865 ns/packet against 23,448 ns/packet for the old sensor on the
same capture and the same trail set, and one worker per core rather than a single
capture process feeding a shared-memory ring
[!] The sensor now refreshes trails on its own timer, as sensor.py always did; without it
a long-running sensor detected against whatever trails.csv it started with
[!] Docker images build and run again, unprivileged, with the sensor enabled
[!] '--version' and other short-lived invocations no longer lose their output to
os._exit() skipping stdio flushing
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 2.1 -> 2.2 (01 Aug 2026)
[+] Retro hunt over collected observables
[+] Searchable metadata collection ('USE_CONDENSED_STORAGE', 'meta.sqlite')
[+] Geomap in the reporting interface, and the heatmap returns in place of the calendar
[+] Events expand to show the source trail file they came from
[+] Fast in-C prefilter and heuristics engine: sliding-window/stealth/UDP scan detection,
capture fanout and CPU affinity, per-heuristic muting (new capture paths opt-in)
[+] The sensor column returns to the table, detail drawer, filter and export, for
multi-sensor deployments
[!] RIPE geo lookups get an air-gap circuit breaker, so an offline server no longer
storms retries and spams the console
[!] Bug fixes (Issues #19558, #19559, #19561, #19562, #19564, #19565, #19581)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 1.5 -> 2.1 (01 Jul 2026)
[+] New reporting interface
[+] '--smoke-test' and '--detect-test' self-checks
[+] Server caches '_counts'/'_statics', caps POST size and applies a request timeout
[=] Drastic reduction in trail RAM usage
[=] Dashboard is faster and more robust: 'complete()' rework, debounced resize, fixed
handler leak, no more NaN charts
[!] Trail regexes were silently cleared during reloads
[!] Trail files were written and read non-atomically
[!] Logging concurrency and memory fixes
[!] CACHE_TYPE enum fix
[!] Offline PCAPs with oversized packets no longer break the sensor
[!] '/events' is served correctly when the client sends no Range header
[-] Defunct/obsolete feed removed
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 1.4 -> 1.5 (01 Jun 2026)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 1.3 -> 1.4 (01 May 2026)
[!] CIDR mask validation no longer relies on 'assert', which is stripped under optimized
Python ('python -O')
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 1.2 -> 1.3 (01 Apr 2026)
[!] Charts size themselves from the browser window rather than the monitor
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 1.1 -> 1.2 (01 Mar 2026)
[+] FAIL2BAN_ALLOWLIST is implemented (Issue #19386)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 1.0 -> 1.1 (01 Feb 2026)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.88 -> 1.0 (01 Jan 2026)
[=] Multiple updates and optimizations for regular static trails and the whitelist
[=] Major version is updated
[=] Project historical data trimmed, ~ 420Mb freed
- Version 0.87 -> 0.88 (01 Dec 2025)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.86 -> 0.87 (01 Nov 2025)
[=] "potential sql injection" heur is improved (Issue #19356)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.85 -> 0.86 (01 Oct 2025)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.84 -> 0.85 (01 Sep 2025)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.83 -> 0.84 (01 Aug 2025)
[=] Multiple improvements for Maltrail in Docker (Issues #19323, #19325)
[=] Multiple updates and optimizations for regular static trails and the whitelist
[-] Removing defunct blacklist (Issue #19339)
- Version 0.82 -> 0.83 (01 Jul 2025)
[!] Fix the bug of self-stored XSS in aliases (Issue #19321)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.81 -> 0.82 (01 Jun 2025)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.80 -> 0.81 (01 May 2025)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.79 -> 0.80 (01 Apr 2025)
[!] Fix the bug of using a custom path configuration file (Issue #19304)
[=] Multiple updates and optimizations for regular static trails and the whitelist
[-] Removing defunct blacklist (Issue #19305)
- Version 0.78 -> 0.79 (01 Mar 2025)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.77 -> 0.78 (01 Feb 2025)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.76 -> 0.77 (01 Jan 2025)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.75 -> 0.76 (01 Dec 2024)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.74 -> 0.75 (01 Nov 2024)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.73 -> 0.74 (01 Oct 2024)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.72 -> 0.73 (01 Sep 2024)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.71 -> 0.72 (01 Aug 2024)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.70 -> 0.71 (01 Jul 2024)
[=] Maltrail docker container run is improved (Issue #19260)
[=] php-inj detection is improved (Issue #19262)
[=] Python 3.12 compability is improved (Issue #19257)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.69 -> 0.70 (01 Jun 2024)
[=] cruzit feed URL changed (Issue #19253)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.68 -> 0.69 (01 May 2024)
[+] Support of simpleton IPv6 bogon address handling was added
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.68 -> 0.69 (01 May 2024)
[+] Support of simpleton IPv6 bogon address handling was added
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.67 -> 0.68 (01 Apr 2024)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.66 -> 0.67 (01 Mar 2024)
[=] Handling usage of pcapy lib instead of pcapy-ng is improved (Issue #19242)
[=] Fixed /server.py and /sensor.py restart in docker container (Issue #19243)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.65 -> 0.66 (01 Feb 2024)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.64 -> 0.65 (01 Jan 2024)
[+] Customisable blacklists via BLACKLIST option in /maltrail.conf file (Issue #19230)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.63 -> 0.64 (01 Dec 2023)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.62 -> 0.63 (01 Nov 2023)
[=] FAIL2BAN_REGEX and REMOTE_SEVERITY_REGEX options were updated to handle "potential iot-malware download" heur (Issue #19207)
[=] Abuseipdb feed was updated (Issue #19208)
[=] "potential remote code execution" heur for CVE-2016-0545 detection is updated (Issue #19210)
[=] "potential remote code execution" heur is updated for MacOS process list tracking in HTTP POST-req (Issue #19214)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.61 -> 0.62 (01 Oct 2023)
[=] Multiple updates and optimizations for regular static trails and the whitelist
[=] Updates for mass_scanner and worst_asns trails
- Version 0.60 -> 0.61 (01 Sep 2023)
[!] Workaround to have working searx server (Issue #19199)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.59 -> 0.60 (01 Aug 2023)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.58 -> 0.59 (01 Jul 2023)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.57 -> 0.58 (01 Jun 2023)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.56 -> 0.57 (01 May 2023)
[!] Fixed login page GUI issue for mobile devices (Issue #19153)
[!] Fixed incorrect parsing of ViriBack feed (Issue #19154)
[=] Added new descriptions in "Specific detections" Wiki chapter
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.55 -> 0.56 (01 Apr 2023)
[=] Minor update for /feeds/emergingthreatsdns.py (Issue #19147)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.54 -> 0.55 (01 Mar 2023)
[!] Fixed unauthenticated OS command injection vulnerability in http.py (Issue #19146)
[=] Minor update for _process_packet func in sensor (Issue #19129)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.53 -> 0.54 (01 Feb 2023)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.52 -> 0.53 (01 Jan 2023)
[-] Defunct 360-netlab feeds were deleted (Issue #19138)
[=] "potential data leakage" heur is improved
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.51 -> 0.52 (01 Dec 2022)
[=] "potential iot-malware download" heur is improved
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.50 -> 0.51 (01 Nov 2022)
[+] New Wiki pages are added
[!] Fixed deadlock of Docker output to stdout (Issue #19121)
[!] Definition of network interfaces is improved (Issue #19123)
[!] Fixed regex for /360bigviktor.py feed (Issue #19124)
[!] Fixed syscalls handling (Issue #19125)
[=] "potential remote code execution" heuristic is improved
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.49 -> 0.50 (01 Oct 2022)
[=] "potential remote code execution" heur for CVE-2022-30190 detection is updated
[=] "Maltrail detection nuances" wiki-page is updated
[=] "Trail classes" wiki-page is updated
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.48 -> 0.49 (01 Sep 2022)
[!] Fixed row rendering in UI (Issue #19109)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.47 -> 0.48 (01 Aug 2022)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.46 -> 0.47 (01 Jul 2022)
[+] "potential ssti injection" heuristic is added (CVE-2022-26134)
[=] "potential data leak" heuristic is improved
[=] "Trail-classes" wiki page is updated
[=] /requirements.txt file is updated (pcapy-ng)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.45 -> 0.46 (01 Jun 2022)
[+] New Wiki page is added
[=] "potential remote code execution" heuristic is improved (CVE-2022-1388)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.44 -> 0.45 (01 May 2022)
[+] systemd-based realization for Maltrail sensor.py, server.py and ipset/iptables ban-list (dedicated repo) have added
[+] New Wiki pages are added
[=] "potential remote code execution" heuristic is improved (detection for Java-related RCE stuff)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.43 -> 0.44 (01 Apr 2022)
[=] "potential remote code execution" heuristic is improved
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.42 -> 0.43 (01 Mar 2022)
[=] "potential remote code execution" heuristic is improved
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.41 -> 0.42 (01 Feb 2022)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.40 -> 0.41 (01 Jan 2022)
[+] "potential remote code execution" heuristic is extended for log4j/log4shell (CVE-2021-44228) vulnerability detection
[+] "generic_log4shell.txt" and "hacked_log4j.txt" trails were added for log4j/log4shell (CVE-2021-44228) vulnerability static detection
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.39 -> 0.40 (01 Dec 2021)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.38 -> 0.39 (01 Nov 2021)
[=] "potential directory traversal" heuristic is extended
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.37 -> 0.38 (03 Oct 2021)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.36 -> 0.37 (02 Sep 2021)
[=] Multiple updates and optimizations for regular static trails and the whitelist
- Version 0.35 -> 0.36 (02 Aug 2021)