diff --git a/.tests/adguardhome-dot-errors/adguardhome-dot-errors.log b/.tests/adguardhome-dot-errors/adguardhome-dot-errors.log new file mode 100644 index 00000000000..80a2a69510d --- /dev/null +++ b/.tests/adguardhome-dot-errors/adguardhome-dot-errors.log @@ -0,0 +1,6 @@ +2026/04/04 20:41:41.673779 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->192.168.1.50:59208: read: connection reset by peer" +2026/04/04 20:41:42.123456 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->192.168.1.50:59209: read: connection reset by peer" +2026/04/04 20:41:43.234567 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->192.168.1.50:59210: read: connection reset by peer" +2026/04/04 20:41:44.345678 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->192.168.1.50:59211: read: connection reset by peer" +2026/04/04 20:41:45.456789 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->192.168.1.50:59212: read: connection reset by peer" +2026/04/04 20:41:46.567890 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->192.168.1.50:59213: read: connection reset by peer" diff --git a/.tests/adguardhome-dot-errors/config.yaml b/.tests/adguardhome-dot-errors/config.yaml new file mode 100644 index 00000000000..3e05f1cbc2f --- /dev/null +++ b/.tests/adguardhome-dot-errors/config.yaml @@ -0,0 +1,7 @@ +parsers: + - ./parsers/s01-parse/PumbaLP/adguardhome-dot-errors.yaml +scenarios: + - ./scenarios/PumbaLP/adguardhome-dot-scan.yaml +postoverflows: [] +log_file: adguardhome-dot-errors.log +log_type: adguardhome diff --git a/.tests/adguardhome-dot-errors/parser.assert b/.tests/adguardhome-dot-errors/parser.assert new file mode 100644 index 00000000000..cde4e7a6e35 --- /dev/null +++ b/.tests/adguardhome-dot-errors/parser.assert @@ -0,0 +1,4 @@ +results["s01-parse"]["PumbaLP/adguardhome-dot-errors"][0].Success == true +results["s01-parse"]["PumbaLP/adguardhome-dot-errors"][0].Evt.Meta["source_ip"] == "192.168.1.50" +results["s01-parse"]["PumbaLP/adguardhome-dot-errors"][0].Evt.Meta["log_type"] == "dot_connection_reset" +results["s01-parse"]["PumbaLP/adguardhome-dot-errors"][0].Evt.Meta["service"] == "adguardhome_dot" diff --git a/.tests/adguardhome-dot-errors/scenario.assert b/.tests/adguardhome-dot-errors/scenario.assert new file mode 100644 index 00000000000..03455618a29 --- /dev/null +++ b/.tests/adguardhome-dot-errors/scenario.assert @@ -0,0 +1 @@ +len(results) == 0 diff --git a/.tests/adguardhome-dot-scan/adguardhome-dot-scan.log b/.tests/adguardhome-dot-scan/adguardhome-dot-scan.log new file mode 100644 index 00000000000..469eee12f69 --- /dev/null +++ b/.tests/adguardhome-dot-scan/adguardhome-dot-scan.log @@ -0,0 +1,12 @@ +2026/04/04 20:41:41.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59201: read: connection reset by peer" +2026/04/04 20:41:42.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59202: read: connection reset by peer" +2026/04/04 20:41:43.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59203: read: connection reset by peer" +2026/04/04 20:41:44.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59204: read: connection reset by peer" +2026/04/04 20:41:45.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59205: read: connection reset by peer" +2026/04/04 20:41:46.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59206: read: connection reset by peer" +2026/04/04 20:41:41.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59201: read: connection reset by peer" +2026/04/04 20:41:42.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59202: read: connection reset by peer" +2026/04/04 20:41:43.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59203: read: connection reset by peer" +2026/04/04 20:41:44.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59204: read: connection reset by peer" +2026/04/04 20:41:45.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59205: read: connection reset by peer" +2026/04/04 20:41:46.000000 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->1.2.3.4:59206: read: connection reset by peer" diff --git a/.tests/adguardhome-dot-scan/config.yaml b/.tests/adguardhome-dot-scan/config.yaml new file mode 100644 index 00000000000..ddae4ce860c --- /dev/null +++ b/.tests/adguardhome-dot-scan/config.yaml @@ -0,0 +1,7 @@ +parsers: + - ./parsers/s01-parse/PumbaLP/adguardhome-dot-errors.yaml +scenarios: + - ./scenarios/PumbaLP/adguardhome-dot-scan.yaml +postoverflows: [] +log_file: adguardhome-dot-scan.log +log_type: adguardhome diff --git a/.tests/adguardhome-dot-scan/parser.assert b/.tests/adguardhome-dot-scan/parser.assert new file mode 100644 index 00000000000..9ddd16ce2fd --- /dev/null +++ b/.tests/adguardhome-dot-scan/parser.assert @@ -0,0 +1,3 @@ +len(results["s01-parse"]["PumbaLP/adguardhome-dot-errors"]) == 6 +results["s01-parse"]["PumbaLP/adguardhome-dot-errors"][0].Success == true +results["s01-parse"]["PumbaLP/adguardhome-dot-errors"][0].Evt.Meta["source_ip"] == "192.0.2.42" diff --git a/.tests/adguardhome-dot-scan/scenario.assert b/.tests/adguardhome-dot-scan/scenario.assert new file mode 100644 index 00000000000..46a0769b452 --- /dev/null +++ b/.tests/adguardhome-dot-scan/scenario.assert @@ -0,0 +1,3 @@ +len(results) == 1 +results[0].Overflow.Alert.Source.IP == "192.0.2.42" +results[0].Overflow.Alert.Scenario == "PumbaLP/adguardhome-dot-scan" diff --git a/collections/PumbaLP/adguardhome-dot.yaml b/collections/PumbaLP/adguardhome-dot.yaml new file mode 100644 index 00000000000..d7e83345535 --- /dev/null +++ b/collections/PumbaLP/adguardhome-dot.yaml @@ -0,0 +1,6 @@ +name: PumbaLP/adguardhome-dot +description: "Detect and block DoT (DNS-over-TLS, port 853) scanners targeting AdGuard Home" +parsers: + - PumbaLP/adguardhome-dot-errors +scenarios: + - PumbaLP/adguardhome-dot-scan diff --git a/parsers/s01-parse/PumbaLP/adguardhome-dot-errors.md b/parsers/s01-parse/PumbaLP/adguardhome-dot-errors.md new file mode 100644 index 00000000000..ed50f7122fe --- /dev/null +++ b/parsers/s01-parse/PumbaLP/adguardhome-dot-errors.md @@ -0,0 +1,40 @@ +# adguardhome-dot-errors + +Parser for AdGuard Home DoT (DNS-over-TLS, port 853) connection reset errors. + +## Description + +AdGuard Home logs connection reset errors when a client connects to port 853 (DoT) but does not complete a proper TLS handshake. This is a common indicator of port scanners and probers. + +This parser extracts the remote IP address and sets the `dot_connection_reset` log type for use by the `PumbaLP/adguardhome-dot-scan` scenario. + +## Matched log example + +```text +2026/04/04 20:41:41.673779 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->192.168.1.50:59208: read: connection reset by peer" +``` +## Limitations + +- Only `connection reset by peer` errors contain a remote IP and are actionable. +- Other DoT errors (TLS version mismatch, cipher suite errors, unexpected EOF) do not contain a remote IP. +- DoQ (port 8853/udp) and direct DoH (port 443) errors also do not contain remote IPs. +- DoH via a reverse proxy (e.g. Nginx) is covered by `crowdsecurity/nginx-logs`. + +## Acquisition + +```yaml +source: docker +container_name: + - adguardhome +labels: + type: adguardhome +``` + +Or with log file: + +```yaml +filenames: + - /var/log/AdGuardHome.log +labels: + type: adguardhome +``` diff --git a/parsers/s01-parse/PumbaLP/adguardhome-dot-errors.yaml b/parsers/s01-parse/PumbaLP/adguardhome-dot-errors.yaml new file mode 100644 index 00000000000..55a573cfe56 --- /dev/null +++ b/parsers/s01-parse/PumbaLP/adguardhome-dot-errors.yaml @@ -0,0 +1,36 @@ +name: PumbaLP/adguardhome-dot-errors +description: "Parse AdGuard Home DoT (DNS-over-TLS, port 853) connection reset errors to detect port scanners and probers" +# +# Matches AdGuard Home log lines containing a remote IP address: +# 2026/04/04 20:41:41.673779 [error] dnsproxy: reading msg proto=tcp err="reading len: read tcp 172.20.2.2:853->85.217.140.42:59208: read: connection reset by peer" +# +# Note: Other DoT error types (TLS version mismatch, cipher suite errors, +# unexpected EOF, bad DNS header) do not contain a remote IP and are +# therefore not actionable by this parser. +# +# Note: DoQ (port 8853/udp) and direct DoH (port 443) errors also do not +# contain remote IPs in AdGuard Home logs and cannot be blocked this way. +# DoH via a reverse proxy (e.g. Nginx) is covered by crowdsecurity/nginx-logs. +# +# Acquisition example: +# source: docker +# container_name: +# - adguardhome +# labels: +# type: adguardhome +# +filter: "evt.Line.Labels.type == 'adguardhome'" +nodes: + - grok: + pattern: '%{YEAR:year}/%{MONTHNUM:month}/%{MONTHDAY:day} %{TIME:time} \[error\] dnsproxy: reading msg proto=tcp err="reading len: read tcp [^>]+->%{IP:remote_ip}:%{INT}: read: connection reset by peer"' + apply_on: message + onsuccess: next_stage + statics: + - meta: source_ip + expression: evt.Parsed.remote_ip + - meta: service + value: adguardhome_dot + - meta: log_type + value: dot_connection_reset + - target: evt.StrTime + expression: evt.Parsed.year + "/" + evt.Parsed.month + "/" + evt.Parsed.day + " " + evt.Parsed.time diff --git a/scenarios/PumbaLP/adguardhome-dot-scan.md b/scenarios/PumbaLP/adguardhome-dot-scan.md new file mode 100644 index 00000000000..0bc45b3c360 --- /dev/null +++ b/scenarios/PumbaLP/adguardhome-dot-scan.md @@ -0,0 +1,22 @@ +# adguardhome-dot-scan + +Detect DoT (DNS-over-TLS, port 853) scanners and probers targeting AdGuard Home. + +## Description + +A legitimate DNS-over-TLS client connects cleanly with a proper TLS handshake. Repeated connection resets from the same IP indicate a scanner or prober testing port 853 without proper TLS support. + +Triggers a ban after **5 connection resets within 10 minutes** from the same IP. + +## Requirements + +Requires the `PumbaLP/adguardhome-dot-errors` parser. + +## Tested against + +- AdGuard Home v0.107.x +- Confirmed working in production (blocked real scanners) + +## MITRE ATT&CK + +- T1046 – Network Service Discovery diff --git a/scenarios/PumbaLP/adguardhome-dot-scan.yaml b/scenarios/PumbaLP/adguardhome-dot-scan.yaml new file mode 100644 index 00000000000..690a4316ea4 --- /dev/null +++ b/scenarios/PumbaLP/adguardhome-dot-scan.yaml @@ -0,0 +1,25 @@ +type: leaky +name: PumbaLP/adguardhome-dot-scan +description: "Detect DoT (DNS-over-TLS, port 853) scanners and probers via repeated connection resets on AdGuard Home" +# +# A legitimate DNS-over-TLS client connects cleanly with a proper TLS handshake. +# Repeated connection resets from the same IP indicate a scanner or prober +# testing port 853 without proper TLS support. +# +# Capacity: 5 resets within 10 minutes triggers a ban. +# Tested against AdGuard Home v0.107.x +# +filter: "evt.Meta.log_type == 'dot_connection_reset' && evt.Meta.service == 'adguardhome_dot'" +groupby: evt.Meta.source_ip +capacity: 5 +leakspeed: "10m" +blackhole: 5m +labels: + service: adguardhome + type: scan + spoofable: 0 + confidence: 3 + classification: + - attack.T1046 + behavior: "network:scan" + remediation: true