Maltrail is a network traffic detection system that identifies communication with known malicious
infrastructure and reports selected traffic anomalies. It matches domains, URLs, IP addresses,
IP:port pairs, and User-Agent values observed on the network against a set of indicators called
trails.
A detection is recorded as a single event containing the source, destination, protocol, matched trail, classification, and trail source:
"2026-08-07 09:14:22.117034" gw 10.13.13.2 57809 1.1.1.1 53 UDP DNS malware.bakewithdavid.com "asyncrat (malware)" (static)
Maltrail is designed for indicator-based network monitoring. Its heuristic detections supplement trail matching, but it is not a replacement for endpoint telemetry or a general-purpose intrusion prevention system.
- A full trail build combining more than 3,000 bundled static files, 42 public-feed integrations, and optional operator-supplied trails.
- A multithreaded Rust sensor using libpcap, with optional Linux
PACKET_FANOUTcapture workers. - A Python server providing the reporting interface, event intake, and HTTP API.
- Plain-text custom trails and whitelists that can be reviewed and version-controlled.
- Heuristics for scanning, DNS exhaustion, DGA-like lookups, suspicious downloads, proxy probes, suspicious User-Agent values, and related network activity.
- Local event logging, remote Maltrail logging, CEF over syslog, and Logstash JSON output.
- Deployment validation with
maltrail-sensor -Tand optional Prometheus metrics.
- Architecture
- Performance
- Installation
- Configuration
- Trails
- Events and API
- Operations
- Documentation
- Contributing
- Project
Maltrail consists of two independent processes that may run on the same host or on separate hosts:
┌──────────┐ events (UDP or file) ┌──────────┐
│ sensor │ ───────────────────────► │ server │ ◄── browser
└──────────┘ └──────────┘
Rust Python
libpcap + PACKET_FANOUT reporting UI + API
trail matching + heuristics
The sensor captures traffic, performs trail matching and heuristic analysis, and produces events.
It can write events locally (LOG_DIR), send them to a remote Maltrail server (LOG_SERVER), or do
both. It can also emit CEF over syslog (SYSLOG_SERVER) and JSON to Logstash
(LOGSTASH_SERVER).
The server receives and stores remote events, serves locally available event logs, and provides the web interface and API.
Performance depends on processor, traffic composition, trail-set size, capture driver, and network interface. The figures below measure the sensor's packet-processing path in isolation; they are not end-to-end live-capture measurements.
Representative measurements on an AMD Ryzen 7 PRO 4750U with heuristics enabled and a 1.5 million-row trail set:
| Traffic | Time per packet |
|---|---|
| ICMP echo, 58 bytes | 101 ns |
| TCP SYN, 70 bytes | 302 ns |
| Bulk TLS, 1,473 bytes | 402 ns |
| DNS query with a warm cache, 93 bytes | 452 ns |
| Mixed traffic, 866-byte average | 552 ns |
| HTTP request, 169 bytes | 602 ns |
| DNS query with a unique name, 93 bytes | 1,102 ns |
Offline comparison runs using the same generated capture, configuration, and trail set measured a 14–37× lower steady-state per-packet cost than the retired Python sensor across the tested systems. The comparison tool reports whole-process time separately because trail loading dominates short replays. It also prints event counts; functional parity is tested independently by the parity corpus.
Run the comparison on the target system with:
python3 sensor/tools/bench_compare.py --packets 300000 \
--trails ~/.maltrail/trails.csv --repeat 3One capture worker is used by default. Additional workers can increase capture capacity, but Linux
flow hashing divides per-source state between workers and therefore reduces the sensitivity of some
scan heuristics. In the documented test, 91% of single-worker heuristic alerts remained with two
workers, 86% with four, and 65% with eight. Exact trail matching was unchanged. Increase
CAPTURE_FANOUT only when capture-drop metrics show that it is necessary.
Benchmark methodology, hardware results, profiler output, memory measurements, and live fanout
checks are documented in sensor/docs/REPORT.md.
The installer supports Debian, Ubuntu, Raspberry Pi OS, RHEL, Fedora, and openSUSE:
curl -fsSL https://raw.githubusercontent.com/stamparm/maltrail/master/install.sh | sudo shIt installs dependencies, creates a managed checkout under /opt/maltrail, verifies the checksum
of the prebuilt sensor, creates an unprivileged maltrail account, installs systemd units, prepares
the log and state directories, and starts the sensor and server. Re-running the installer upgrades
the managed checkout.
Review the script before running it with elevated privileges. From an existing checkout, the dry run shows the commands without changing the system:
sh install.sh --dry-runCommon installer options:
sh install.sh --role sensor # Install only the sensor
sh install.sh --ref 3.1.1 # Install a release tag instead of master
sh install.sh --no-service # Install without changing systemd
sh install.sh --dry-run # Print commands without applying them
sh install.sh --uninstall # Remove the managed installation; keep logs and stateThe dashboard is available at http://127.0.0.1:8338 after installation. Note that the shipped
HTTP_ADDRESS is 0.0.0.0, so it is reachable on every interface, not only loopback — and
the default credentials are admin / changeme!. Change USERS, and set HTTP_ADDRESS to
127.0.0.1 (or put the server behind a reverse proxy with TLS), before the host is on an
untrusted network.
The initial trail build can take several minutes. The sensor does not detect trail matches until a
valid trail set is available. The systemd unit runs the sensor's -T validation before startup so
that missing privileges, an unwritable log directory, or an invalid trail set causes startup to
fail visibly.
The installer test harness covers Ubuntu, Debian, Fedora, openSUSE, and Alpine containers. Alpine uses musl and does not use the prebuilt glibc sensor binary; build the sensor from source there.
The sensor requires Rust 1.74 or newer, libpcap development headers, and the system's capability tools. The server and trail updater require Python 3.6 or newer.
Install the distribution packages:
# Debian / Ubuntu / Raspberry Pi OS
sudo apt-get install cargo libpcap-dev libcap2-bin python3
# RHEL / Fedora
sudo dnf install cargo libpcap-devel libcap python3
# openSUSE / SLES
sudo zypper install cargo rust libpcap-devel libcap-progs python311Then build and validate the sensor:
git clone --depth 1 https://github.com/stamparm/maltrail.git
cd maltrail
cargo build --release --manifest-path sensor/Cargo.toml
sudo setcap cap_net_raw,cap_net_admin=eip \
sensor/target/release/maltrail-sensor
sudo install -d -o "$USER" -g "$(id -gn)" -m 750 /var/log/maltrail
sensor/target/release/maltrail-sensor -T
sensor/target/release/maltrail-sensorStart the server in another terminal or on another host:
python3 server.pyPrebuilt x86_64 and aarch64 sensor binaries are attached to current releases with SHA-256
checksums. They target glibc 2.28 and require libpcap at runtime. On musl-based systems such as
Alpine Linux, build from source.
The retired Python sensor is used only by comparison and parity tools. Those tools additionally
require pcapy-ng and the Python development headers described in
sensor/docs/INSTALL.md.
The supplied maltrail-server.service and maltrail-sensor.service units run both processes as the
unprivileged maltrail user. Systemd creates /var/log/maltrail and /var/lib/maltrail, restricts
filesystem access, and grants the sensor CAP_NET_RAW and CAP_NET_ADMIN.
The installer configures these units automatically. For an existing source installation, follow
the manual service procedure in sensor/docs/INSTALL.md.
Check service state and logs with:
systemctl status maltrail-sensor maltrail-server
journalctl -u maltrail-sensor -fStart the supplied Compose deployment with:
docker compose -f docker/docker-compose.yml up -dContainer configuration, storage, privileges, and health checks are documented in
docker/README.md.
Maltrail reads maltrail.conf, which contains separate [Sensor] and [Server] settings. The
installer places the managed configuration at /etc/maltrail.conf.
Frequently used sensor options include:
| Option | Purpose |
|---|---|
MONITOR_INTERFACE |
Capture interface or interfaces; any selects all supported interfaces |
CAPTURE_FILTER |
BPF capture filter |
CAPTURE_FANOUT |
Number of Linux capture sockets; defaults to one |
LOG_DIR |
Local event-log directory |
TRAILS_FILE |
Generated trail database |
LOG_SERVER |
Remote Maltrail event server |
SYSLOG_SERVER |
CEF syslog destination or destinations |
LOGSTASH_SERVER |
Logstash JSON destination or destinations |
STATS_ADDRESS |
Prometheus metrics listener; disabled unless configured |
UPDATE_PERIOD |
Trail refresh interval |
USER_WHITELIST |
Operator-managed indicators that should not alert |
CUSTOM_TRAILS_DIR |
Operator-managed trail directory |
PROCESS_COUNT applies to the retired Python sensor. Configure the Rust sensor's capture workers
with CAPTURE_FANOUT instead.
Run the deployment check after changing configuration:
sensor/target/release/maltrail-sensor -TThe check validates configuration, trails, whitelist entries, capture filter, privileges, log storage, update support, and worker settings. A successful check includes positive trail and whitelist counts rather than only confirming that files exist.
Trails are stored as plain-text indicators:
trails/static/malware/ malware-related static trails
trails/static/malicious/ malicious infrastructure
trails/static/suspicious/ suspicious infrastructure and behavior
trails/feeds/*.py public feed integrations
Add local indicators under CUSTOM_TRAILS_DIR. Add indicators that should never generate alerts
to USER_WHITELIST. Keeping custom data outside the managed checkout prevents upgrades from
overwriting it.
The updater rebuilds TRAILS_FILE from enabled feeds, bundled static trails, and custom trails. A
new file is published atomically only after a successful build. Empty or failed feeds are reported
so that a running deployment does not silently depend on stale or retired sources.
Trail contributions should include the indicator, classification, and a verifiable source. See Contributing before submitting a pull request.
Maltrail records one whitespace-separated event per detection, using CSV quoting where a value contains spaces:
"<time>" <sensor> <src_ip> <src_port> <dst_ip> <dst_port> <proto> <type> <trail> "<info>" <reference>
The type field identifies what matched, including DNS, IP, IPORT, URL, PATH, HTTP,
UA, PORT, and CERT. The info field contains the trail classification, and reference
identifies the static list, feed, custom source, or heuristic that produced it.
Use /check to query one domain, IP address, or URL:
curl 'http://127.0.0.1:8338/check?q=www.sub.evil.example'{
"query": "www.sub.evil.example",
"found": true,
"trail": "evil.example",
"info": "asyncrat (malware)",
"reference": "(static)"
}A subdomain lookup can match its listed parent. URL lookups check host/path before checking the
host alone. The server reads the memory-mapped trail database and observes trail updates without a
restart.
Public static and feed trails are available without authentication, consistent with the /trails
endpoint used by remote sensors. Custom trails require an authorized session; an unauthorized
custom-only lookup is reported as a miss. Event data remains authenticated.
Use maltrail-sensor -T as a deployment and configuration gate. The supplied systemd unit runs it
as ExecStartPre.
When STATS_ADDRESS is configured, monitor at least these Prometheus metrics:
| Metric | Operational meaning |
|---|---|
maltrail_up == 0 |
No capture worker is running |
Increasing maltrail_capture_dropped_total |
The capture ring is dropping packets |
Increasing maltrail_local_log_errors_total |
Events were produced but could not be written locally |
Increasing maltrail_remote_log_errors_total |
Events could not be delivered to a remote sink; with DISABLE_LOCAL_LOG_STORAGE they are lost |
maltrail_trail_generation not advancing |
The active trail set is not being refreshed |
maltrail_log_dir_free_bytes |
Remaining capacity for local event storage |
Increasing maltrail_state_saturations_total |
A heuristic state limit was reached |
Increasing maltrail_throttle_evictions_total |
The event-throttle table is at its cap, so events are aggregated earlier than configured |
State saturation affects the corresponding heuristic; exact trail matching remains active.
Send SIGHUP or use systemctl reload maltrail-sensor to request a trail reload. Trail files
updated by another process are detected automatically and published to workers without restarting
the sensor.
The condensed observable store (USE_CONDENSED_STORAGE, meta.sqlite) supports the server's
novelty and retro-hunt views. Compatibility with the retired sensor is documented in
sensor/docs/COMPATIBILITY.md.
Maltrail does not rotate or delete event logs. Operators are responsible for defining retention, archival, and deletion according to storage requirements and organizational policy.
Recommended practices:
- Send the durable event copy to a remote Maltrail server or SIEM with
LOG_SERVER,SYSLOG_SERVER, orLOGSTASH_SERVER. - Alert on
maltrail_log_dir_free_byteswith enough headroom for the expected event rate. - Rotate, archive, or remove local daily logs using external tooling.
- Keep files needed by the reporting interface uncompressed in
LOG_DIR; archive compressed files elsewhere.
When the log filesystem is full, the sensor cannot append events. Event logs may also contain IP addresses and domains that are regulated as personal data in some jurisdictions; retention policy should account for the applicable requirements.
| Document | Contents |
|---|---|
sensor/docs/INSTALL.md |
Installation, privileges, configuration, and troubleshooting |
sensor/docs/ARCHITECTURE.md |
Sensor internals and data flow |
sensor/docs/COMPATIBILITY.md |
Deliberate differences from the retired Python sensor |
sensor/docs/REPORT.md |
Measurements, profiles, and test results |
sensor/docs/ROADMAP.md |
Open sensor work |
old/README.md |
Retired Python sensor, retained as a parity oracle |
Trail additions, feed maintenance, bug reports, documentation, and sensor improvements are welcome. Trail submissions should include a reliable source and should use the narrowest appropriate classification.
Run the relevant checks before submitting code. The complete sensor gate is:
bash sensor/tools/check.shIt runs formatting, Clippy with warnings denied, debug and release tests, and parity replay against the retired Python sensor. Run the Python server suite with:
bash tests/run.sh python3Maltrail is distributed under the MIT License. See LICENSE.
- Miroslav Stampar (@stamparm)
- Mikhail Kasimov (@MikhailKasimov)
- 47th TF-CSIRT Meeting, Prague, 2016 (slides)
- Detect attacks on your network with Maltrail, Linux Magazine, 2022 (article)
- Best Cyber Threat Intelligence Feeds, Silent Push, 2022 (review)
- Research on Network Malicious Traffic Detection System Based on Maltrail, Nanotechnology Perceptions, 2024 (paper)
A domain-only list derived from trails/static/malware is published at
maltrail-malware-domains.txt.
It can be used as an input to DNS filtering systems, but operators should review and test it before
enabling blocking. Threat-intelligence lists can contain false positives or indicators that are not
appropriate for every environment.
- FreeBSD Port
- OPNsense Gateway Plugin
- D4 Project
- BlackArch Linux
- Validin
- Maltrail Add-on for Splunk
- Maltrail decoder and rules for Wazuh
- GScan (trails only)
- MalwareWorld (trails only)
- oisd domain blocklist (trails only)
- NextDNS (trails only)
- NoTracking (trails only)
- OWASP Mobile Audit (trails only)
- Mobile Security Framework MobSF (trails only)
- pfBlockerNG-devel (trails only)
- Sansec eComscan (trails only)
- Palo Alto Networks Cortex XSOAR (trail connector)
- Thomas Kristner
- Eduardo Arcusa Les
- James Lay
- Ladislav Baco (@laciKE)
- John Kristoff (@jtkdpu)
- Michael Münz (@mimugmail)
- David Brush
- @Godwottery
- Chris Wild (@briskets)
- Keith Irwin (@ki9us)
- Simon Szustkowski (@simonszu)
