Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions lib/cuckoo/common/dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@
# This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
# See the file 'docs/LICENSE' for copying permission.

import logging
import select
import socket
import threading
from typing import Callable

try:
import requests

HAVE_REQUESTS = True
DOH_SESSION = requests.Session()
except Exception:
HAVE_REQUESTS = False
DOH_SESSION = None
DOH_SESSION = None

try:
import pycares

HAVE_CARES = True
except Exception:
HAVE_CARES = False

log = logging.getLogger(__name__)

# try:
# import gevent, gevent.socket
# HAVE_GEVENT = True
Expand Down Expand Up @@ -137,8 +150,84 @@ def resolve_gevent_real(name):
"""


# DNS-over-HTTPS
# Supports Google (/resolve JSON API) and other application/dns-json
# compatible endpoints (for example, Cloudflare-style JSON APIs).
# Default: Google DNS. Configurable via set_doh_url().
DOH_URL = "https://dns.google/resolve"
USE_DOH = False

# Expected DNS response type numbers for rdtype validation
_RDTYPE_MAP = {"A": 1, "AAAA": 28, "PTR": 12, "CNAME": 5, "MX": 15, "TXT": 16, "NS": 2, "SOA": 6}


def set_doh(enabled: bool):
global USE_DOH
USE_DOH = enabled


def set_doh_url(url: str):
global DOH_URL
if url:
if not url.startswith("https://"):
log.warning("DoH URL %s does not use HTTPS — DNS queries will not be encrypted", url)
DOH_URL = url.rstrip("/")


def resolve_doh(name: str, rdtype: str = "A") -> str:
"""Resolve a DNS name using DNS-over-HTTPS (JSON API).

Compatible with Google (/resolve), Cloudflare (/dns-query), and other
providers that support the application/dns-json content type.

Uses a persistent requests.Session for connection pooling.
"""
if not HAVE_REQUESTS or DOH_SESSION is None:
if rdtype == "A":
log.warning("requests library not available for DoH, falling back to system DNS")
return resolve_thread(name)
log.warning(
"requests library not available for DoH, no system DNS fallback for %s queries",
rdtype,
)
return DNS_TIMEOUT_VALUE
try:
expected_type = _RDTYPE_MAP.get(rdtype.upper())
resp = DOH_SESSION.get(
DOH_URL,
params={"name": name, "type": rdtype},
headers={"Accept": "application/dns-json"},
timeout=DNS_TIMEOUT,
)
if resp.status_code != 200:
log.debug("DoH request for %s returned HTTP %d", name, resp.status_code)
return DNS_TIMEOUT_VALUE
data = resp.json()
for answer in data.get("Answer", []):
answer_type = answer.get("type")
# If we know the expected type, only return matching records
if expected_type and answer_type == expected_type:
result = answer["data"]
if answer_type == 12: # PTR — strip trailing dot
result = result.rstrip(".")
return result
# Fallback for unknown rdtype: return first A/AAAA/PTR
if not expected_type and answer_type in (1, 12, 28):
result = answer["data"]
if answer_type == 12:
result = result.rstrip(".")
return result
except requests.RequestException as e:
log.debug("DoH resolution failed for %s: %s", name, e)
except (ValueError, KeyError) as e:
log.debug("DoH response parse error for %s: %s", name, e)
return DNS_TIMEOUT_VALUE


# choose resolver automatically
def resolve(name: str) -> str:
if USE_DOH:
return resolve_doh(name)
if HAVE_CARES:
return resolve_cares(name)
# elif HAVE_GEVENT:
Expand Down
21 changes: 17 additions & 4 deletions modules/processing/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from data.safelist.domains import domain_passlist_re
from lib.cuckoo.common.abstracts import Processing
from lib.cuckoo.common.config import Config
from lib.cuckoo.common.dns import resolve
from lib.cuckoo.common.dns import resolve, resolve_doh, set_doh, set_doh_url
from lib.cuckoo.common.exceptions import CuckooProcessingError
from lib.cuckoo.common.irc import ircMessage
from lib.cuckoo.common.network_utils import _norm_domain
Expand Down Expand Up @@ -98,6 +98,13 @@
enabled_passlist = proc_cfg.network.dnswhitelist
passlist_file = proc_cfg.network.dnswhitelist_file

# Enable DNS-over-HTTPS if configured
if getattr(cfg.processing, "dns_over_https", False):
set_doh(True)
doh_url = getattr(cfg.processing, "doh_url", "")
if doh_url:
set_doh_url(doh_url)

enabled_ip_passlist = proc_cfg.network.ipwhitelist
ip_passlist_file = proc_cfg.network.ipwhitelist_file

Expand Down Expand Up @@ -320,7 +327,8 @@ def _add_hosts(self, connection):
def _enrich_hosts(self, unique_hosts):
enriched_hosts = []

if cfg.processing.reverse_dns:
use_doh = getattr(cfg.processing, "dns_over_https", False)
if cfg.processing.reverse_dns and not use_doh:
d = dns.resolver.Resolver()
d.timeout = 5.0
d.lifetime = 5.0
Expand All @@ -330,8 +338,13 @@ def _enrich_hosts(self, unique_hosts):
inaddrarpa = ""
hostname = ""
if cfg.processing.reverse_dns:
with suppress(Exception):
inaddrarpa = d.query(from_address(ip), "PTR").rrset[0].to_text()
if use_doh:
with suppress(Exception):
ptr_name = str(from_address(ip))
inaddrarpa = resolve_doh(ptr_name, rdtype="PTR")
else:
with suppress(Exception):
inaddrarpa = d.query(from_address(ip), "PTR").rrset[0].to_text().rstrip(".")
for request in self.dns_requests.values():
for answer in request["answers"]:
if answer["data"] == ip:
Expand Down
Loading