October 23, 2026
MudaleTunnel: A Control Plane for Network Pivoting
Manage SSH tunnels, network discovery, and pivoting across a penetration-testing engagement β from a standalone CLI, an msfconsole-styleβ¦
By Ori Ashkenazi
5 min read
Manage SSH tunnels, network discovery, and pivoting across a penetration-testing engagement β from a standalone CLI, an msfconsole-style interactive console, or a real-time web dashboard that share one engine.
The Problem: Pivoting Is Plumbing, Not Intelligence
Every pentester knows the drill. You land on a foothold, open an SSH tunnel, run nmap through it, find ten services behind the jump host, and then hand-craft ten more ssh -L commands to reach them. You paste each one into a different terminal tab, forget which port maps to what, lose track after a reboot, and pray you remember what you touched when you write the report.
The tools that move packets β SSH, proxychains, ligolo-ng β are excellent at their job. But nobody is tracking what a pivot actually proved reachable, who opened it, or whether it was in scope. That state lives in your head, your terminal history, and nowhere else.
MudaleTunnel flips the workflow. Instead of routing a subnet you hope is there, it:
- Reaches β opens a SOCKS proxy or TUN interface through the foothold.
- Discovers β scans through the pivot. Every open port is evidence, recorded against the tunnel it was seen through.
- Exposes β turns findings into local forwards automatically, so tools that don't speak SOCKS get a
localhostport. - Chains β a newly reachable host can become the next foothold.
The map is evidence, not assumption.
Architecture
MudaleTunnel has a clean separation between what an operator wants and what is.
Three interfaces, one engine. The standalone CLI works out of the box with SQLite β no server, no Docker, no setup. The interactive console adds an msfconsole-style use/set/run workflow with tab-completion. The web dashboard adds multi-operator access, real-time WebSocket updates, and a topology view. All three share mudale_engine, which owns every subprocess and never lets the web tier touch a process directly.
The web tier never spawns a process. It records desired state in PostgreSQL. The engine daemon reads that, reconciles reality, and writes back observed state. If the API dies, tunnels keep running. If the engine restarts, it adopts orphaned processes by PID.
Five Tunnel Kinds
KindToolUse Forssh_localSSH -LReach one remote service locallyssh_dynamicSSH -DSOCKS proxy into the remote networkssh_remoteSSH -RExpose a local service to the remote sidessh_remote_dynamicSSH -R (SOCKS)Reverse SOCKS (OpenSSH 7.6+ on both ends)ligololigolo-ngFull IP-layer pivoting β TCP, UDP, and ICMP through a TUN interface
SSH SOCKS carries TCP only. ligolo-ng routes the full IP layer, so ping and UDP-based tools work natively β but it needs an agent binary on the target and root on the operator machine. Both are first-class citizens in MudaleTunnel.
Scan-Driven Pivoting
Scanning isn't a side feature β it is the pivot workflow.
When you scan through a tunnel, MudaleTunnel runs nmap genuinely proxied through your SOCKS pivot (via proxychains) or natively through a ligolo TUN interface. Every open port it finds is recorded as evidence: this host, this port, proven reachable through this tunnel.
Then you expose what you found. One command turns a discovered service into a localhost port-forward:
mudale > use pivot/auto
mudale pivot(auto) > set TUNNEL 3
mudale pivot(auto) > run
# 8 services exposed as local forwardsmudale > use pivot/auto
mudale pivot(auto) > set TUNNEL 3
mudale pivot(auto) > run
# 8 services exposed as local forwardsFor ligolo tunnels, services are already routable β MudaleTunnel tells you that instead of creating redundant forwards, and generates the ip route add commands for discovered subnets.
Auto-expose is bounded by design. Administrative ports (22, 3389, 5985) are skipped unless explicitly opted in. The forward count is capped. "Open a hundred ports into a client network" should never be the accidental outcome of one command.
The msfconsole-Style Interactive Console
If you've used Metasploit, you already know how this works:
ββ¦ββ¦ β¦ββ¦βββββ¦ βββ
ββββ β βββ ββ£β ββ£
β© β©βββββ©ββ© β©β©βββββ v3.0.0
[ SSH tunnels β’ nmap scanning β’ network pivoting ]
7 modules | 0 active tunnels | 0 running scans | engine: ready
mudale > show modules
mudale > use tunnel/ssh_dynamic
mudale tunnel(ssh_dynamic) > set RHOST 10.4.0.10
mudale tunnel(ssh_dynamic) > set USERNAME opsec
mudale tunnel(ssh_dynamic) > set LPORT 1080
mudale tunnel(ssh_dynamic) > run
mudale > use scan/nmap
mudale scan(nmap) > set TARGET 10.4.50.0/24
mudale scan(nmap) > set VIA_TUNNEL 1
mudale scan(nmap) > run
mudale > show servicesββ¦ββ¦ β¦ββ¦βββββ¦ βββ
ββββ β βββ ββ£β ββ£
β© β©βββββ©ββ© β©β©βββββ v3.0.0
[ SSH tunnels β’ nmap scanning β’ network pivoting ]
7 modules | 0 active tunnels | 0 running scans | engine: ready
mudale > show modules
mudale > use tunnel/ssh_dynamic
mudale tunnel(ssh_dynamic) > set RHOST 10.4.0.10
mudale tunnel(ssh_dynamic) > set USERNAME opsec
mudale tunnel(ssh_dynamic) > set LPORT 1080
mudale tunnel(ssh_dynamic) > run
mudale > use scan/nmap
mudale scan(nmap) > set TARGET 10.4.50.0/24
mudale scan(nmap) > set VIA_TUNNEL 1
mudale scan(nmap) > run
mudale > show servicesSeven modules: scan/nmap, four SSH tunnel types, tunnel/ligolo, and pivot/auto. Tab-completion, option validation, and the same engine underneath.
The Dashboard
Everything the CLI does, the dashboard does β same API, same policy checks. A tunnel opened in the terminal appears in the browser without a reload.
Sessions β every tunnel in the engagement, its forwards, and how long it's been up. Status is observed by the engine, never asserted by the web tier.
Topology β what each pivot has been proven to reach. Solid edges are direct, dashed ones go through a foothold. Clicking a host offers to expose any of its open ports.
Scans β results grouped by host, with the tunnel each was found through.
Audit β who did what, when, and what was refused. Append-only. Survives operator deletion.
Security Model β Scope Is Enforced in Code
The security model deserves its own section because it's the reason this tool exists at all:
- CIDR allowlist per engagement, enforced in code on every scan, tunnel, and route. Targets are parsed into
ipaddressobjects and the command line is rebuilt from those objects β a string that isn't an address can never reach anargv. Partial overlap fails closed: a/16allowance does not let you scan the enclosing/8. - Hardened spec validation. Every tunnel spec passes through frozen Pydantic models with
extra="forbid". SSH uses-l user hostformat (notuser@host) to block scope-ID smuggling through the @ separator. - Authentication everywhere. DRF defaults to
IsAuthenticated, WebSockets are authorised per-engagement on connect, and API rate limiting (30/min anonymous, 120/min authenticated) is on by default. - Append-only audit trail that records denials, names the actor, and cannot be deleted by operators.
- Hardened XML parsing with
defusedxmlbecause nmap output comes from the target β hostile service banners are a real threat. - Process supervision with SIGTERM β wait β SIGKILL escalation and PID tracking that survives daemon restarts. No stray processes.
Production-Ready
- Docker multi-stage build β one image runs both the web tier (Daphne) and the engine daemon as separate containers. Frontend built as SPA, served by WhiteNoise.
- PostgreSQL + Redis for durable state and real-time WebSocket channels.
- Pre-built binaries for Linux, macOS, and Windows via PyInstaller β no Python installation required.
- CI pipeline with pytest, ruff, TypeScript checks, and tag-driven releases.
- Runs unprivileged β the Docker image creates a dedicated
mudaleuser. The v2 image ran as root, which combined with an unauthenticated endpoint meant root-level filesystem access from the network.
Try It
Standalone (no server, no Docker)
git clone https://github.com/TarzEH/MudaleTunnel.git
cd MudaleTunnel
pip install ./backend
mudale consolegit clone https://github.com/TarzEH/MudaleTunnel.git
cd MudaleTunnel
pip install ./backend
mudale consoleFull Stack (dashboard + multi-operator)
git clone https://github.com/TarzEH/MudaleTunnel.git
cd MudaleTunnel
cp backend/.env.example .env
# Set MUDALE_SECRET_KEY and MUDALE_DB_PASSWORD in .env
docker compose up -d --build
docker compose exec web python manage.py createsuperusergit clone https://github.com/TarzEH/MudaleTunnel.git
cd MudaleTunnel
cp backend/.env.example .env
# Set MUDALE_SECRET_KEY and MUDALE_DB_PASSWORD in .env
docker compose up -d --build
docker compose exec web python manage.py createsuperuserDashboard on http://127.0.0.1:8000. It binds to loopback on purpose β this is a console that opens tunnels into client networks.
Pre-built binaries
# Download from GitHub Releases
chmod +x mudale-darwin-arm64
./mudale-darwin-arm64 --help# Download from GitHub Releases
chmod +x mudale-darwin-arm64
./mudale-darwin-arm64 --helpBuilt With
Django 5.2 Β· Django REST Framework Β· Django Channels Β· Pydantic 2 Β· React 18 Β· Vite Β· PostgreSQL Β· Redis Β· PyInstaller Β· defusedxml Β· prompt_toolkit
Runtime tools (not bundled): OpenSSH Β· Nmap Β· proxychains-ng Β· ligolo-ng
Disclaimer
This tool is intended for authorised security testing only. The scope allowlist exists to keep you inside what a client actually agreed to. It is a safety rail, not permission. You are responsible for obtaining explicit written authorisation before testing any network or system.
MIT License.
GitHub: https://github.com/TarzEH/MudaleTunnel
#Cybersecurity #PenetrationTesting #Pivoting #SSH #NetworkSecurity #OpenSource #Django #React #InfoSec #RedTeam #OSCP #HackTheBox #BugBounty #Python #OffensiveSecurity