
현재 구조는 대략 이렇게 되어 있습니다.
[클라이언트 PC] ──(인터넷/사내망)──> [Windows 호스트] ──(netsh portproxy)──> [WSL2]
80/tcp 80/tcp
Windows에서 이런 식으로 설정
netsh interface portproxy add v4tov4 `
listenaddress=0.0.0.0 listenport=80 `
connectaddress=<WSL_IP> connectport=80
WSL에서 웹서버(예: Nginx, Apache, Node 앱 등)를 80 포트에 띄워놓았고, 외부에서 접속하면 서비스는 잘 되지만 WSL에서 보이는 클라이언트 IP가 전부 Windows 게이트웨이 IP로만 보이는 문제가 있습니다.
예를 들면
- WSL에서
ss -tn해보면:결과가 전부src = 172.19.144.1같은 WSL 게이트웨이 IP로 보임ss -tn sport = :80 - Nginx
access.log에도:처럼 실제 클라이언트가 아닌 WSL 게이트웨이 IP만 찍힘172.19.144.1 - - [08/Dec/2025:10:00:00 +0900] "GET / HTTP/1.1" 200 ...
원인: netsh portproxy + WSL2 네트워크 구조
이 문제는 단순 설정 실수나 옵션 부족이 아니라, 구조적·설계적 한계에 가깝습니다.
1. WSL2 네트워크 구조
WSL2는 사실상 작은 VM이라서
- Windows 호스트와는 가상 스위치 (vSwitch) 로 연결된 별도 네트워크
- Windows 호스트에서 보면 WSL은 내부 사설망(172.x.x.x 등)에 있는 별도 머신
이때 외부에서 들어오는 트래픽은
- 먼저 Windows 호스트가 받음
- Windows가 내부에서 WSL VM으로 NAT 또는 포트 프록시(netsh)로 재전달
2. netsh interface portproxy 동작 방식
netsh interface portproxy는 커널 레벨의 포트 포워딩이라기보다는 “소켓 기반 간접 프록시”에 가깝습니다.
- Windows가
listenaddress:listenport로 소켓을 열고 - 연결이 들어오면 새로운 outbound 연결을
connectaddress:connectport로 생성 - 이 outbound 연결의 소스 IP는 항상 Windows 호스트(혹은 vNIC) IP
따라서
- 원본 클라이언트 IP는 Windows에서 이미 끊겨버리고
- WSL로 갈 때는 “Windows → WSL” 연결만 남습니다.
- WSL 입장에서는 항상 Windows IP에서 오는 TCP 세션으로만 보게 됩니다.
👉 결론
netsh 포트프록시를 사용하는 한, WSL 내부에서 커널/소켓 레벨(REMOTE_ADDR)에서 “실제 클라이언트 IP”를 그대로 보는 것은 구조적으로 불가능합니다.
이건 설정 잘못이 아니라 원리 자체의 한계입니다.
현실적인 해결 전략의 방향
그렇다면 “실제 IP를 남기고 싶다”는 요구사항을 어떻게 충족시킬 수 있느냐?
핵심 아이디어는
L4 레벨(IP/포트)에서 해결하려 하지 말고,
L7(HTTP 레벨)에서 프록시를 두고 “헤더”로 IP를 전달하게 만들자.
즉
- Windows에서 HTTP 리버스 프록시 (Nginx, IIS+ARR 등)를 사용
- 이 프록시가 클라이언트의 실제 IP를 알고 있으므로
- WSL로 요청을 넘길 때
X-Forwarded-For,X-Real-IP같은 헤더에 원본 IP를 넣어줌 - WSL 내부 웹서버/애플리케이션은 이 헤더를 기반으로 로그를 남김
추천 구조: Windows 리버스 프록시 + WSL 서비스
기존 구조
클라이언트 → Windows(netsh portproxy:80) → WSL:80
권장 구조
클라이언트 → Windows(Nginx or IIS 80) → WSL:80
그리고 netsh portproxy는 아예 제거하거나 최소한 외부 접속용 HTTP 포트(80/443)에는 쓰지 않는 것을 추천합니다.
예시 A: Windows Nginx를 리버스 프록시로 사용
(1) 구성 개요
[클라이언트] ───> [Windows:80 - Nginx] ───> [WSL:80 - Web Server(Nginx/Apache/App)]
(실제 IP를 알고 있음) (X-Forwarded-For 기반으로 IP 인식)
(2) Windows Nginx 설치 (요약)
- 공식 Nginx for Windows zip 다운로드 후 압축 해제
- 예:
C:\nginx위치에 배치 - 테스트 실행
cd C:\nginx start nginx.exe - Windows 방화벽에서 80 포트 inbound 허용
(운영용이면 nssm 같은 도구로 Windows 서비스로 등록해서 자동 시작하게 구성 가능)
(3) Windows Nginx 설정 예시
C:\nginx\conf\nginx.conf (핵심 부분만 예시)
worker_processes 1;
events {
worker_connections 1024;
}
http {
# 로그 형식 정의
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log logs/access.log main;
# WSL 서비스 주소 (WSL IP는 ifconfig/ipconfig / wsl hostname -I 등으로 확인)
upstream wsl_service {
server 172.21.23.5:80; # 예: WSL IP
}
server {
listen 80;
server_name _;
location / {
proxy_pass http://wsl_service;
# ===== 여기서 실제 클라이언트 IP 전달 =====
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
}
}
}
이제
- 클라이언트 → Windows:80 (Nginx)
- Nginx가 클라이언트의 실제 IP를
$remote_addr로 알고 있음 - 이를
X-Real-IP,X-Forwarded-For에 담아서 WSL로 전달
여기까지 하면 Windows 측 로그(access.log) 에는 이미 실제 IP가 찍힙니다.
(4) WSL 내부 Nginx에서 X-Forwarded-For 기반으로 IP 인식
WSL 내부 Nginx 설정 (/etc/nginx/nginx.conf 예시)
http {
# 프록시(Nginx for Windows)의 IP를 신뢰
# 실제 환경에 맞는 IP/대역 입력 (예: 172.21.23.1/32 또는 사내 대역)
set_real_ip_from 172.21.23.0/24;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/access.log main;
server {
listen 80;
server_name _;
location / {
proxy_pass http://127.0.0.1:8080; # 뒤에 애플리케이션이 있으면
}
}
}
이제 /var/log/nginx/access.log를 보면
203.0.113.45 - - [08/Dec/2025:11:00:00 +0900] "GET / HTTP/1.1" 200 ...
처럼 실제 외부 클라이언트 IP가 $remote_addr로 찍힙니다.
예시 B: IIS + ARR로 리버스 프록시 구성
IIS를 선호하거나 이미 쓰고 있다면 아래 구조도 가능합니다.
클라이언트 → Windows:80(IIS+ARR) → WSL:80
(1) 기본 단계
- IIS 설치 (Web Server 역할)
- Application Request Routing(ARR) 및 URL Rewrite 모듈 설치
- IIS 관리자에서
- “서버” 선택 → “Application Request Routing Cache” → “Server Proxy Settings”
- “Enable proxy” 체크
- “사이트”에서
Default Web Site또는 별도 사이트를 생성 후 “Reverse Proxy…” 메뉴 선택- Back-end 서버로
http://<WSL_IP>:80입력
- Back-end 서버로
(2) X-Forwarded-For 헤더 설정
ARR는 보통 자동으로 X-Forwarded-For를 넣도록 옵션이 있습니다.
- “Server Proxy Settings(서버 프록시 설정)”에서
- “X-Forwarded-For 헤더 사용” 체크
- 또는 URL Rewrite 규칙에서 직접 헤더 추가
(3) WSL 내부 웹서버 설정
역시 동일하게
- Nginx →
real_ip_header X-Forwarded-For; - Apache →
mod_remoteip설정
예: Apache (httpd.conf)
LoadModule remoteip_module modules/mod_remoteip.so
RemoteIPHeader X-Forwarded-For
RemoteIPTrustedProxy 172.21.23.1 # IIS 서버 IP
LogFormat "%a %l %u %t \"%r\" %>s %b" common
CustomLog logs/access_log common
이렇게 하면 %a 에 실제 클라이언트 IP가 기록됩니다.
netsh portproxy를 꼭 유지해야 한다면?
정직하게 말하면
“netsh portproxy를 유지하면서도 WSL 내부에서 REMOTE_ADDR를 실제 IP로 만드는 방법은 없다”
(IP 레벨에서 NAT/TCP 재연결이 이미 끝난 상태)
현실적인 타협은 다음 두 가지 정도입니다.
- Windows에서 별도 로그를 먼저 남기는 방식
- netsh portproxy 앞단에 HTTP 프록시를 하나 더 두고, 그 프록시에서 실IP 로그
- 또는 Windows 방화벽 고급 로깅, 커스텀 서비스 등으로 IP 기록
→ 하지만 이 경우 “실제 서비스 로그(WSL)와 IP 로그(Windows)가 분리”되어 SIEM 통합이 번거롭고,
WSL만 보고는 클라이언트 식별이 어려움
- netsh는 개발/테스트 용도로만 사용하고
실제 서비스/보안 관점에서는 리버스 프록시 구조를 표준으로 가져가는 것
보안·운영 측면에서 보면 두 번째가 훨씬 건전한 구조입니다.
보안 관점에서의 가이드 & 점검 포인트
보안 입장에서 내부 사용자/개발팀에 제공할 수 있는 “정책/가이드” 형태로 정리해보면
1. 아키텍처/설계 정책
- WSL2 + netsh portproxy 구조에서는 원본 IP가 WSL에 보이지 않는다는 점을 공식화
- “이는 시스템 구조 특성으로 인한 제한이며, 보안 로그 기준을 L7(프록시) 레벨로 전환해야 한다”는 내용을 문서화
- 표준 접속 구조 정의
- 외부/사내 클라이언트 → Windows 리버스 프록시(Nginx or IIS) → WSL 서비스
- HTTP/HTTPS 포트(80/443)에 대해서는 netsh portproxy 사용 금지(권고 수준이 아니라 policy 수준으로)
- 표준 헤더 규칙
- 리버스 프록시는 항상 다음 헤더를 삽입/관리
X-Forwarded-For: 클라이언트 IP(프록시 체인 포함)X-Real-IP: 바로 앞단 클라이언트 IPX-Forwarded-Proto,X-Forwarded-Host등 필요시
- 외부에서 들어오는 임의의
X-Forwarded-For는 걷어내고, 프록시가 새로 생성하도록 설정
(IP 스푸핑 방지)
- 리버스 프록시는 항상 다음 헤더를 삽입/관리
2. 애플리케이션/웹서버 설정 가이드
- WSL 내부 웹서버는 REMOTE_ADDR 대신 헤더 기반으로 IP 사용
- Nginx
set_real_ip_from <프록시IP>real_ip_header X-Forwarded-Forreal_ip_recursive on
- Apache
mod_remoteip로RemoteIPHeader X-Forwarded-For설정
- Node.js (Express)
app.set('trust proxy', true); // 또는 'loopback', '172.21.23.1' 등 프록시 한정 app.use((req, res, next) => { console.log('client ip:', req.ip); // X-Forwarded-For 기반 next(); }); - Flask
from flask import request @app.route("/") def index(): real_ip = request.headers.get("X-Forwarded-For", request.remote_addr) print("client ip:", real_ip) return "ok"
- Nginx
- 신뢰 범위 제한
trust proxy혹은set_real_ip_from에 내부 프록시의 IP만 지정
아무 IP나 X-Forwarded-For를 보낸다고 믿지 않도록
3. 로그 포맷 & SIEM 연계
- 웹서버 로그 포맷 표준화
- 예: Nginx
log_format security '$remote_addr [$time_local] ' 'xff="$http_x_forwarded_for" ' 'host="$host" request="$request" ' 'status=$status bytes=$body_bytes_sent ' 'ua="$http_user_agent" ref="$http_referer"'; access_log /var/log/nginx/security.log security; $remote_addr→ 최종적으로 실제 클라이언트 IP (XFF 기반)$http_x_forwarded_for→ 전체 프록시 체인- 필요하다면 WSL이 실제로 받은 REMOTE_ADDR(게이트웨이 IP)도 별도 필드로 로그에 남길 수 있음
(이건 변조 위험이 없으니 forensic 용도로 쓸 수 있음)
- 예: Nginx
- SIEM/EDR 필드 매핑
source.ip/client.ip=$remote_addr(실제 클라이언트 IP)proxy.ip/gateway.ip= WSL의 REMOTE_ADDR (게이트웨이 IP)- 탐지 룰, 대시보드는
client.ip기준으로 설계
- 운영 체크리스트
- Windows 리버스 프록시가 정상 동작 중인가?
- WSL 내 웹서버가
X-Forwarded-For기반으로 IP를 인식하고 있는가? - 무작위 접속 테스트 시, WSL 로그에 실제 클라이언트 IP가 찍히는지 확인
- SIEM에서 source.ip 필드가 실제 IP로 들어오는지 검증
- 외부에서 임의의
X-Forwarded-For값을 넣어도 로그에 반영되지 않는지(프록시에서 필터링하는지) 테스트
최종 정리
- 문제의 본질
- WSL2 +
netsh interface portproxy구조에서는 Windows가 TCP를 다시 열어 전달하기 때문에
WSL에서는 항상 Windows/게이트웨이 IP만 보입니다. - 이건 옵션으로 해결 가능한 문제가 아니라 구조 자체의 한계입니다.
- WSL2 +
- 실제 클라이언트 IP를 로그에 남기는 현실적인 방법
- Windows에 리버스 프록시(Nginx, IIS+ARR 등)를 두고
- 이 프록시가
X-Forwarded-For/X-Real-IP헤더로 원본 IP를 WSL로 전달 - WSL 내부 웹서버/앱은 이 헤더를 기반으로 로그를 찍도록 설정
- 보안/운영 관점
- HTTP/HTTPS 서비스 표준 구조를 “리버스 프록시 → WSL”로 정하고
- netsh portproxy는 개발용·임시용 수준으로 제한하는 것이 바람직
- 헤더 신뢰 범위, 로그 포맷, SIEM 매핑을 사내 표준으로 문서화
댓글