~/apparatus/the-proxy-and-the-chain.md
The proxy and the chain
These pages are props, not lessons. No tier, no order, no exercise of their own. A lesson that names this page owes you the means, and this page pays.
The law travels with the props. Everything here runs inside one machine you own, in docker networks that touch nothing real. The far network in this lab is a second docker bridge, not somebody's subnet. The reverse tunnel shape connects two containers you started. Nothing here leaves your box.
The three network lab, stood up
Three docker networks, one hop wired into all of them, one web box the hop can reach and you cannot.
$ docker network create near-net --subnet 172.31.1.0/24
$ docker network create far-net --subnet 172.31.2.0/24
$ docker network create mid-net --subnet 172.31.3.0/24
$ docker run -d --name hop --network near-net -p 127.0.0.1:2222:2222 linuxserver/openssh-server
$ docker run -d --name target-web --network far-net nginx:alpine
$ docker network connect mid-net hop
$ docker network connect far-net hop
Reading it. near-net is yours: your loopback reaches it through the published port 2222. far-net is the target network: no port is published from it anywhere, so from your seat it does not exist. The hop joins near-net at birth, then docker network connect wires it into the other two, which is the lab's whole point in one command: one machine, three faces, the middle of your diagram.
Find the addresses once the lab is up:
$ docker inspect target-web --format '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}'
172.31.2.2
The hop's key, three honest steps
The openssh image ships with password login off and AllowTcpForwarding no, and both must change before any tunnel lives. The image's sshd reads /config/sshd/sshd_config, which is the file to edit, not /etc/ssh/sshd_config. Its user is linuxserver.io with home /config.
$ ssh-keygen -t ed25519 -f lab39key -N ""
$ docker cp lab39key.pub hop:/config/.ssh/lab.pub
$ docker exec hop sh -c 'mkdir -p /config/.ssh && cat /config/.ssh/lab.pub > /config/.ssh/authorized_keys && chmod 700 /config/.ssh && chmod 600 /config/.ssh/authorized_keys && chown -R 911:911 /config/.ssh && sed -i "s/^AllowTcpForwarding no/AllowTcpForwarding yes/" /config/sshd/sshd_config'
Then restart the daemon by its own full invocation, because a plain service ssh restart does not exist in this image:
$ docker exec hop sh -c 'pkill sshd; sleep 2; /usr/sbin/sshd.pam -D -e -f /config/sshd/sshd_config -h /config/ssh_host_keys/ssh_host_ecdsa_key -h /config/ssh_host_keys/ssh_host_ed25519_key -h /config/ssh_host_keys/ssh_host_rsa_key &'
And the receipt that the seat exists:
$ ssh -i lab39key -p 2222 linuxserver.io@127.0.0.1 whoami
linuxserver.io
The lesson here is the debugging, not the typing. A reset connection with no sshd log line usually means the port publish points at the wrong container port. A Permission denied (publickey) with the key in place usually means the key landed in root's home while sshd serves linuxserver.io. And channel open failure with the tunnel otherwise up is AllowTcpForwarding no still in force. Three walls, three one-line fixes, all of them met and passed building this page.
The three shapes, fired
Port forward, the door:
$ ssh -i lab39key -p 2222 -N -L 127.0.0.1:8080:172.31.2.2:80 linuxserver.io@127.0.0.1 &
$ curl -s http://127.0.0.1:8080/ | head -1
<!DOCTYPE html>
Dynamic, the corridor:
$ ssh -i lab39key -p 2222 -N -D 127.0.0.1:1080 linuxserver.io@127.0.0.1 &
$ curl -s --socks5 127.0.0.1:1080 http://172.31.2.2/ | head -1
<!DOCTYPE html>
The same page, two seats: once through the door, once through the corridor. If both answer the nginx page, the lab is alive and every later receipt has a place to run.
nmap through the corridor, two honest forms. proxychains when it is installed, and nmap's own socks support when it is not:
$ nmap --proxy socks4://127.0.0.1:1080 -sT -Pn -p 22,80,443 172.31.2.2
PORT STATE SERVICE
22/tcp filtered ssh
80/tcp open http
443/tcp filtered https
The -sT and -Pn are not decoration, and the lesson says why: socks carries full connections, not half-open packets, and the probe would have to ride the corridor too.
Reverse, the door that opens outward
The reverse shape needs a you-box the hop can reach. In this lab that is a second ssh container standing in for your cloud host:
$ docker run -d --name listener --network near-net -p 127.0.0.1:2299:2222 linuxserver/openssh-server
Give it the same key-and-forwarding setup as the hop, then give the hop a key of its own and add that key to the listener, because the reverse tunnel is the hop ssh-ing outward, and it must authenticate like anyone else:
$ docker exec hop ssh-keygen -t ed25519 -f /config/.ssh/id_ed25519 -N '""'
$ docker exec hop cat /config/.ssh/id_ed25519.pub
$ docker exec listener sh -c 'mkdir -p /config/.ssh && echo PASTE-HOP-PUB-HERE >> /config/.ssh/authorized_keys && chown -R 911:911 /config/.ssh'
The firing, typed on the hop, asking the listener to hold a door into the far net:
$ docker exec hop ssh -i /config/.ssh/id_ed25519 -p 2222 -o StrictHostKeyChecking=no -N -R 127.0.0.1:8080:172.31.2.2:80 linuxserver.io@172.31.1.3 &
And the receipt, read from the listener: its own port 8080, which nothing outside the hop ever connected to, now serves the far network's web page:
$ docker exec listener wget -q -O- http://127.0.0.1:8080/ | head -1
<!DOCTYPE html>
One machine behind NAT-equivalent isolation just reached a network it has no route to, by opening a door outward. That is the whole third shape.
Timing receipts
The lab runs on one machine, so its numbers understate a real engagement. They still teach the shape. Ten page fetches, direct on loopback against through the socks corridor: 0.048 seconds direct, 0.058 through the corridor. About one millisecond of per-connection overhead on a loopback-scale hop, where a real wide-area hop costs tens to hundreds. The lesson's ratio multiplies with distance and with connection count, which is why the assessment through the tunnel is paced by the tunnel.
The sweep receipt: a full /24 at five ports through the corridor took 15.8 seconds, 256 hosts up, with everything but the live target reading filtered because the docker bridge declines to answer for addresses that have no container. On a real network those same rows fill with real services and the sweep takes as long as it takes. The number to carry away is not 15.8, it is that the corridor sets the pace.
Tooling notes, both wrappers
proxychains wraps programs that have no socks support of their own. Its config wants strict_chain, and a proxy list with one line, socks5 127.0.0.1 1080. The banner check is the receipt that the chain is even loaded: run proxychains -v curl against any address and look for [proxychains] on stderr. No banner, no chain, and the tool has been talking to the far address on its own, unreachable, and lying to you with connection refused.
nmap speaks socks4 natively through --proxy socks4://127.0.0.1:1080, which is worth knowing on a box where installing proxychains is not yours to do. Same two flags, -sT -Pn, same reasons.
curl takes --socks5 host:port per command. Browsers take a socks proxy in their connection settings, with the loopback-resolves-DNS checkbox ticked, which is the same idea as proxychains' proxy_dns.
Take it down
$ docker rm -f hop target-web listener
$ docker network rm near-net far-net mid-net
The lab that forgets nothing is a lab that lingers. Down in one paste, and the three networks are gone.