Skip to content

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

  1. The relay listens on a public UDP port (e.g., 51820)
  2. The implant initiates the WireGuard handshake to the relay's endpoint
  3. After handshake, both sides have a tunnel with assigned IPs (10.99.0.1 and 10.99.0.2)
  4. Keepalive packets keep the NAT mapping alive
  5. 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.