Port Forwarding 101: `Netsh Portproxy` vs Linux Equivalents
# Port Forwarding 101: `netsh portproxy` vs Linux Equivalents
최근 프로젝트에서 사용하는 보드를 원격으로 자리에서 편하게 디버깅하려고 여분 Windows 노트북을 보드와 연결해 두었습니다. Windows 노트북은 사내망(`172.16.10.22`)과 보드망(`192.168.0.x`)에 동시에 붙어 있는 구조였습니다. 다른 사람들도 쉽게 붙을 수 있게 `netsh interface portproxy`로 SSH 점프 포인트를 하나 만들었습니다.
```text
> netsh interface portproxy show all
Listen on ipv4: Connect to ipv4:
Address Port Address Port
--------------- ---------- --------------- ----------
172.16.10.22 2222 192.168.0.10 22
이렇게 하면 저의 민간한 Windows 계정 정보나 SSH 키를 다른 사람들에게 나눠주지 않아도, 노트북 IP(172.16.10.22)의 2222 포트로만 SSH하면 연결된 보드(192.168.0.10:22)로 바로 들어갈 수 있게 만들 수 있었습니다. 172.16.10.22:2222로 들어온 트래픽을 192.168.0.10:22로 넘기는 룰입니다. portproxy 룰만으로는 부족했고, 방화벽·라우팅 등 추가로 맞춰야 할 조건이 있었습니다. 그래서 내부 동작을 살펴보고, 회사 임베디드 보드(Linux)에서도 같은 셋업이 가능한지 확인했습니다.
1. netsh portproxy가 실제로 하는 일
처음 이를 셋업하고 사용하고 있을때는 이게 커널 라우팅으로 동작하는 줄 으례 짐작했습니다. 그런데 막상 자세히 공부해 보니 라우팅이 아니라 user-mode L4 relay였습니다. 커널이 패킷을 고쳐서 넘기는 게 아니라, iphlpsvc(IP Helper) 서비스가 TCP 소켓 두 개를 열어두고 그 사이 바이트를 그대로 복사하는(pump) 방식이었습니다.
동작 순서는 이렇습니다.
iphlpsvc가172.16.10.22:2222에 리스닝 소켓을 bind합니다- 클라이언트가 접속하면 iphlpsvc가 새 소켓을 열어
192.168.0.10:22로 접속합니다 - 두 소켓 사이 데이터를 그대로 복사합니다. 클라이언트 눈에는 연결 하나로 보이지만, 실제로는 서로 다른 TCP 연결 두 개입니다
172.16.10.22
(iphlpsvc / netsh portproxy) participant T as Target
192.168.0.10:22 (sshd) C->>W: TCP connect 172.16.10.22:2222 W->>T: new TCP connect 192.168.0.10:22 Note over W: two independent sockets,
byte-copy relay in user space C-->>T: application data flows through the relay
이 구조를 이해하고 나니 왜 처음에 안 됐는지도 보였습니다. 아래 항목들이 갖춰져 있어야 합니다.
| 체크포인트 | 왜 필요한가 |
|---|---|
iphlpsvc 서비스 실행 (net start iphlpsvc) |
portproxy 룰이 이 서비스에 종속돼 있습니다 |
listen 주소(172.16.10.22)가 이 머신의 실제 NIC IP |
bind()는 로컬 인터페이스에만 걸립니다. 아니면 리스닝 자체가 실패합니다 |
| 방화벽 인바운드 허용 (TCP 2222, 올바른 프로파일) | New-NetFirewallRule -Direction Inbound -Protocol TCP -LocalPort 2222 -Action Allow |
192.168.0.10으로 가는 라우팅 |
대상 서브넷에 물려있는 NIC나 라우트가 있어야 두 번째 소켓 연결이 성공합니다 |
IPEnableRouter(커널 라우팅 플래그)는 건드릴 필요가 없었습니다. portproxy는 라우터가 아니라 소켓 릴레이 프록시이기 때문입니다.
2. Linux에서도 되는지 확인 — 방식 A: socat (같은 개념의 user-space relay)
netsh portproxy와 가장 가까운 건 socat이었습니다. 커널 포워딩이나 iptables 없이, 프로세스 하나가 소켓 두 개를 릴레이해줍니다.
socat TCP-LISTEN:2222,bind=172.16.10.22,fork,reuseaddr TCP:192.168.0.10:22
172.16.10.22
(socat process) participant T as Target
192.168.0.10:22 (sshd) C->>L: TCP connect 172.16.10.22:2222 L->>T: new TCP connect 192.168.0.10:22 Note over L: same relay concept as netsh portproxy —
no ip_forward, no iptables needed C-->>T: application data flows through the relay
S32G274A 같은 임베디드 리눅스 보드에서 상시로 띄워야 한다면 systemd unit 하나만 만들면 됐습니다.
# /etc/systemd/system/ssh-portproxy.service
[Unit]
Description=SSH port relay 172.16.10.22:2222 -> 192.168.0.10:22
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/bin/socat TCP-LISTEN:2222,bind=172.16.10.22,fork,reuseaddr TCP:192.168.0.10:22
Restart=on-failure
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now ssh-portproxy
ssh -L/-R 포트포워딩도 같은 계열(user-space relay)이지만, SSH 세션이 끊기면 같이 죽습니다. 상시 서비스라면 socat + systemd가 더 맞았습니다.
3. Linux 방식 B: iptables/nftables DNAT (커널 레벨 포워딩)
추가로 확인한 방식인데, netsh portproxy보다는 실제 NAT 라우터에 가까웠습니다. 새 소켓을 만드는 게 아니라 커널이 패킷의 목적지 주소/포트를 그 자리에서 고쳐서(rewrite) 그대로 넘깁니다. 클라이언트와 타겟 사이가 하나의 연속된 TCP 연결이라는 점이 앞의 방식과 가장 큰 차이였습니다.
# 커널 포워딩 활성화 (필수)
sysctl -w net.ipv4.ip_forward=1
# PREROUTING: 목적지를 실제 타겟으로 재작성
iptables -t nat -A PREROUTING -p tcp -d 172.16.10.22 --dport 2222 \
-j DNAT --to-destination 192.168.0.10:22
# FORWARD: 재작성된 패킷이 지나가도록 허용
iptables -A FORWARD -p tcp -d 192.168.0.10 --dport 22 -j ACCEPT
# POSTROUTING: 리턴 트래픽이 제대로 돌아오게 하려면 (라우팅 구조에 따라 필요)
iptables -t nat -A POSTROUTING -p tcp -d 192.168.0.10 --dport 22 -j MASQUERADE
dst → 192.168.0.10:22"] P --> F["FORWARD chain
net.ipv4.ip_forward=1"] F --> M["POSTROUTING / MASQUERADE
(조건부)"] M -->|"same conn, header rewritten"| T[Target 192.168.0.10:22] T -.->|"return traffic, rewritten again"| C
MASQUERADE는 매번 필요하진 않았습니다. 타겟(192.168.0.10)이 리턴 패킷을 다시 이 라우터로 보낼 수 있는 구조(예: 기본 게이트웨이가 이 라우터)라면 생략해도 됩니다. 확실하지 않으면 넣어 두는 편이 안전했습니다.
4. 한눈에 비교
| netsh portproxy (Windows) | socat (Linux) | iptables DNAT (Linux) | |
|---|---|---|---|
| 계층 | User-space, L4 소켓 릴레이 | User-space, L4 소켓 릴레이 | 커널, 패킷 레벨 NAT |
| TCP 연결 개수 | 2개 (독립) | 2개 (독립) | 1개 (헤더만 재작성) |
ip_forward 필요? |
불필요 | 불필요 | 필요 |
| 리스닝 IP 요건 | 로컬 NIC에 bind된 실제 IP | 로컬 NIC에 bind된 실제 IP | 없음 (패킷 가로채기라 bind 불필요) |
| 적합한 상황 | 단일 서비스 점프/릴레이 | 단일 서비스 점프/릴레이, 데몬화 쉬움 | 여러 포트/서비스를 라우터처럼 통째로 포워딩 |
5. 트러블슈팅 체크리스트
작업하면서 안 될 때마다 아래 순서대로 확인했습니다.
- 리스닝이 실제로 떠 있는지 —
netstat -an | findstr 2222또는Get-NetTCPConnection -LocalPort 2222(Windows) /ss -tlnp | grep 2222(Linux) - 방화벽이 해당 포트/프로파일을 막고 있는지
- 릴레이 호스트에서 타겟으로 라우팅이 되는지 —
ping 192.168.0.10,route print/ip route - (iptables DNAT만)
net.ipv4.ip_forward가 1인지, 리턴 경로에MASQUERADE가 필요한 구조인지 - 클라이언트 쪽에서
Test-NetConnection 172.16.10.22 -Port 2222또는nc -zv 172.16.10.22 2222로 최종 확인