WireGuard Transport¶
WireGuard provides an encrypted UDP tunnel between the implant and the relay. Inside the tunnel, the standard HTTP C2 protocol runs unchanged, giving you full network-level connectivity with strong encryption and NAT traversal capabilities.
graph LR
subgraph Target Network
Implant["Implant<br/>10.99.0.2"]
end
subgraph Relay
WG["WireGuard Endpoint<br/>UDP :51820"]
HTTP["HTTP C2 Server<br/>10.99.0.1:8080"]
end
Implant <-->|"WireGuard UDP Tunnel<br/>(Curve25519 + ChaCha20-Poly1305)"| WG
WG --- HTTP Use cases:
- Full network-level access through an encrypted tunnel
- NAT traversal where TCP-based transports are unreliable
- Direct IP connectivity without HTTP overhead on the wire
- Environments where UDP traffic blends better than HTTP/HTTPS
Key Management¶
WireGuard uses Curve25519 key pairs. Both the relay and the implant need their own key pair. The API provides a convenience endpoint to generate both pairs at once.
Generate Key Pairs¶
curl -s -X POST https://stentor.app/api/v1/listeners/wireguard/keygen \
-H "Authorization: Bearer $TOKEN"
Response:
{
"relay_private_key": "WEhNb3V...<base64>...",
"relay_public_key": "aGVsbG8...<base64>...",
"implant_private_key": "c2VjcmV...<base64>...",
"implant_public_key": "d29ybGQ...<base64>..."
}
All keys are base64-encoded Curve25519 keys (32 bytes decoded).
Key Storage
Private keys are sensitive credentials. Store them with the same care as passwords or TLS private keys. The relay_private_key is stored in the listener configuration. The implant_private_key is embedded in the implant build.
Creating a WireGuard Listener¶
# 1. Generate keys
KEYS=$(curl -s -X POST https://stentor.app/api/v1/listeners/wireguard/keygen \
-H "Authorization: Bearer $TOKEN")
RELAY_PRIV=$(echo "$KEYS" | jq -r '.relay_private_key')
IMPLANT_PUB=$(echo "$KEYS" | jq -r '.implant_public_key')
# 2. Create listener
LISTENER_ID=$(curl -s -X POST https://stentor.app/api/v1/listeners \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"WireGuard Tunnel\",
\"type\": \"wireguard\",
\"relay_id\": \"aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee\",
\"port\": 51820,
\"wg_private_key\": \"$RELAY_PRIV\",
\"wg_peer_public_key\": \"$IMPLANT_PUB\",
\"wg_tunnel_ip\": \"10.99.0.1/24\",
\"wg_peer_tunnel_ip\": \"10.99.0.2/32\",
\"wg_c2_port\": 8080
}" | jq -r '.id')
# 3. Start listener
curl -s -X POST "https://stentor.app/api/v1/listeners/$LISTENER_ID/start" \
-H "Authorization: Bearer $TOKEN"
Configuration Options¶
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Display name for the listener |
type | string | "wireguard" | Must be wireguard for WireGuard listeners |
relay_id | UUID | required | Relay that will host the WireGuard endpoint |
port | int | required | External UDP port for WireGuard tunnel |
wg_private_key | string | required | Relay's Curve25519 private key (base64) |
wg_public_key | string | derived | Relay's public key (auto-derived from private key, for display only) |
wg_peer_public_key | string | required | Implant's Curve25519 public key (base64) |
wg_tunnel_ip | string | "10.99.0.1/24" | Relay's IP address inside the WireGuard tunnel |
wg_peer_tunnel_ip | string | "10.99.0.2/32" | Implant's IP address inside the WireGuard tunnel |
wg_c2_port | int | 8080 | HTTP port inside the tunnel where the C2 handler listens |
guardrails | JSON | null | Beacon filtering rules. null means accept all beacons. |
NAT Traversal¶
WireGuard uses UDP hole-punching for NAT traversal, making it effective in most network configurations:
- PersistentKeepalive: The implant sends keepalive packets every 25 seconds (default) to maintain NAT mappings.
- Supported NAT types: Full cone, restricted cone, and port-restricted NAT all work reliably.
- Symmetric NAT: May require a publicly routable relay endpoint.
MTU Considerations
The default MTU is 1420 bytes. On double-NAT networks or networks with additional encapsulation (PPPoE, VPN-in-VPN), you may need to reduce the MTU further (e.g., 1280) to avoid fragmentation.
How It Works¶
- The relay listens on a public UDP port (e.g., 51820)
- The implant initiates the WireGuard handshake to the relay's endpoint
- After handshake, both sides have a tunnel with assigned IPs (10.99.0.1 and 10.99.0.2)
- Keepalive packets keep the NAT mapping alive
- HTTP C2 traffic flows inside the tunnel, invisible to network inspection
Direct Connectivity Setup¶
Step-by-step setup for a direct implant-to-relay WireGuard tunnel:
Step 1: Generate Key Pairs¶
Use the keygen API endpoint to create Curve25519 key pairs for both relay and implant:
curl -s -X POST https://stentor.app/api/v1/listeners/wireguard/keygen \
-H "Authorization: Bearer $TOKEN" | jq .
Save all four keys. The relay private key goes into the listener config. The implant private key and relay public key go into the implant build config.
Step 2: Create and Start the Listener¶
Create the WireGuard listener on the relay with the relay's private key and implant's public key:
curl -s -X POST https://stentor.app/api/v1/listeners \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "WireGuard Tunnel",
"type": "wireguard",
"relay_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
"port": 51820,
"wg_private_key": "<relay_private_key>",
"wg_peer_public_key": "<implant_public_key>"
}'
Step 3: Configure the Implant¶
The implant needs the following WireGuard configuration:
| Implant Config Field | Value | Source |
|---|---|---|
PrivateKey | <implant_private_key> | From keygen response |
PeerPublicKey | <relay_public_key> | From keygen response |
Endpoint | <relay-ip>:51820 | Relay's public IP and UDP port |
TunnelIP | 10.99.0.2/32 | Must match wg_peer_tunnel_ip |
RelayTunnelIP | 10.99.0.1 | Must match wg_tunnel_ip |
C2Port | 8080 | Must match wg_c2_port |
Step 4: Implant Connects¶
When the implant starts, it initiates the WireGuard handshake. Once the tunnel is established, the implant makes standard HTTP C2 requests (checkin, get task, submit result) to http://10.99.0.1:8080 inside the tunnel.
Architecture Comparison¶
graph TB
subgraph HTTPS Transport
I1["Implant"] -->|"HTTPS (TLS)"| R1["Relay :8443"]
R1 --> H1["C2 Handler"]
end
subgraph WireGuard Transport
I2["Implant"] -->|"UDP (WireGuard)"| R2["Relay :51820"]
R2 --> T["Tunnel Network<br/>10.99.0.0/24"]
T --> H2["C2 Handler<br/>10.99.0.1:8080"]
end | Aspect | HTTPS | WireGuard |
|---|---|---|
| Protocol | TCP + TLS | UDP + ChaCha20-Poly1305 |
| Layer | Application (HTTP) | Network (IP tunnel) |
| NAT traversal | TCP (reliable) | UDP hole-punching (keepalive needed) |
| Traffic signature | Looks like HTTPS web traffic | Looks like WireGuard VPN traffic |
| Latency | Higher (TCP handshake + TLS) | Lower (UDP, no handshake per request) |
| Overhead | TLS frame headers | WireGuard packet overhead (32 bytes) |
Implant Configuration¶
The implant's WireGuard client uses the following configuration structure:
| Field | Type | Default | Description |
|---|---|---|---|
PrivateKey | string | required | Implant's Curve25519 private key (base64) |
PeerPublicKey | string | required | Relay's Curve25519 public key (base64) |
Endpoint | string | required | Relay's WireGuard UDP endpoint (host:port) |
TunnelIP | string | "10.99.0.2/32" | Implant's assigned IP inside the tunnel |
RelayTunnelIP | string | "10.99.0.1" | Relay's IP inside the tunnel |
C2Port | int | 8080 | HTTP port inside the tunnel for C2 traffic |
PersistentKeepalive | int | 25 | Keepalive interval in seconds for NAT traversal |
MTU | int | 1420 | Tunnel MTU in bytes |
Userspace Implementation¶
The implant uses netstack (userspace TCP/IP stack) to operate the WireGuard tunnel without requiring kernel drivers or administrator privileges:
- No kernel WireGuard module needed
- No TUN/TAP adapter installation required
- Works in userspace -- no special privileges for the tunnel itself
- The HTTP client inside the tunnel reuses standard C2 endpoints (
/api/v1/c2/beacon,/api/v1/c2/beacon/{id}/task,/api/v1/c2/beacon/{id}/result)
The relay side also uses netstack, creating a TCP listener inside the tunnel that serves the same C2 HTTP mux used by HTTP/HTTPS listeners.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| No tunnel established | UDP port blocked on relay firewall | Open the configured UDP port (e.g., 51820) on the relay's firewall |
| Tunnel up but no C2 | C2 port mismatch | Verify wg_c2_port on the listener matches C2Port on the implant |
| Tunnel up but no C2 | Tunnel IP mismatch | Verify wg_tunnel_ip and wg_peer_tunnel_ip match between listener and implant |
| Intermittent drops | NAT mapping expires | Increase PersistentKeepalive interval (default 25s should work for most NATs) |
| Fragmentation / packet loss | MTU too large | Reduce MTU from 1420 to 1280 on double-NAT or encapsulated networks |
| "decode private key" error | Malformed base64 key | Regenerate keys using the keygen API. Keys must be exactly 32 bytes when decoded. |
| Key mismatch | Wrong key pairing | Ensure relay uses relay_private_key and implant uses implant_private_key. The peer keys must cross-match. |