errantpacket
Oblique-Relay
JavaScript

Cloudflare Workers edge redirector for authorized red team infrastructure. Validates implant traffic against C2 profiles, proxies to backend, decoys everything else.

Last updated Jul 15, 2026
10
Stars
0
Forks
0
Issues
0
Stars/day
Attention Score
19
Language breakdown
JavaScript 60.7%
Shell 26.7%
HTML 12.6%
โ–ธ Files click to expand
README

Oblique Relay

Edge redirector built on Cloudflare Workers for authorized penetration testing infrastructure.

Sits between implants and your teamserver. Inbound requests are validated against a configurable profile. Traffic that matches gets proxied to the backend. Everything else receives a decoy response, either a 302 redirect to a legitimate site or a static placeholder page. The profile lives in KV and can be updated at runtime without redeployment.

Architecture

Architecture

Request Flow

Request Flow

Quick Start

git clone https://github.com/errantpacket/Oblique-Relay.git && cd Oblique-Relay
npm install

local dev (uses .dev.vars for secrets)

cp .dev.vars.example .dev.vars npm run dev

run tests

npm test

Deployment

wrangler login

wrangler secret put BACKEND_URL # https://your-teamserver.internal wrangler secret put DECOY_URL # https://en.wikipedia.org wrangler secret put PROFILE_SECRET # shared token for operator endpoints

npm run deploy

Profile Configuration

The profile controls which requests get proxied and which get decoyed. It is stored in the PROFILE KV namespace at key profile:active and can be updated at runtime through the operator API. When no KV profile exists, a hardcoded default is used.

Profile Schema

{
  "paths": ["/api/v1/status", "/api/v1/data"],
  "methods": ["GET", "POST"],
  "headers": {"X-Request-ID": "^[a-f0-9-]{36}$"},
  "ua_pattern": "Mozilla/5\\.0",
  "geo_allow": ["US", "CA"],
  "geo_deny": ["CN", "RU"],
  "timewindow": {"start": "08:00", "end": "22:00", "tz": "America/NewYork"},
  "jitter_ms": 100,
  "backends": {"/content": "https://alt-teamserver.example.com"}
}

Validation Pipeline

Validation Pipeline

Every check must pass. The first failure triggers a decoy response.

PROFILE_SECRET is not part of this pipeline. It only gates operator endpoints (/health, /profile, etc.). Implant traffic is validated by the profile alone, because C2 frameworks typically do not include custom auth headers in their HTTP callbacks by default.

| Field | Type | Description | |-------|------|-------------| | paths | string[] | URI prefixes that pass validation | | methods | string[] | Allowed HTTP methods | | headers | {name: regex} | Required request headers with regex patterns | | ua_pattern | string\|null | Regex the User-Agent must match (null to skip) | | geo_allow | string[] | Allowed country codes from CF-IPCountry (empty allows all) | | geo_deny | string[] | Blocked country codes (empty blocks none) | | time_window | object\|null | Active hours as {start, end, tz} in HH:MM (null for always) | | jitter_ms | number | Max random delay before responding, in ms (0 to disable) | | backends | {prefix: url} | Route by path prefix (unmatched paths use BACKEND_URL) |

Operator API

All operator endpoints require an X-Auth-Token header matching PROFILE_SECRET.

Health Check

curl -H "X-Auth-Token: <secret>" https://oblique-relay.example.com/__health

{"status":"ok","ts":1710000000000,"profile":"kv"}

View / Update Profile

# view current profile
curl -H "X-Auth-Token: <secret>" https://oblique-relay.example.com/__profile

replace profile

curl -X PUT -H "X-Auth-Token: <secret>" -H "Content-Type: application/json" \ -d '{"paths":["/beacon"],"methods":["GET","POST"],"geo_allow":["US"]}' \ https://oblique-relay.example.com/__profile

Import C2 Profile

Import a native C2 framework profile. The parser extracts paths, methods, headers, and user-agent into the relay's normalized format.

# dry run (preview without saving)
curl -X POST -H "X-Auth-Token: <secret>" \
  -d @malleable.profile \
  "https://oblique-relay.example.com/_profile/import?dryrun=true"

Cobalt Strike (default format)

curl -X POST -H "X-Auth-Token: <secret>" \ -d @malleable.profile \ "https://oblique-relay.example.com/__profile/import"

Sliver

curl -X POST -H "X-Auth-Token: <secret>" \ -d @sliver-http.json \ "https://oblique-relay.example.com/__profile/import?format=sliver"

Mythic

curl -X POST -H "X-Auth-Token: <secret>" \ -d @mythic-profile.json \ "https://oblique-relay.example.com/__profile/import?format=mythic"

Havoc

curl -X POST -H "X-Auth-Token: <secret>" \ -d @havoc.yaotl \ "https://oblique-relay.example.com/__profile/import?format=havoc"

PoshC2

curl -X POST -H "X-Auth-Token: <secret>" \ -d @poshc2-config.txt \ "https://oblique-relay.example.com/__profile/import?format=poshc2"

Oblique Server (private repo)

curl -X POST -H "X-Auth-Token: <secret>" \ -d @oblique-profile.json \ "https://oblique-relay.example.com/__profile/import?format=oblique"

Supported parsers:

| Format | Config type | What it extracts | |--------|-------------|------------------| | cobalt-strike | Malleable C2 .profile | URIs, methods, client headers, useragent | | sliver | HTTP C2 JSON (native export) | Path segments, methods, implant headers, user_agent | | mythic | JSON profile | get/post URIs, AgentHeaders, user_agent | | havoc | Yaotl/HCL-like config | Uris, UserAgent, Headers array | | poshc2 | Python config | UrlsX, UserAgent, DomainFrontHeader, PayloadCommsHost | | oblique | JSON (native adapter export) | paths, methods, headers, user_agent |

Fields not present in the C2 config (geo, time window, jitter, backends) stay at their defaults. Configure them separately with PUT /__profile.

Both PUT and import validate against the profile schema. Invalid profiles are rejected with descriptive errors.

Metrics Dashboard

curl -H "X-Auth-Token: <secret>" \
  "https://oblique-relay.example.com/__metrics" -o metrics.html

filter by time range and entry limit

curl -H "X-Auth-Token: <secret>" \ "https://oblique-relay.example.com/__metrics?hours=72&limit=1000"

Shows valid/decoy counts, valid ratio, requests per hour, top source IPs, paths, countries, and user-agents. Aggregated from the LOG KV namespace.

Session Tracking

Per-implant session tracking using Durable Objects with SQLite storage. Sessions are keyed by X-Session-ID header, falling back to CF-Connecting-IP.

# list active sessions
curl -H "X-Auth-Token: <secret>" https://oblique-relay.example.com/__sessions

view a specific session

curl -H "X-Auth-Token: <secret>" https://oblique-relay.example.com/__sessions/<session-id>

Each session tracks:

| Field | Description | |-------|-------------| | request_count | Total requests from this session | | first_seen | Timestamp of first request | | last_seen | Timestamp of most recent request | | paths | Last 100 paths accessed with timestamps | | flagged | Auto-flagged if request count exceeds 1000 | | flag_reason | Reason for flagging |

Sessions auto-expire after 24 hours of inactivity via DO alarms. A KV-based index (session: prefix in LOG) enables listing without enumerating all Durable Objects.

Logging

Every request is logged to the LOG KV namespace with a 7-day TTL. Keys are prefixed valid: or decoy: for filtering.

npm run logs         # valid traffic
npm run logs:decoy   # rejected traffic
npm run tail         # real-time stream

Testing

Unit Tests

146 tests across 12 files covering validation, proxy, operator endpoints, logging, sessions, and all parsers.

npm test             # run full suite
npm run test:watch   # watch mode

E2E Tests

The E2E suites validate the full chain against real C2 servers with no mocks.

Sliver E2E (27 tests): log-based and PTY-driven verification:

  • Start Sliver server with HTTP listener and implant generation
  • Establish a cloudflared tunnel to the Sliver backend
  • Import the real Sliver C2 profile through the relay's parser
  • Verify negative-path filtering (invalid paths get decoyed)
  • Compile and run a beacon implant through the relay
  • Confirm beacon registration in Sliver's server logs
  • Execute C2 tasks (whoami, file download) through the relay
  • Verify bidirectional traffic (task results returned through relay)
# requires Docker, Sliver v1.7.3 image, and a deployed relay
./test/e2e/validate-sliver.sh <relay-url> <auth-token>

Mythic E2E (33 tests): API-driven verification with 9 containers:

  • Start Mythic stack (postgres, rabbitmq, server, Hasura GraphQL, nginx, HTTP C2 profile, Poseidon)
  • Authenticate via REST API, poll until C2 profile and agent type register via postgres
  • Start HTTP C2 profile listener via webhook API
  • Establish a cloudflared tunnel to the HTTP C2 profile listener
  • Extract C2 profile parameters from postgres, transform to parser format, and import into relay
  • Verify negative-path filtering (invalid paths get decoyed, valid Mythic paths proxied)
  • Generate Poseidon payload via webhook API with relay as callback host (~30s cross-compilation)
  • Run agent on victim container, confirm callback registration and AES key exchange via postgres
  • Execute C2 tasks (shell whoami, file download) via webhook API, verify completion and relay traffic
# requires Docker, Mythic GHCR images (latest), and a deployed relay
./test/e2e/validate-mythic.sh <relay-url> <auth-token>

Operator Dashboard

A local HTML dashboard for managing the relay from your browser. No server, no build step. Open the file and connect.

open tools/dashboard.html

Five tabs: Health, Profile (view/edit/import), Metrics (charts and breakdown tables), Logs (filterable raw entries), and Sessions (expandable detail with path history). All tabs support JSON export.

See docs/dashboard.md for full documentation.

Key Files

| File | Role | |------|------| | src/worker.js | Entry point, routing, operator endpoints | | src/validation.js | Request validation pipeline (method, path, headers, UA, geo, time) | | src/proxy.js | Backend proxying and decoy responses | | src/profile.js | Profile loading and management (KV-backed) | | src/logging.js | Request logging (best-effort, non-blocking) | | src/metrics.js | HTML metrics dashboard | | src/sessions.js | Durable Object session tracking | | src/util.js | Response helpers, timing-safe comparison, escaping | | src/parsers/ | C2 profile parsers (Cobalt Strike, Sliver, Mythic, Havoc, PoshC2, Oblique) | | tools/dashboard.html | Local operator dashboard (single HTML file, no deps) | | test/e2e/ | E2E validation suites (Sliver: 27 tests, Mythic: 33 tests) | | docs/dashboard.md | Dashboard documentation | | docs/adding-a-parser.md | Guide for adding new C2 parser support |

Environment Bindings

| Binding | Type | Purpose | |---------|------|---------| | BACKEND_URL | Secret | Default backend/teamserver URL | | DECOY_URL | Secret | Redirect target for invalid traffic | | PROFILE_SECRET | Secret | Operator auth token (gates operator endpoints only) | | LOG | KV Namespace | Request logging with 7-day TTL | | PROFILE | KV Namespace | Runtime profile configuration | | SESSIONS | Durable Object | Per-implant session tracking |

Commands

npm run dev          # local dev server on :8787
npm run deploy       # deploy to Cloudflare
npm run tail         # real-time request logs
npm run logs         # list valid-traffic KV entries
npm run logs:decoy   # list decoy-traffic KV entries
npm run lint         # ESLint with security rules
npm run lint:fix     # auto-fix lint issues
npm test             # 146 tests across 12 files
npm run test:watch   # tests in watch mode

Security

Auth Model

There are two separate trust boundaries:

Operator endpoints (/health, /profile, /metrics, /sessions) are protected by PROFILE_SECRET. Requests must include an X-Auth-Token header with the correct token. Comparison is timing-safe (HMAC-SHA256).

Implant traffic is validated by the profile pipeline only: method, path, headers, user-agent, geo, and time window. There is no shared secret on this path. C2 frameworks typically do not include custom auth headers in their HTTP callbacks by default, so a blanket token check here would reject real implant traffic out of the box. Instead, the profile itself is the security boundary. An attacker would need to know the exact C2 configuration (path prefixes, required headers, UA pattern) to craft requests that pass validation. Even then, the backend C2 server has its own encryption and key exchange, so proxied garbage gets ignored.

Operators who want tighter control can add required headers to the profile. For example, setting "headers": {"X-Custom": "^secret-value$"} forces implant traffic to include that header. This is framework-dependent and must be configured in both the C2 profile and the relay profile.

General

  • Secrets never in source. Use wrangler secret put or the CF dashboard.
  • All C2 parser output escaped with escapeRegex to prevent regex injection
  • Parsers only extract request headers (what the implant sends), never response headers
  • Backend errors fall through to the decoy silently
  • server and x-powered-by headers stripped from proxied responses
  • HTML output escaped with escapeHTML to prevent XSS in the metrics dashboard
  • Client IP forwarded via X-Forwarded-For
  • CF-specific headers (CF-Connecting-IP, CF-IPCountry, CF-Ray) stripped before proxying
  • Decoy responses never expose backend URLs or secrets
  • ESLint with eslint-plugin-security enforced in CI

Adding a New C2 Parser

See docs/adding-a-parser.md for the full guide.

  • Create src/parsers/<name>.js exporting { name, parse(text) } that returns a normalized profile
  • Register it in src/parsers/index.js
  • Add tests in test/parsers/<name>.test.js covering parsing, edge cases, and rejection
  • Escape all extracted strings with escapeRegex()
  • Only extract request headers, never response headers

License

MIT. Developed for authorized penetration testing and research. Do what you want. You are responsible for how you use it.

๐Ÿ”— More in this category

ยฉ 2026 GitRepoTrend ยท errantpacket/Oblique-Relay ยท Updated daily from GitHub