배경
HAProxy 뒤에 있는 백엔드 서버는 클라이언트 IP를 직접 볼 수 없습니다. 요청이 HAProxy를 거쳐 오기 때문에 백엔드 입장에서는 출발지 IP가 HAProxy IP로 보입니다.
클라이언트 실제 IP를 백엔드에 전달하는 방법은 세 가지입니다.
| 방법 | 특징 |
| X-Forwarded-For | HTTP 헤더로 전달. HTTP 전용, 어플리케이션이 헤더를 읽도록 수정 필요 |
| Transparent Proxy | 네트워크 레이어에서 출발지 IP 자체를 클라이언트 IP로 변조. 어플리케이션 수정 불필요, 인프라 변경 필요 |
| Proxy Protocol | TCP 연결 시작 시 클라이언트 정보를 헤더로 전달. HTTP/TCP 모두 적용 가능, 백엔드에서 파싱 지원 필요 |
이 글은 X-Forwarded-For로 해결되지 않는 TCP 트래픽 환경을 중심으로, Transparent Proxy의 설정 전 과정과 더 실용적인 대안인 Proxy Protocol을 함께 정리한 것입니다.
1. 동작 원리
일반 프록시 vs Transparent Proxy
일반 프록시
Client (1.1.1.1) → HAProxy (10.0.0.1) → Backend
↑
Backend가 보는 출발지 IP: 10.0.0.1 (HAProxy IP)
Transparent Proxy
Client (1.1.1.1) → HAProxy (10.0.0.1) → Backend
↑
Backend가 보는 출발지 IP: 1.1.1.1 (Client 실제 IP)
왜 라우팅 설정이 필요한가
HAProxy가 클라이언트 IP를 출발지로 패킷을 보내면, 백엔드 서버는 클라이언트 IP로 응답을 돌려보내려 합니다. 그런데 클라이언트는 백엔드 서버와 직접 연결된 네트워크에 없으므로 게이트웨이를 통해야 합니다.
이 때문에 두 가지 작업이 필요합니다.
- 백엔드 서버의 기본 게이트웨이를 HAProxy로 변경 — 응답 패킷이 HAProxy를 통해 나가도록
- HAProxy에서 백엔드의 응답 패킷 처리 — 자신에게 온 패킷을 로컬로 처리하도록 라우팅 규칙 추가
2. HAProxy 서버 사전 설정
HAProxy가 동작하는 서버에 아래 두 가지 커널 파라미터를 먼저 활성화해야 합니다.
vi /etc/sysctl.conf
# 패킷 포워딩 활성화
net.ipv4.ip_forward = 1
# 로컬에 없는 IP 주소로 바인딩 허용 (클라이언트 IP를 출발지로 쓰기 위해 필요)
net.ipv4.ip_nonlocal_bind = 1
sysctl -p
3. HAProxy 설정
주의: TCP 모드만 지원
Transparent Proxy는 HTTP 모드에서 지원되지 않습니다. mode tcp로 설정해야 합니다.
권한 설정
Transparent Proxy는 CAP_NET_RAW 권한이 필요합니다. 두 가지 방법이 있습니다.
# 방법 1: cap_net_raw 권한 부여 (권장)
setcap cap_net_raw+ep /usr/sbin/haproxy
# 방법 2: root로 실행 (setcap이 동작하지 않을 경우)
# haproxy.cfg global 섹션에서 user/group을 root로 설정
vi /etc/haproxy/haproxy.cfg
global
user root
group root
frontend test_frontend
mode tcp # http 모드에서는 Transparent 지원 안됨
bind 1.1.1.1:80 transparent # transparent 옵션 추가
backend test_backend
source 0.0.0.0 usesrc clientip # 출발지 IP를 클라이언트 IP로 사용
핵심 옵션 두 가지입니다.
| 옵션 | 위치 | 설명 |
| bind ... transparent | frontend | 해당 IP로 바인딩 시 transparent 소켓 사용 |
| source 0.0.0.0 usesrc clientip | backend | 백엔드로 보낼 때 클라이언트 IP를 출발지로 설정 |
3. 방화벽 설정
백엔드 서버에서 응답 패킷이 HAProxy로 돌아올 때 올바르게 처리되도록 mangle 테이블에 규칙을 추가합니다.
# DIVERT 체인 생성
firewall-cmd --permanent --direct --add-chain ipv4 mangle DIVERT
# 이미 열려있는 소켓(TCP established)으로 들어오는 패킷을 DIVERT로
firewall-cmd --permanent --direct --add-rule ipv4 mangle PREROUTING 0 -p tcp -m socket -j DIVERT
# DIVERT 체인: 패킷에 마크 1 설정
firewall-cmd --permanent --direct --add-rule ipv4 mangle DIVERT 0 -j MARK --set-mark 1
# DIVERT 체인: ACCEPT
firewall-cmd --permanent --direct --add-rule ipv4 mangle DIVERT 1 -j ACCEPT
이 규칙의 흐름입니다.
백엔드 응답 패킷 도착
↓
PREROUTING: 이미 열린 소켓? → DIVERT 체인으로
↓
DIVERT: 마크 1 부여 → ACCEPT
↓
라우팅 규칙에서 마크 1 패킷을 로컬 처리
4. 라우팅 설정
마크 1이 붙은 패킷을 로컬에서 처리하도록 라우팅 규칙을 추가합니다.
# 마크 1인 패킷은 라우팅 테이블 100 사용
ip rule add fwmark 1 lookup 100
# 라우팅 테이블 100: 모든 IP를 로컬(lo)로 처리
ip route add local 0.0.0.0/0 dev lo table 100
이 두 줄 덕분에 백엔드 서버가 클라이언트 IP로 보낸 응답이 HAProxy에서 로컬 소켓으로 처리됩니다.
5. 재부팅 후에도 유지하기
ip rule, ip route 명령은 재부팅하면 초기화됩니다. rc.local에 등록하여 부팅 시 자동 적용합니다.
vi /etc/rc.d/rc.local
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
exit 0
# 실행 권한 부여
chmod 755 /etc/rc.d/rc.local
# rc-local 서비스 활성화
vi /lib/systemd/system/rc-local.service
# [install] 섹션에 아래 추가
# WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable rc-local
systemctl start rc-local
6. 백엔드 서버 설정
게이트웨이 변경
백엔드 서버의 기본 게이트웨이를 HAProxy IP로 변경합니다. 클라이언트 IP로 보내는 응답이 반드시 HAProxy를 통해 나가야 합니다.
기존 게이트웨이: 192.168.100.1 (L3 스위치 등)
변경 후: 192.168.100.10 (HAProxy IP)
ICMP Redirect 비활성화
게이트웨이를 변경하면 기존 게이트웨이가 "더 좋은 경로가 있다"는 ICMP Redirect를 보낼 수 있습니다. 이를 무시하도록 설정합니다.
vi /etc/sysctl.conf
net.ipv4.conf.all.accept_redirects=0
sysctl -p
# net.ipv4.conf.all.accept_redirects = 0
7. 동작 확인
백엔드 서버(Nginx)의 access 로그를 확인합니다.
tail -f /var/log/nginx/access.log
192.168.137.1 - - [06/Apr/2023:18:25:33 +0900] "GET / HTTP/1.1" 200 4074 ...
192.168.137.1이 클라이언트 실제 IP입니다. X-Forwarded-For 헤더 없이도 출발지 IP가 클라이언트 IP로 기록됩니다.
8. 주의사항
동일 서브넷 클라이언트에서는 동작하지 않습니다.
HAProxy와 클라이언트가 같은 서브넷에 있을 때 VIP로 요청하는 경우, 응답 패킷이 HAProxy를 거치지 않고 직접 클라이언트에게 전달되어 TCP 세션이 깨집니다.
같은 서브넷 클라이언트 → VIP → HAProxy (투명 프록시로 Client IP 사용)
백엔드 응답 → Client IP로 직접 전송 (HAProxy 우회) → 세션 깨짐
이 경우 해당 대역은 X-Forwarded-For 방식을 사용하거나, 별도 네트워크 분리를 고려해야 합니다.
10. Proxy Protocol — 더 단순한 대안
Transparent Proxy가 네트워크 레이어에서 IP를 바꾸는 방식이라면, Proxy Protocol은 TCP 연결 시작 시 클라이언트 정보를 헤더로 전달하는 방식입니다. 백엔드 서버가 해당 헤더를 파싱하도록 지원하면 되고, 게이트웨이 변경이나 복잡한 라우팅 설정이 필요 없습니다.
동작 방식
Client (1.1.1.1) → HAProxy → Backend
↓
TCP 연결 직후 이 한 줄을 먼저 전송
PROXY TCP4 1.1.1.1 10.0.0.1 12345 80\r\n
백엔드는 첫 줄을 읽고 클라이언트 IP를 파악합니다. 이후 데이터는 평소와 동일합니다.
v1 vs v2
| v1 | v2 | |
| 형식 | 텍스트 | 바이너리 |
| 가독성 | 사람이 읽을 수 있음 | 기계 파싱 효율적 |
| 추가 메타데이터 | 없음 | TLV로 SSL 정보 등 추가 가능 |
| HAProxy 옵션 | send-proxy | send-proxy-v2 |
HAProxy 설정
보내는 쪽 (Backend)
backend webservers
# v1 (텍스트)
server s1 192.168.56.20:3000 check send-proxy
# v2 (바이너리, 권장)
server s1 192.168.56.20:3000 check send-proxy-v2
받는 쪽 (Frontend) — HAProxy가 Proxy Protocol을 받아야 할 때
frontend mywebsite
bind :80 accept-proxy # v1, v2 모두 자동 감지
default_backend webservers
백엔드 서버 설정 (Nginx)
server {
listen 80 proxy_protocol;
# Proxy Protocol 헤더를 보내는 HAProxy IP만 신뢰
set_real_ip_from 10.0.0.1;
real_ip_header proxy_protocol;
}
Transparent Proxy vs Proxy Protocol
| Transparent Proxy | Proxy Protocol | |
| 인프라 변경 | 게이트웨이 변경 필요 | 없음 |
| 백엔드 수정 | 불필요 | PP 파싱 지원 필요 |
| 적용 프로토콜 | TCP 전용 | TCP 기반 모두 (HTTP, MySQL 등) |
| 클라우드 환경 | 어려움 | AWS NLB 등 기본 지원 |
| 설정 복잡도 | 높음 | 낮음 |
HTTP 트래픽이라면 X-Forwarded-For, TCP 프로토콜이라면 Proxy Protocol이 대부분의 상황에서 더 실용적인 선택입니다.
마치며
백엔드 서버에서 클라이언트 IP를 보는 방법은 상황에 따라 다릅니다.
Transparent Proxy는 인프라 변경 범위가 크고 제약이 많아 적용 가능한 환경이 한정적입니다. 다만 설정 과정을 따라가다 보면 HAProxy가 패킷을 어떻게 처리하는지, 커널의 mangle 테이블과 policy-based routing이 어떻게 맞물리는지 구체적으로 이해할 수 있습니다.
Proxy Protocol은 게이트웨이 변경 없이 설정만으로 클라이언트 IP를 전달할 수 있어 실무에서 더 자주 선택됩니다. HTTP가 아닌 TCP 프로토콜(MySQL, Redis 등)에도 적용할 수 있고, AWS NLB 같은 클라우드 환경에서도 기본으로 지원됩니다.
둘을 알아두면 상황에 맞는 방법을 고를 수 있습니다.
참고
'system' 카테고리의 다른 글
| 리눅스 격리 기술 - cgroup, namespace, union mount (0) | 2026.06.04 |
|---|---|
| 도와줘! strace (0) | 2026.06.04 |
| 인프라 모니터링: 무엇을, 왜, 어떻게 볼 것인가 (0) | 2026.06.04 |
| 시스템 성능 분석: CPU, 메모리, 디스크, 네트워크 (0) | 2026.06.04 |
| 스토리지 종류와 구성 방식 정리 (0) | 2026.06.04 |