Mastering SSH Tunneling: Local (-L), Remote (-R), and Dynamic (-D) – with Hands-On Labs and a SOCKS5/ProxyChains Deep Dive

This article walks through setting up a safe lab environment to experiment with all three types of SSH tunneling. By recreating realistic scenarios, you’ll learn how to use local, remote, and dynamic port forwards to securely access hidden services, expose local tools, and pivot traffic through a SOCKS5 proxy.

Why this guide?

SSH tunneling lets you move traffic through an encrypted channel to reach services you otherwise couldn’t. Whether you’re a pentester, sysadmin, SRE, or developer, you’ll eventually meet a database that only binds to 127.0.0.1 on a remote host, or you’ll need the remote machine to reach a service running on your machine, or you’ll want a general-purpose proxy to explore a network without re-writing tool flags every time.

This post is half tutorial, half lab manual. You’ll build three small labs – Local, Remote, and Dynamic – and you’ll finish with a practical, detailed walk-through of SOCKS5 + ProxyChains, including configuration options and chaining “anonymous” proxies responsibly.


Audience & prerequisites

  • Comfortable with Linux CLI on two VMs (your “attacker” – e.g., Kali – and a target Ubuntu Server).
  • SSH access to the target (password or key).
  • Root/sudo on your machine; non-privileged access is fine on the target (that’s the point of tunneling).
  • Basic networking (TCP, ports) and a database client (we’ll use psql locally in Lab 1).

Lab topology

  • Attacker (Kali) – where you initiate tunnels and run clients.
  • Target (Ubuntu) – runs services, some bound to 127.0.0.1 only (not exposed to the network).
  • Optional jump host – for multi-hop/ProxyJump demos (not required for core labs).

Tip: confirm what’s listening on the target with ss. Common flags: -l (listening), -t (TCP), -n (no DNS lookups).

So, ss -tln lists listening TCP sockets numerically.


Mental model: L–R–D (Local, Remote, Dynamic)

  • Local port-forward -L LPORT:HOST:TPORT
    Open a listener on your box; traffic goes through SSH to a host/port reachable from the target.
    Use it when: a service is only reachable on the target’s 127.0.0.1 (e.g., Postgres on 5432).

  • Remote port-forward -R RPORT:HOST:LPORT
    Open a listener on the target; traffic goes back through SSH to a host/port reachable from you.
    Use it when: the target must reach a service or handler running on your machine (or another internal box).

  • Dynamic port-forward -D LPORT (SOCKS5)
    Start a SOCKS5 proxy on your box; tools choose destinations on the fly.
    Use it when: you want a general-purpose, “pick any host:port” tunnel – great with ProxyChains.

Lab 1: Local Port Forward (-L): Reaching a local-only database on the target

Goal: Database on the target listens on 127.0.0.1:5432. You can’t install psql there (no sudo), so install the client locally and forward TCP through SSH.

  1. Verify the target’s service is local-only (on the target):
    ss -tln | grep 5432
    # expect: 127.0.0.1:5432 listening (not 0.0.0.0)
    1. Create a local tunnel on Kali (listener on Kali → target’s Postgres):
    ssh -L 1234:127.0.0.1:5432 user@<target-ip>
    # Add -fN to background the tunnel without an interactive shell:
    # ssh -fN -L 1234:127.0.0.1:5432 user@<target-ip>
    • Why -fN? -f backgrounds after authentication; -N tells SSH not to run a remote command (tunnel-only).
    1. Confirm the listener on Kali:
    ss -tlpn | grep 1234
    # should show ssh listening on 127.0.0.1:1234 on YOUR machine
    • Seeing the new local socket is expected: a local port (1234) now forwards into the target’s 5432.
    1. Install the psql client on Kali and connect through the tunnel:
    sudo apt update && sudo apt install -y postgresql-client
    psql -U <db_user> -h 127.0.0.1 -p 1234
    • You install the client locally (Kali), then point -h 127.0.0.1 -p 1234 at your tunnel.
    1. Enumerate databases & tables (inside psql):
    \l         -- list databases
    \c secrets -- connect to the "secrets" DB
    \dt        -- list tables
    SELECT * FROM flag;
    • These are standard psql introspection commands used in the scenario.

    Real-world context: in the exercise this was driven by credentials discovered via password spraying (user / password) and a local-only Postgres. The pivot with -L is the clean way to interact without installing tools on the target.

    Why choose -L? When the remote service isn’t exposed to the network (bound to 127.0.0.1), -L lets you “bring it home” so your local tools can talk to it securely.


    Lab 2: Remote Port Forward (-R) – Let the target reach a service on your machine

    Goal: The target needs to reach something on your machine (or a handler). Classic uses: serving payloads to a host that can’t egress directly, or exposing your local web UI for a remote collaborator.

    1. Start a service on Kali:
    python3 -m http.server 8000
    1. Create a remote tunnel (on Kali):
    ssh -R 9000:127.0.0.1:8000 user@<target-ip>
    • This opens a listener on the target (127.0.0.1:9000 by default), which forwards back through SSH to your 127.0.0.1:8000.
    1. From the target, verify:
    curl http://127.0.0.1:9000

    Variations:

    • Make it listen on all interfaces on the target: ssh -R 0.0.0.0:9000:127.0.0.1:8000 user@<target-ip>
    • Hop through a bastion: ssh -J bastion user@<target-ip> -R 9000:127.0.0.1:8000

    Why choose -R? When the remote needs to consume a service that exists local to you (or on your LAN), or you need the remote side to pull a connection back through your SSH link.


    Lab 3: Dynamic Port Forward (-D) – SOCKS5 pivot + ProxyChains

    Goal: Create a local SOCKS5 proxy that routes arbitrary TCP through the SSH session. Perfect when you’ll probe multiple ports/hosts without re-creating tunnels each time.

    1. Start a SOCKS5 proxy on Kali:
    ssh -fN -D 1080 user@<target-ip>
    • This sets one local port (1080) and dynamically assigns the remote destination per connection.
    1. Configure ProxyChains (Kali):
      Create ~/.proxychains.conf (or edit /etc/proxychains4.conf) with:
    # one of these chain policies (pick ONE)
    strict_chain      # use proxies in the order listed (good for predictable paths)
    #dynamic_chain    # skip dead proxies, keep order
    #random_chain     # pick random proxies each connection
    
    proxy_dns         # send DNS over the proxy (avoids DNS leaks)
    
    [ProxyList]
    # Examples of “anonymous” or privacy-first endpoints:
    # socks5 127.0.0.1 9050      # Tor (if running locally)
    socks5 127.0.0.1 1080        # our SSH -D SOCKS proxy
    • For this scenario, ensure strict_chain is enabled and put the SOCKS entry under [ProxyList].
    1. Use any TCP tool through the tunnel:
    proxychains psql -h 127.0.0.1 -p 5432 -U <db_user>
    proxychains curl http://127.0.0.1:8080
    proxychains nmap -sT -p 22,80,5432 127.0.0.1
    • ProxyChains will be verbose about which proxy a connection used – normal and helpful for troubleshooting.

    Why choose -D? You want a general, reusable path. Instead of rebuilding -L for each new port or host, you keep one SOCKS listener and let each tool decide the destination at runtime.


    Choosing Local vs. Remote vs. Dynamic (real-world scenarios)

    • Local -L
      • Reach “localhost-only” services on the target (DBs, admin panels, metrics endpoints).
      • Safely use your mature toolchain (psql, pg_dump, Burp, browser) without installing on the target.
      • Great when you have creds but not sudo (exactly the Postgres scenario here).
    • Remote -R
      • Expose your local web server, webhook receiver, or reverse shell handler to the target.
      • Collaborate: let the remote host view a local Grafana or notebook via target:9000.
    • Dynamic -D
      • General pivoting over SOCKS for many tools/ports without re-tunneling.
      • Layers nicely with additional proxies (e.g., Tor, commercial SOCKS) for privacy, testing geo-routes, or simulating client connectivity.

    Deep dive: SOCKS5 + ProxyChains

    What is SOCKS5 here?

    A simple, TCP-level proxy protocol. When you run ssh -D 1080 ..., SSH creates a local SOCKS5 server. Any SOCKS-aware client can connect to 127.0.0.1:1080 and request “please connect me to host X on port Y,” and SSH carries that stream through your encrypted session to the remote end, then onward.

    ProxyChains config—common options you’ll actually use

    (Examples shown in a per-user config at ~/.proxychains.conf though /etc/proxychains4.conf works too.)

    # --- Chain policy (choose one) ---
    strict_chain         # Use proxies strictly in the order listed.
    #dynamic_chain       # Skip dead proxies but keep the order of the rest.
    #random_chain        # Pick proxies randomly for each connection.
    
    # --- DNS handling ---
    proxy_dns            # Send DNS over the first proxy to avoid DNS leaks.
    
    # --- Timeouts (milliseconds) ---
    tcp_connect_time_out 5000
    tcp_read_time_out    15000
    
    # --- Noise level ---
    #quiet_mode          # Silence ProxyChains’ connect banners.
    
    # --- Local networks that should bypass proxies (optional) ---
    #localnet 127.0.0.0/255.0.0.0
    #localnet 10.0.0.0/255.0.0.0
    #localnet 192.168.0.0/255.255.0.0
    
    [ProxyList]
    # Stack proxies in order for strict_chain; first is used first.
    # Some responsible “anonymous” options you might use:
    
    # 1) Tor (if the Tor service runs locally)
    # socks5 127.0.0.1 9050
    
    # 2) Your SSH -D dynamic tunnel (this lab)
    socks5 127.0.0.1 1080
    
    # 3) A commercial SOCKS provider endpoint (if you subscribe)
    # socks5 <provider-host> <provider-port>  username password

    Notes on “anonymous proxies”:

    • Tor is easy and widely used for privacy testing; remember many sites block Tor exit nodes.
    • Commercial SOCKS (paid) can offer stable egress IPs and geos; treat providers like you would a VPN—read privacy policies, test for DNS leaks, and avoid violating ToS/laws.
    • Random “free proxy lists” are generally a bad idea: they’re unstable, untrustworthy, and may MITM you. Use only for harmless, throwaway experiments—never for credentials.

    How chaining works: With strict_chain, ProxyChains will try proxies top-to-bottom. With dynamic_chain, it skips dead ones but preserves order. With random_chain, it picks randomly per connection. In this lab, we keep it simple: enable strict_chain and put your SSH -D SOCKS at the bottom of the file under [ProxyList].

    When to use ProxyChains vs. direct SOCKS support:

    • Use ProxyChains with tools that don’t natively support SOCKS (e.g., psql, some nmap modes—prefer -sT instead of SYN scans when proxied).
    • Use native SOCKS (ALL_PROXY=socks5://127.0.0.1:1080) when tools support it; it’s often cleaner.

    Troubleshooting checklist

    • Tunnel not listening where you think it is?
      Run ss -tlpn | grep <port> on the machine where you expect the listener (Kali for -L and -D; target for -R).
    • psql: command not found on the target?
      That’s expected; install psql locally and talk through the tunnel.
    • Connection refused to the forwarded port?
      The tunnel didn’t come up, or you ran -L on the wrong side. Re-run with -vvv to see SSH’s tunnel logs.
    • ProxyChains spew too verbose?
      It’s normal – useful to see which proxy succeeded. You can enable quiet_mode if you prefer silence.

    Quick reference (copy/paste)

    Local (-L) – reach target’s local-only service:

    ssh -fN -L 1234:127.0.0.1:5432 user@<target>
    psql -h 127.0.0.1 -p 1234 -U <dbuser>
    • (Use -fN to background and avoid a remote shell.)

    Remote (-R) – expose your local service to target:

    python3 -m http.server 8000
    ssh -R 9000:127.0.0.1:8000 user@<target>
    # then on target:  curl http://127.0.0.1:9000

    Dynamic (-D) – SOCKS5 for general pivoting:

    ssh -fN -D 1080 user@<target>
    # ~/.proxychains.conf
    strict_chain
    proxy_dns
    [ProxyList]
    socks5 127.0.0.1 1080
    # then:
    proxychains curl http://127.0.0.1:8080
    • (One local port; destinations chosen per connection.)

    Final thoughts

    • -L is your go-to when a service hides behind localhost on the target.
    • -R is your bridge when the target must pull from your machine.
    • -D turns SSH into a flexible SOCKS proxy – pair it with ProxyChains for a powerful, tool-agnostic pivot.

    If you want, I can wrap this into a printable PDF or add simple ASCII diagrams throughout – and we can expand the dynamic lab to include a Tor hop for a controlled “anonymous proxy” chain.

    Stay In Touch.

    Let's Get Creative.