Skip to content

DNS Listeners

DNS C2 uses DNS queries and responses as a covert data channel. DNS traffic is rarely blocked by firewalls and is often overlooked by network monitoring, making it an ideal backup transport for restrictive environments. The trade-off is very low bandwidth compared to HTTP/HTTPS -- DNS is best suited for long-term persistence, initial staging, and environments where HTTP egress is blocked.


Architecture

DNS C2 encodes beacon data in DNS subdomain labels and decodes server responses from DNS records. The relay runs a DNS server that processes these queries and interfaces with the same beacon registry and task queue used by HTTP listeners.

sequenceDiagram
    participant Implant as Implant (Windows)
    participant DNS as DNS Server (Relay)
    participant C2 as C2 Handler (Relay)
    participant Backend as Backend API Server

    Implant->>DNS: DNS query (beacon.c2.example.com)
    DNS->>C2: Decode query, register beacon
    C2->>Backend: WebSocket event (beacon check-in)
    Backend-->>C2: WebSocket command (task queue)
    C2-->>DNS: Encode response
    DNS-->>Implant: A/AAAA/TXT record response

The DNS server shares the beacon registry and task queue with any HTTP/HTTPS listeners on the same relay, so beacons can be managed through a unified interface regardless of their transport.


DNS Infrastructure Setup

Before creating a DNS listener, you need a domain with DNS records pointing to the relay.

Requirements

  1. A domain you control (or a subdomain delegated to you)
  2. An NS record pointing the C2 subdomain to the relay's public IP
  3. Firewall rules allowing inbound UDP port 53 to the relay

Step-by-Step Zone Setup

1. Choose a C2 domain

Use a subdomain dedicated to C2 traffic. Avoid using the root domain since NS delegation applies to the entire zone.

c2.example.com

2. Create NS record

At your DNS registrar or zone editor, create an NS record that delegates the C2 subdomain to the relay's public IP:

c2.example.com.    IN    NS    ns1.example.com.
ns1.example.com.   IN    A     <RELAY_PUBLIC_IP>

3. Verify delegation

dig NS c2.example.com

The answer section should show your NS record pointing to the relay IP.

DNS propagation

DNS changes can take up to 48 hours to propagate globally. In practice, most changes take effect within 15-30 minutes. Use dig with a specific resolver to check propagation: dig @8.8.8.8 NS c2.example.com.

Lab environments

For lab testing, you do not need public DNS. Configure the implant's DNS resolver to point directly at the relay IP, or use a local DNS server (e.g., dnsmasq) to resolve the C2 domain. Set the dns_resolver field on the listener to bypass the system resolver entirely.


Creating a DNS Listener

DNS listeners use the same API as HTTP listeners but require DNS-specific fields.

curl -s -X POST https://stentor.app/api/v1/listeners \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "DNS Covert",
    "relay_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "type": "dns",
    "port": 53,
    "dns_domain": "c2.example.com",
    "dns_ttl": 60,
    "dns_idle": "0.0.0.0",
    "dns_max_txt": 252
  }' | jq

Then start the listener:

curl -s -X POST https://stentor.app/api/v1/listeners/$LISTENER_ID/start \
  -H "Authorization: Bearer $TOKEN" | jq '.status'

Port 53 requires root

UDP port 53 is a privileged port. The relay process must run as root (or have CAP_NET_BIND_SERVICE) to bind to port 53. For non-root testing, use port 5353 and configure the implant's resolver accordingly.


Configuration Options

The table below lists all fields relevant to DNS listeners.

Field Type Default Description
name string -- Required. Display name for the listener.
type string -- Required. Must be "dns".
relay_id string -- Required. UUID of the relay hosting this listener.
port int 53 Required. UDP port for the DNS server.
dns_domain string -- Required. C2 domain suffix (e.g., "c2.example.com"). All queries must be subdomains of this domain.
dns_ttl int 60 Time-to-live for DNS responses in seconds. Lower values mean faster task delivery but more DNS traffic.
dns_idle string "0.0.0.0" IP address returned when the beacon has no pending tasks. The idle response tells the implant "nothing to do."
dns_max_txt int 252 Maximum TXT record data length per response in bytes. Higher values increase bandwidth but may be split by resolvers.
dns_resolver string null Custom DNS resolver address (e.g., "8.8.8.8:53"). Overrides the system resolver for beacon DNS queries. See DNS Resolver Override.
guardrails JSON null Beacon filtering rules. Same schema as HTTP listeners -- see the HTTP/HTTPS Guardrails section.

Data Channel Modes

Stentor DNS C2 supports three data channel modes that control which DNS record type is used for server-to-beacon communication. Each mode offers different bandwidth and stealth trade-offs.

Mode Comparison

Mode Record Type Bandwidth Stealth Compatibility
dns A (IPv4) ~4 bytes/response High Universal
dns6 AAAA (IPv6) ~16 bytes/response Medium Most networks
dns-txt TXT ~252 bytes/response Lower May be logged

dns (A Records) -- Default

The default mode encodes beacon data in DNS subdomain labels and returns server data in A records (IPv4 addresses). Each A record carries 4 bytes of data.

  • Bandwidth: Lowest (~4 bytes per response)
  • Stealth: Highest -- A record queries are the most common DNS query type
  • Compatibility: Universal -- works on every network that allows DNS

dns6 (AAAA Records)

Same encoding as dns mode but uses AAAA records (IPv6 addresses). Each AAAA record carries 16 bytes of data -- four times the bandwidth of A records.

  • Bandwidth: Medium (~16 bytes per response)
  • Stealth: Medium -- AAAA queries are common on modern networks but less so than A queries
  • Compatibility: Most networks -- some legacy networks or restrictive proxies may filter AAAA queries

dns-txt (TXT Records)

Uses TXT records for server responses, providing the highest bandwidth. The dns_max_txt configuration controls the maximum TXT record length (default 252 bytes).

  • Bandwidth: Highest (~252 bytes per response)
  • Stealth: Lower -- large TXT record responses are more conspicuous to DNS monitoring
  • Compatibility: Wide -- but some corporate DNS proxies log or inspect TXT records

Switching Modes at Runtime

Operators can switch the data channel mode for a live beacon through the beacon console:

mode dns       # Switch to A record mode (default)
mode dns6      # Switch to AAAA record mode
mode dns-txt   # Switch to TXT record mode

Start low, scale up

Begin with dns (A records) for initial check-in and staging. Once the beacon is established and you need more bandwidth (e.g., for downloading files), switch to dns-txt mode. Switch back to dns when idle to minimize detection risk.


DNS Task Pipeline

This section documents the complete backend task flow -- from initial beacon check-in through task delivery to result submission. Understanding this pipeline is essential for troubleshooting DNS C2 communications and configuring P2P child beacon routing.

Beacon Check-in Flow

When an implant first connects (or reconnects) over DNS, it initiates a check-in by sending a TXT query with a Base32-encoded JSON payload in the subdomain labels. The relay's DNS handler (handleBeaconCheckin) processes this as follows:

  1. Decode request: The handler joins subdomain labels and Base32-decodes them into a JSON CheckinRequest containing: hostname, username, ip, os, arch, pid, and beacon_id
  2. Guardrails evaluation: If guardrails are configured on the listener, the handler evaluates the beacon's IP, hostname, and username against the ruleset. Non-matching beacons receive RCODE_REFUSED and a guardrail_violation event is fired over WebSocket
  3. First check-in (empty beacon_id): The handler creates a new beacon via the shared BeaconRegistry and fires a beacon_new WebSocket event to the backend server
  4. Subsequent check-in (existing beacon_id): The handler updates the existing beacon record. If the beacon was removed during stale-cleaning, it is re-registered. A beacon_checkin WebSocket event is fired
  5. Response: The handler returns a Base32-encoded JSON response as a TXT record containing beacon_id, sleep interval, and jitter percentage
sequenceDiagram
    participant Implant as Implant (Windows)
    participant DNS as DNS Server (Relay)
    participant Registry as BeaconRegistry
    participant Backend as Backend API Server

    Implant->>DNS: TXT query (beacon.{base32_checkin}.c2.example.com)
    DNS->>DNS: Base32-decode + JSON parse CheckinRequest
    DNS->>DNS: Evaluate guardrails (if configured)
    DNS->>Registry: Register or update beacon
    DNS->>Backend: WebSocket event (beacon_new / beacon_checkin)
    DNS->>Implant: TXT response (Base32 JSON: {beacon_id, sleep, jitter})

Shared registry

The BeaconRegistry is shared between DNS and HTTP/HTTPS listeners on the same relay. A beacon that checks in over DNS is visible to all listeners, and tasks enqueued by any listener can be delivered over any transport.

Task Delivery Flow

After check-in, the beacon periodically polls for tasks. The DNS handler supports three polling modes depending on the DNS record type used:

TXT mode (handleTaskPoll): The beacon sends a TXT query with its Base32-encoded beacon ID. The handler processes this through a two-tier dequeue:

  1. Decode beacon ID: Base32-decode the subdomain labels and parse the UUID
  2. Validate beacon: Confirm the beacon exists in the registry and update its heartbeat timestamp
  3. Local queue check: Dequeue the next task from the local TaskQueue (shared with HTTP listeners)
  4. Backend proxy fallback: If the local queue is empty AND a backend proxy is configured, the handler calls GET /api/v1/c2/task?beacon_id={id} on the backend server to dequeue any server-enqueued tasks
  5. Response: Full task as Base32-encoded JSON containing task_id, type, base64-encoded data, and optional relay_for field
  6. Task status: The dequeued task is marked as dispatched

A mode (handleTaskPollA): Returns 4 bytes of task data encoded as an IPv4 address. When no tasks are pending, the handler returns the dns_idle IP (default 0.0.0.0).

AAAA mode (handleTaskPollAAAA): Returns 16 bytes of task data encoded as an IPv6 address. When no tasks are pending, the handler returns :: (all-zeros IPv6).

sequenceDiagram
    participant Beacon as Beacon
    participant DNS as DNS Handler
    participant Queue as TaskQueue (Local)
    participant Backend as Backend API

    Beacon->>DNS: TXT/A/AAAA query ({base32_beacon_id}.c2.example.com)
    DNS->>Queue: Dequeue(beacon_id)
    alt Task in local queue
        Queue-->>DNS: Task
    else Local queue empty
        DNS->>Backend: GET /api/v1/c2/task?beacon_id={id}
        Backend-->>DNS: Task (with optional relay_for)
    end
    DNS->>Beacon: Response (task data or idle)

Backend proxy configuration

The backend proxy is enabled by calling SetBackendProxy(backendURL, relaySecret) on the DNS handler. This is required for server-enqueued tasks to reach DNS beacons -- without it, only tasks enqueued directly on the relay's local queue are delivered.

P2P Aggregation Through DNS Parent

When a P2P child beacon (e.g., an SMB or TCP bind beacon) has no direct C2 channel, it relies on its parent beacon to relay tasks. The DNS handler's backend proxy mechanism enables this through the relay_for field:

  1. An operator enqueues a task for a P2P child beacon on the server
  2. The DNS parent beacon polls the backend via proxyTaskPollToBackend()
  3. The backend returns the task with the relay_for field set to the child beacon's ID
  4. The DNS handler includes relay_for in the TXT task response to the parent beacon
  5. The parent beacon recognizes the relay_for field and relays the task to the child over its P2P link (SMB named pipe, TCP bind socket)
  6. The child beacon executes the task and returns the result through the parent
  7. The parent forwards the result back to the server
sequenceDiagram
    participant Server as Backend Server
    participant DNS as DNS Parent Beacon
    participant SMB as SMB Child Beacon

    Server->>DNS: Task poll response (relay_for=child_id)
    DNS->>SMB: Relay task over P2P link (named pipe / TCP)
    SMB->>SMB: Execute task
    SMB->>DNS: Return task result over P2P link
    DNS->>Server: Forward result (task_complete event)

Parent beacon must be alive

P2P child tasks are only delivered when the parent beacon actively polls. If the DNS parent beacon goes offline or its sleep interval is too long, child tasks queue up on the server until the parent resumes polling.

Large Result Chunking

Task results that exceed DNS label size limits are split into chunks and submitted across multiple DNS queries. The handler's handleResultSubmission() and processCompleteResult() manage this process:

  1. The implant splits the task result into chunks that fit within DNS subdomain labels
  2. Each chunk is sent as an A record query with the following label format:
{base32_chunk_data}.{chunk_index}.{total_chunks}.{task_id}.result.c2.example.com
  1. The handler stores each chunk in a resultChunks map keyed by task_id
  2. Each chunk is acknowledged with an A record response: 1.0.0.{chunk_index} (success) or 1.0.0.255 (error)
  3. When all chunks are received (len(chunks) == total_chunks), processCompleteResult() reassembles them in order by iterating chunk indices 0 through N-1
  4. The reassembled data is parsed as JSON containing beacon_id, success, output, and error fields
  5. The complete result is forwarded to the backend via a task_complete WebSocket event

Chunk acknowledgment

The per-chunk acknowledgment (1.0.0.{chunk_index}) allows the implant to detect lost chunks and retransmit. The error response (1.0.0.255) indicates a decode or parse failure on that specific chunk.

DNS Transport Mode Routing

The ServeDNS entry point routes incoming queries based on the DNS record type requested. This determines which handler processes the query:

Record Type Handler Routes To Data Capacity
TXT handleTXTQuery() Beacon check-in (QueryTypeBeacon) or task poll (QueryTypeTask) Full JSON (up to dns_max_txt bytes)
A handleAQuery() Task poll (QueryTypeTask), result submission (QueryTypeResult), or exfil (QueryTypeExfil) 4 bytes per response
AAAA handleAAAAQuery() Task poll (QueryTypeTask) 16 bytes per response

The query type (QueryTypeBeacon, QueryTypeTask, QueryTypeResult, QueryTypeExfil) is determined by the subdomain prefix parsed from the query name. These prefixes are configurable through the malleable profile's dns-beacon block (see Malleable DNS Configuration).

For runtime mode switching from the operator console, see Switching Modes at Runtime above.


DNS Resolver Override

The dns_resolver field configures the beacon to send DNS queries to a specific resolver instead of the system's default DNS server. This is useful in environments where corporate DNS servers inspect or block C2 domains.

Example:

curl -s -X POST https://stentor.app/api/v1/listeners \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "DNS Bypass",
    "relay_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
    "type": "dns",
    "port": 53,
    "dns_domain": "c2.example.com",
    "dns_resolver": "8.8.8.8:53"
  }' | jq

This tells the beacon to send all C2 DNS queries to Google's public DNS (8.8.8.8) instead of the corporate DNS server at the target.

Non-standard DNS traffic detection

Many corporate environments monitor for DNS traffic to external resolvers (especially 8.8.8.8 and 1.1.1.1). A host sending DNS queries to an external resolver instead of the internal DNS server is a strong indicator of compromise. Consider using the system resolver when possible, or use DNS-over-HTTPS (DoH) to wrap queries in HTTPS traffic.


Rate Limiting and Anti-Detection

The DNS server supports rate limiting and response jitter to prevent detection by IDS/IPS systems that monitor for DNS flood patterns.

Setting Type Default Description
RateLimitQPS float 0 (disabled) Maximum queries per second per source IP. Excess queries receive SERVFAIL.
RateLimitBurst int 20 Burst allowance above the QPS limit. Allows short bursts without throttling.
JitterMs int 0 (disabled) Random response delay in milliseconds. Adds noise to response timing.

Recommended settings for stealth

For covert operations, set RateLimitQPS to 2-5 and JitterMs to 100-500. This prevents the beacon from generating a detectable burst of DNS queries while adding timing noise that defeats statistical analysis.

Idle Response

The dns_idle field controls what IP address the DNS server returns when a beacon checks in but has no pending tasks.

  • Default: "0.0.0.0" for A records, "::" for AAAA records
  • The implant interprets the idle response as "no tasks available" and returns to sleep

Setting dns_idle to a plausible IP address (e.g., a legitimate server IP) can make idle responses appear more natural in DNS logs.


Malleable DNS Configuration

Malleable C2 profiles include a dns-beacon block that customizes DNS C2 behavior. These settings override the listener defaults when a profile is attached.

Key Settings

Setting Description
dns_idle IP returned for idle beacons (overrides listener dns_idle)
dns_max_txt Maximum TXT record length (overrides listener dns_max_txt)
dns_sleep Sleep interval between DNS queries in milliseconds
dns_ttl TTL for DNS responses (overrides listener dns_ttl)
dns_stager_prepend Data prepended to DNS stager responses
dns_stager_subhost Subdomain prefix for stager queries

Custom Subdomain Prefixes

The malleable profile controls subdomain prefixes used for different C2 operations:

Prefix Default Purpose
beacon beacon Beacon check-in queries
get_A cdn A record data retrieval
get_AAAA www6 AAAA record data retrieval
get_TXT api TXT record data retrieval
put_output post Task result submission
put_metadata www Metadata submission (check-in via profiled handler)

DNS-over-HTTPS (DoH)

The dns-over-https nested block enables DoH transport as an alternative to raw DNS. When configured, the beacon wraps DNS queries in HTTPS requests to a DoH server, making them indistinguishable from normal HTTPS traffic.

Setting Description
doh_server DoH server URL (e.g., "https://dns.google/dns-query")
doh_verb HTTP method: "GET" or "POST"
doh_useragent Custom User-Agent for DoH requests

For full malleable profile syntax and configuration examples, see the Malleable Profiles page.


Troubleshooting

DNS queries not reaching the relay

  1. Check NS records: Verify delegation with dig NS c2.example.com -- the answer should point to the relay's IP
  2. Check firewall: Ensure UDP port 53 is open inbound to the relay
  3. Check listener status: Confirm the listener is running via GET /api/v1/listeners/$LISTENER_ID
  4. Check domain suffix: The DNS server only processes queries that are subdomains of the configured dns_domain

Slow beacon callback

  1. DNS caching: Intermediate resolvers cache responses based on TTL. Lower dns_ttl for faster task delivery (at the cost of more DNS traffic)
  2. Rate limiting: If RateLimitQPS is set too low, queries may be throttled. Check relay logs for SERVFAIL responses
  3. Resolver chain: Multiple resolver hops add latency. Consider using dns_resolver to point directly at the relay

Data channel issues

  1. AAAA queries blocked: Some networks filter AAAA queries. Fall back to dns (A record) mode
  2. TXT responses truncated: Some DNS proxies truncate large TXT records. Lower dns_max_txt to 180 or switch to A/AAAA mode
  3. Encoding errors: Verify the dns_domain matches exactly between the listener and the implant configuration

Resolver override not working

  1. Outbound DNS blocked: The corporate firewall may block DNS to external resolvers on port 53. Try DNS-over-HTTPS (DoH) instead
  2. IP mismatch: Verify the dns_resolver value includes the port (e.g., "8.8.8.8:53", not just "8.8.8.8")
  3. Firewall egress rules: Some environments only allow DNS to specific whitelisted resolvers