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¶
- A domain you control (or a subdomain delegated to you)
- An NS record pointing the C2 subdomain to the relay's public IP
- 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.
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:
3. Verify delegation
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:
- Decode request: The handler joins subdomain labels and Base32-decodes them into a JSON
CheckinRequestcontaining:hostname,username,ip,os,arch,pid, andbeacon_id - 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_REFUSEDand aguardrail_violationevent is fired over WebSocket - First check-in (empty
beacon_id): The handler creates a new beacon via the sharedBeaconRegistryand fires abeacon_newWebSocket event to the backend server - 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. Abeacon_checkinWebSocket event is fired - Response: The handler returns a Base32-encoded JSON response as a TXT record containing
beacon_id,sleepinterval, andjitterpercentage
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:
- Decode beacon ID: Base32-decode the subdomain labels and parse the UUID
- Validate beacon: Confirm the beacon exists in the registry and update its heartbeat timestamp
- Local queue check: Dequeue the next task from the local
TaskQueue(shared with HTTP listeners) - 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 - Response: Full task as Base32-encoded JSON containing
task_id,type, base64-encodeddata, and optionalrelay_forfield - 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:
- An operator enqueues a task for a P2P child beacon on the server
- The DNS parent beacon polls the backend via
proxyTaskPollToBackend() - The backend returns the task with the
relay_forfield set to the child beacon's ID - The DNS handler includes
relay_forin the TXT task response to the parent beacon - The parent beacon recognizes the
relay_forfield and relays the task to the child over its P2P link (SMB named pipe, TCP bind socket) - The child beacon executes the task and returns the result through the parent
- 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:
- The implant splits the task result into chunks that fit within DNS subdomain labels
- Each chunk is sent as an A record query with the following label format:
- The handler stores each chunk in a
resultChunksmap keyed bytask_id - Each chunk is acknowledged with an A record response:
1.0.0.{chunk_index}(success) or1.0.0.255(error) - When all chunks are received (
len(chunks) == total_chunks),processCompleteResult()reassembles them in order by iterating chunk indices 0 through N-1 - The reassembled data is parsed as JSON containing
beacon_id,success,output, anderrorfields - The complete result is forwarded to the backend via a
task_completeWebSocket 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¶
- Check NS records: Verify delegation with
dig NS c2.example.com-- the answer should point to the relay's IP - Check firewall: Ensure UDP port 53 is open inbound to the relay
- Check listener status: Confirm the listener is
runningviaGET /api/v1/listeners/$LISTENER_ID - Check domain suffix: The DNS server only processes queries that are subdomains of the configured
dns_domain
Slow beacon callback¶
- DNS caching: Intermediate resolvers cache responses based on TTL. Lower
dns_ttlfor faster task delivery (at the cost of more DNS traffic) - Rate limiting: If
RateLimitQPSis set too low, queries may be throttled. Check relay logs for SERVFAIL responses - Resolver chain: Multiple resolver hops add latency. Consider using
dns_resolverto point directly at the relay
Data channel issues¶
- AAAA queries blocked: Some networks filter AAAA queries. Fall back to
dns(A record) mode - TXT responses truncated: Some DNS proxies truncate large TXT records. Lower
dns_max_txtto 180 or switch to A/AAAA mode - Encoding errors: Verify the
dns_domainmatches exactly between the listener and the implant configuration
Resolver override not working¶
- Outbound DNS blocked: The corporate firewall may block DNS to external resolvers on port 53. Try DNS-over-HTTPS (DoH) instead
- IP mismatch: Verify the
dns_resolvervalue includes the port (e.g.,"8.8.8.8:53", not just"8.8.8.8") - Firewall egress rules: Some environments only allow DNS to specific whitelisted resolvers