TPROXY 완전 실습 랩
TCP·UDP·nftables·netns·C epoll 투명 프록시·eBPF TC hook·Squid/Envoy/HAProxy 연동까지 8개 Lab으로 구성된 단계별 실습 가이드입니다. 모든 코드와 명령은 직접 복사하여 실행할 수 있습니다.
전제 조건: 이 실습을 진행하기 전에 다음 문서를 먼저 읽으세요.
- TPROXY (투명 프록시) — 커널 내부 구조, IP_TRANSPARENT, nf_tproxy_get_sock, fwmark/정책 라우팅 원리
- Netfilter 프레임워크 — iptables/nftables mangle 체인, 훅 시스템
- 라우팅 서브시스템 — ip rule, ip route, 정책 라우팅 테이블
실습 목표: 각 Lab은 독립적으로 실행 가능합니다. Lab 1(기본 TCP)부터 순서대로 진행하거나, 관심 있는 Lab을 바로 실행할 수 있습니다. 각 Lab 끝에 정리(clean-up) 명령이 포함되어 있어 시스템 상태를 원래대로 복원할 수 있습니다.
핵심 요약
- TPROXY 핵심 3요소 — mangle/PREROUTING TPROXY 타겟 + fwmark 기반 정책 라우팅 + IP_TRANSPARENT 소켓 옵션
- 원본 목적지 획득 — TCP:
getsockname(), UDP:IP_RECVORIGDSTADDR+recvmsg() - upstream 연결 — IP_TRANSPARENT 소켓으로 클라이언트 원본 IP에
bind()후 실제 서버에connect() - eBPF TPROXY — TC ingress hook에서
bpf_sk_redirect_map()또는bpf_sk_assign()으로 소켓 리다이렉트 - netns 격리 — veth 쌍으로 클라이언트 네임스페이스와 TPROXY 호스트를 분리하면 프로덕션 환경 모사 가능
- 실제 도구 — Squid(
tproxy모드), Envoy(original_dst 클러스터), HAProxy(tcp-request connection expect-proxy) 모두 IP_TRANSPARENT 소켓 사용
단계별 이해
- 환경 구성 (Lab-env)
실습 네트워크 토폴로지 파악, 패키지 설치, 커널 옵션 확인. - 기본 TCP TPROXY (Lab 1)
iptables mangle 규칙 + 정책 라우팅 + Python TCP 프록시로 TPROXY 원리를 체험. - UDP/DNS TPROXY (Lab 2)
UDP는 연결 지향이 아니므로 NOTRACK + IP_RECVORIGDSTADDR로 원본 목적지를 추출하는 방법 학습. - nftables 재구현 (Lab 3)
Lab 1·2를 nftables 문법으로 재작성하여 최신 설정 방식 습득. - netns 격리 환경 (Lab 4)
veth 쌍과 ip netns로 실제 라우터/클라이언트 분리 환경 구성. - C epoll 투명 프록시 (Lab 5)
약 250줄의 완성된 C 프로그램으로 고성능 비동기 투명 프록시 구현 및 빌드·실행. - eBPF TC hook (Lab 6)
BPF C 코드 + bpftool로 커널 TC hook에서 소켓 리다이렉트 실습. - 실제 도구 연동 (Lab 7)
Squid/Envoy/HAProxy 완성 설정 파일로 실무 TPROXY 배포 실습. - 트러블슈팅 실습 (Lab 8)
4가지 고의 실패 시나리오로 진단 명령과 원인 분석 훈련.
실습 환경 구성
실습 네트워크 토폴로지
패키지 설치 및 커널 옵션 확인
# Debian/Ubuntu 계열
sudo apt-get update
sudo apt-get install -y iptables nftables iproute2 tcpdump python3 \
gcc make clang linux-headers-$(uname -r) bpftool
# RHEL/CentOS/Fedora 계열
sudo dnf install -y iptables nftables iproute2 tcpdump python3 \
gcc make clang kernel-devel bpftool
# 커널 모듈 확인 (TPROXY 지원 필수)
modinfo xt_TPROXY 2>/dev/null && echo "xt_TPROXY 사용 가능" || echo "모듈 없음 - 커널 재빌드 필요"
# 커널 설정 확인
grep -E "TPROXY|NF_TPROXY" /boot/config-$(uname -r)
# CONFIG_NETFILTER_XT_TARGET_TPROXY=m (또는 y) 이 있어야 합니다
# IPv4 포워딩 활성화
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
# 커널 버전 확인 (4.18 이상 권장)
uname -r
Lab 1: 기본 TCP TPROXY
목표: iptables mangle/PREROUTING + 정책 라우팅 + Python TCP 프록시로 기본 TPROXY 흐름을 완성합니다. 클라이언트가 10.0.0.2:80으로 보낸 패킷을 TPROXY 호스트의 프록시 소켓(:8080)이 투명하게 가로채고, 원본 목적지를 보존한 채 upstream에 재연결합니다.
1단계: iptables 규칙 설정
# mangle 테이블 PREROUTING 체인에 TPROXY 규칙 추가
sudo iptables -t mangle -A PREROUTING \
-p tcp --dport 80 \
-j TPROXY \
--tproxy-mark 0x1/0x1 \
--on-port 8080 \
--on-ip 127.0.0.1
# 설정 확인
sudo iptables -t mangle -L PREROUTING -n -v
2단계: 정책 라우팅 설정
# fwmark 0x1 패킷은 라우팅 테이블 100번 사용
sudo ip rule add fwmark 0x1 lookup 100
# 테이블 100: 모든 패킷을 loopback으로 라우팅
sudo ip route add local 0.0.0.0/0 dev lo table 100
# 확인
ip rule list
ip route show table 100
3단계: Python TCP 투명 프록시
#!/usr/bin/env python3
# tcp_tproxy.py - 기본 TCP 투명 프록시
import socket, threading, struct, errno
LISTEN_PORT = 8080
BUFFER_SIZE = 65536
def make_listen_socket():
"""IP_TRANSPARENT 옵션으로 리슨 소켓 생성"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
# IP_TRANSPARENT = 19 (linux/in.h)
s.setsockopt(socket.IPPROTO_IP, 19, 1)
s.bind(('0.0.0.0', LISTEN_PORT))
s.listen(128)
return s
def get_orig_dst(conn):
"""accept()된 소켓에서 getsockname()으로 원본 목적지 획득"""
# TPROXY 환경에서는 getsockname()이 원본 목적지를 반환
orig_dst = conn.getsockname()
return orig_dst # (ip, port) 튜플
def relay(src, dst):
"""두 소켓 사이 데이터 중계"""
try:
while True:
data = src.recv(BUFFER_SIZE)
if not data:
break
dst.sendall(data)
except (ConnectionResetError, BrokenPipeError, OSError):
pass
finally:
try: src.shutdown(socket.SHUT_RD)
except: pass
try: dst.shutdown(socket.SHUT_WR)
except: pass
def handle(conn, client_addr):
orig_ip, orig_port = get_orig_dst(conn)
print(f"[TPROXY] {client_addr[0]}:{client_addr[1]} → {orig_ip}:{orig_port}")
try:
# upstream 소켓도 IP_TRANSPARENT로 클라이언트 IP에 bind 후 연결
up = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
up.setsockopt(socket.IPPROTO_IP, 19, 1) # IP_TRANSPARENT
up.bind((client_addr[0], 0)) # 클라이언트 원본 IP로 bind
up.connect((orig_ip, orig_port))
t1 = threading.Thread(target=relay, args=(conn, up), daemon=True)
t2 = threading.Thread(target=relay, args=(up, conn), daemon=True)
t1.start(); t2.start()
t1.join(); t2.join()
except Exception as e:
print(f"[오류] {e}")
finally:
try: conn.close()
except: pass
def main():
listen_sock = make_listen_socket()
print(f"TCP 투명 프록시 시작: 포트 {LISTEN_PORT}")
while True:
conn, addr = listen_sock.accept()
t = threading.Thread(target=handle, args=(conn, addr), daemon=True)
t.start()
if __name__ == '__main__':
main()
4단계: 프록시 실행 및 검증
# root 권한으로 프록시 실행 (IP_TRANSPARENT에 CAP_NET_ADMIN 필요)
sudo python3 tcp_tproxy.py &
# 클라이언트에서 HTTP 요청 (TPROXY 호스트를 경유)
curl http://10.0.0.2/
# 예상 프록시 출력:
# [TPROXY] 192.168.100.2:54321 → 10.0.0.2:80
# 패킷 캡처로 동작 확인
sudo tcpdump -i any -n 'port 80 or port 8080' -l &
# 소켓 상태 확인
ss -tnp | grep 8080
정리 (clean-up)
sudo pkill -f tcp_tproxy.py
sudo iptables -t mangle -D PREROUTING -p tcp --dport "num">80 \
-j TPROXY --tproxy-mark 0x1/0x1 --on-port "num">8080 --on-ip "num">127.0."num">0.1
sudo ip rule del fwmark 0x1 lookup "num">100
sudo ip route del local "num">0.0."num">0.0/"num">0 dev lo table "num">100
Lab 2: UDP/DNS TPROXY
목표: UDP는 연결 지향이 아니므로
accept()가 없습니다. IP_RECVORIGDSTADDR 소켓 옵션과 recvmsg()를 사용해 각 UDP 데이터그램의 원본 목적지를 추출합니다. DNS(UDP :53) 요청을 투명하게 가로채는 간이 DNS 프록시를 구현합니다.
1단계: NOTRACK + TPROXY 규칙
# UDP는 conntrack이 방해할 수 있으므로 NOTRACK 처리
sudo iptables -t raw -A PREROUTING -p udp --dport 53 -j NOTRACK
# UDP TPROXY 규칙
sudo iptables -t mangle -A PREROUTING \
-p udp --dport 53 \
-j TPROXY \
--tproxy-mark 0x2/0x2 \
--on-port 5353
# 정책 라우팅 (fwmark 0x2 전용 테이블)
sudo ip rule add fwmark 0x2 lookup 200
sudo ip route add local 0.0.0.0/0 dev lo table 200
# 확인
sudo iptables -t mangle -L PREROUTING -n -v
sudo iptables -t raw -L PREROUTING -n -v
2단계: Python UDP/DNS 투명 프록시
#!/usr/bin/env python3
# udp_tproxy.py - UDP 투명 프록시 (DNS 포워딩 예제)
import socket, struct
LISTEN_PORT = 5353
UPSTREAM_DNS = '8.8.8.8'
UPSTREAM_PORT = 53
# 소켓 옵션 상수
IP_TRANSPARENT = 19
IP_RECVORIGDSTADDR = 20
SOL_IP = 0
def make_udp_socket(port):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.setsockopt(SOL_IP, IP_TRANSPARENT, 1)
s.setsockopt(SOL_IP, IP_RECVORIGDSTADDR, 1) # cmsg로 원본 목적지 수신
s.bind(('0.0.0.0', port))
return s
def get_orig_dst_udp(cmsg_list):
"""recvmsg() cmsg_list에서 원본 목적지(IP:port) 추출"""
for cmsg_level, cmsg_type, cmsg_data in cmsg_list:
if cmsg_level == SOL_IP and cmsg_type == IP_RECVORIGDSTADDR:
# sockaddr_in 구조: sin_family(2) + sin_port(2) + sin_addr(4) + 패딩(8)
family, port_n, addr_n = struct.unpack('!HH4s', cmsg_data[:8])
ip = socket.inet_ntoa(addr_n)
port = socket.ntohs(port_n)
return ip, port
return None, None
def main():
sock = make_udp_socket(LISTEN_PORT)
print(f"UDP 투명 프록시 시작: 포트 {LISTEN_PORT} → {UPSTREAM_DNS}:{UPSTREAM_PORT}")
while True:
# recvmsg()로 패킷 + cmsg(원본 목적지) 동시 수신
data, cmsg_list, flags, client_addr = sock.recvmsg(
4096, socket.CMSG_SPACE(24))
orig_ip, orig_port = get_orig_dst_udp(cmsg_list)
print(f"[UDP] {client_addr[0]}:{client_addr[1]} → {orig_ip}:{orig_port} ({len(data)} bytes)")
# upstream DNS 서버로 포워딩
up_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
up_sock.setsockopt(SOL_IP, IP_TRANSPARENT, 1)
up_sock.bind((client_addr[0], 0)) # 클라이언트 IP로 bind
up_sock.sendto(data, (UPSTREAM_DNS, UPSTREAM_PORT))
# 응답 수신 후 클라이언트에 반환
resp, _ = up_sock.recvfrom(4096)
sock.sendto(resp, client_addr)
up_sock.close()
if __name__ == '__main__':
main()
3단계: 실행 및 검증
sudo python3 udp_tproxy.py &
# dig로 DNS 쿼리 테스트 (클라이언트에서 실행)
dig @10.0.0.2 example.com
# 예상 출력 (프록시 로그):
# [UDP] 192.168.100.2:49152 → 10.0.0.2:53 (46 bytes)
# UDP 패킷 캡처로 확인
sudo tcpdump -i any -n 'udp port 53 or udp port 5353' -l
정리 (clean-up)
sudo pkill -f udp_tproxy.py
sudo iptables -t raw -D PREROUTING -p udp --dport "num">53 -j NOTRACK
sudo iptables -t mangle -D PREROUTING -p udp --dport "num">53 \
-j TPROXY --tproxy-mark 0x2/0x2 --on-port "num">5353
sudo ip rule del fwmark 0x2 lookup "num">200
sudo ip route del local "num">0.0."num">0.0/"num">0 dev lo table "num">200
Lab 3: nftables 버전
목표: Lab 1·2의 iptables 규칙을 nftables 문법으로 재작성합니다. nftables는 단일 규칙 셋으로 TCP/UDP를 동시에 처리하며 더 간결한 문법을 제공합니다.
1단계: 기존 iptables 규칙 정리
# Lab 1·2 규칙이 남아 있으면 먼저 제거
sudo iptables -t mangle -F PREROUTING
sudo iptables -t raw -F PREROUTING
2단계: nftables 규칙 파일 작성
# /etc/nftables-tproxy.conf
cat <<'EOF' | sudo tee /etc/nftables-tproxy.conf
table ip tproxy_table {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
# TCP 80 → TPROXY 포트 8080, fwmark 0x1
tcp dport 80 tproxy to :8080 meta mark set 0x1
# UDP 53 → TPROXY 포트 5353, fwmark 0x2
udp dport 53 tproxy to :5353 meta mark set 0x2
}
}
EOF
# nftables 규칙 적용
sudo nft -f /etc/nftables-tproxy.conf
# 확인
sudo nft list table ip tproxy_table
3단계: 정책 라우팅 (nftables 공통)
# nftables에서도 정책 라우팅은 ip rule/ip route로 동일하게 설정
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100
sudo ip rule add fwmark 0x2 lookup 200
sudo ip route add local 0.0.0.0/0 dev lo table 200
4단계: IPv6 TPROXY (nftables 확장)
cat <<'EOF' | sudo tee /etc/nftables-tproxy6.conf
table ip6 tproxy6_table {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
# IPv6 TCP 80 → TPROXY 포트 8080
tcp dport 80 tproxy to :8080 meta mark set 0x1
# IPv6 UDP 53 → TPROXY 포트 5353
udp dport 53 tproxy to :5353 meta mark set 0x2
}
}
EOF
sudo nft -f /etc/nftables-tproxy6.conf
# IPv6 정책 라우팅
sudo ip -6 rule add fwmark 0x1 lookup 100
sudo ip -6 route add local ::/0 dev lo table 100
검증
sudo nft list ruleset
curl http://10.0.0.2/ # TCP TPROXY 확인
dig @10.0.0.2 example.com # UDP TPROXY 확인
정리 (clean-up)
sudo nft delete table ip tproxy_table
sudo nft delete table ip6 tproxy6_table
sudo ip rule del fwmark 0x1 lookup "num">100
sudo ip route del local "num">0.0."num">0.0/"num">0 dev lo table "num">100
sudo ip rule del fwmark 0x2 lookup "num">200
sudo ip route del local "num">0.0."num">0.0/"num">0 dev lo table "num">200
sudo ip -"num">6 rule del fwmark 0x1 lookup "num">100
sudo ip -"num">6 route del local ::/"num">0 dev lo table "num">100
Lab 4: 네트워크 네임스페이스 격리
목표: veth 쌍과 ip netns를 사용하여 단일 호스트에서 클라이언트/라우터/서버 역할을 분리합니다. 프로덕션과 유사한 멀티-호스트 환경을 시뮬레이션합니다.
1단계: 네임스페이스 및 veth 설정
# 네임스페이스 생성
sudo ip netns add client
sudo ip netns add server
# veth 쌍 생성: 호스트(tproxy-host) ↔ 클라이언트(veth-client)
sudo ip link add veth-host type veth peer name veth-client
sudo ip link set veth-client netns client
# veth 쌍 생성: 호스트(veth-srv-host) ↔ 서버(veth-srv)
sudo ip link add veth-srv-host type veth peer name veth-srv
sudo ip link set veth-srv netns server
# 호스트 인터페이스 IP 설정
sudo ip addr add 192.168.100.1/24 dev veth-host
sudo ip link set veth-host up
sudo ip addr add 10.0.0.1/24 dev veth-srv-host
sudo ip link set veth-srv-host up
# 클라이언트 네임스페이스 설정
sudo ip netns exec client ip addr add 192.168.100.2/24 dev veth-client
sudo ip netns exec client ip link set veth-client up
sudo ip netns exec client ip link set lo up
sudo ip netns exec client ip route add default via 192.168.100.1
# 서버 네임스페이스 설정
sudo ip netns exec server ip addr add 10.0.0.2/24 dev veth-srv
sudo ip netns exec server ip link set veth-srv up
sudo ip netns exec server ip link set lo up
sudo ip netns exec server ip route add default via 10.0.0.1
# 호스트 IP 포워딩 활성화
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
2단계: 간이 HTTP 서버 (server netns)
# server 네임스페이스에서 간이 HTTP 서버 실행
sudo ip netns exec server python3 -m http.server 80 &
# 직접 연결 확인
sudo ip netns exec client curl http://10.0.0.2/
3단계: TPROXY 규칙 및 정책 라우팅
# 호스트에서 TPROXY 규칙 적용 (클라이언트 → 서버 방향 TCP 80)
sudo iptables -t mangle -A PREROUTING \
-i veth-host -p tcp --dport 80 \
-j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080
# 정책 라우팅
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100
# 프록시 실행
sudo python3 tcp_tproxy.py &
# 클라이언트에서 TPROXY를 통한 요청
sudo ip netns exec client curl http://10.0.0.2/
4단계: 검증
# 클라이언트 측 연결 확인
sudo ip netns exec client ss -tnp
# 호스트 프록시 연결 확인
ss -tnp | grep 8080
# 서버 측에서 클라이언트 IP로 요청이 들어오는지 확인
sudo ip netns exec server ss -tnp
# tcpdump로 veth 인터페이스 패킷 확인
sudo tcpdump -i veth-host -n 'port 80'
정리 (clean-up)
sudo pkill -f tcp_tproxy.py
sudo pkill -f "http.server ">80"
sudo iptables -t mangle -D PREROUTING -i veth-host -p tcp --dport "num">80 \
-j TPROXY --tproxy-mark 0x1/0x1 --on-port "num">8080
sudo ip rule del fwmark 0x1 lookup "num">100
sudo ip route del local "num">0.0."num">0.0/"num">0 dev lo table "num">100
sudo ip netns del client
sudo ip netns del server
sudo ip link del veth-host "num">2>/dev/null || true
sudo ip link del veth-srv-host "num">2>/dev/null || true
Lab 5: 완전한 C 투명 프록시 (epoll)
목표: epoll 기반의 비동기 이벤트 루프로 구현한 완전한 C 투명 프록시입니다. edge-triggered 모드와 비동기 I/O로 고성능 처리가 가능합니다.
splice(2)를 사용하여 커널 공간에서 직접 데이터를 복사합니다.
소스 코드: tproxy_proxy.c
/* tproxy_proxy.c — IP_TRANSPARENT epoll 기반 투명 프록시
* 빌드: gcc -O2 -o tproxy_proxy tproxy_proxy.c
* 실행: sudo ./tproxy_proxy 8080
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <linux/netfilter_ipv4.h>
#define MAX_EVENTS 1024
#define BACKLOG 512
#define PIPE_BUF_SZ (256 * 1024) /* splice 파이프 크기 */
typedef struct {
int fd_client; /* 클라이언트 측 fd */
int fd_upstream; /* upstream 서버 측 fd */
int pipe_c2u[2]; /* 클라이언트→upstream 파이프 */
int pipe_u2c[2]; /* upstream→클라이언트 파이프 */
struct sockaddr_in orig_dst; /* 원본 목적지 */
struct sockaddr_in client; /* 클라이언트 주소 */
} conn_t;
/* 소켓을 비동기(O_NONBLOCK) 모드로 설정 */
static int set_nonblock(int fd)
{
int flags = fcntl(fd, F_GETFL, 0);
if (flags < 0) return -1;
return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}
/* IP_TRANSPARENT 리슨 소켓 생성 */
static int make_listen(int port)
{
int fd, opt = 1;
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(port),
.sin_addr.s_addr = INADDR_ANY
};
fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0) { perror("socket"); return -1; }
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
setsockopt(fd, IPPROTO_IP, IP_TRANSPARENT, &opt, sizeof(opt));
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); close(fd); return -1;
}
if (listen(fd, BACKLOG) < 0) {
perror("listen"); close(fd); return -1;
}
return fd;
}
/* getsockname()으로 원본 목적지 획득 */
static void get_orig_dst(int fd, struct sockaddr_in *dst)
{
socklen_t len = sizeof(*dst);
if (getsockname(fd, (struct sockaddr *)dst, &len) < 0)
memset(dst, 0, sizeof(*dst));
}
/* IP_TRANSPARENT upstream 소켓 생성 (클라이언트 IP로 bind) */
static int connect_upstream(struct sockaddr_in *orig_dst,
struct sockaddr_in *client)
{
int fd, opt = 1;
struct sockaddr_in bind_addr = {
.sin_family = AF_INET,
.sin_port = 0,
.sin_addr.s_addr = client->sin_addr.s_addr /* 클라이언트 원본 IP */
};
fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
if (fd < 0) return -1;
setsockopt(fd, IPPROTO_IP, IP_TRANSPARENT, &opt, sizeof(opt));
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &opt, sizeof(opt));
if (bind(fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) < 0) {
perror("upstream bind"); close(fd); return -1;
}
int ret = connect(fd, (struct sockaddr *)orig_dst, sizeof(*orig_dst));
if (ret < 0 && errno != EINPROGRESS) {
close(fd); return -1;
}
return fd;
}
/* splice로 파이프를 거쳐 fd_src → fd_dst 데이터 이동 */
static ssize_t do_splice(int fd_src, int *pipe_fds, int fd_dst)
{
ssize_t n, total = 0;
/* fd_src → pipe (최대 PIPE_BUF_SZ) */
n = splice(fd_src, NULL, pipe_fds[1], NULL, PIPE_BUF_SZ,
SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
if (n <= 0) return n;
/* pipe → fd_dst */
while (n > 0) {
ssize_t s = splice(pipe_fds[0], NULL, fd_dst, NULL, n,
SPLICE_F_MOVE | SPLICE_F_NONBLOCK);
if (s <= 0) break;
n -= s; total += s;
}
return total;
}
static void conn_free(conn_t *c)
{
if (!c) return;
if (c->fd_client >= 0) close(c->fd_client);
if (c->fd_upstream >= 0) close(c->fd_upstream);
if (c->pipe_c2u[0] >= 0) { close(c->pipe_c2u[0]); close(c->pipe_c2u[1]); }
if (c->pipe_u2c[0] >= 0) { close(c->pipe_u2c[0]); close(c->pipe_u2c[1]); }
free(c);
}
int main(int argc, char **argv)
{
int port = (argc > 1) ? atoi(argv[1]) : 8080;
int lfd = make_listen(port);
if (lfd < 0) return 1;
int epfd = epoll_create1(EPOLL_CLOEXEC);
if (epfd < 0) { perror("epoll_create1"); return 1; }
struct epoll_event ev = { .events = EPOLLIN | EPOLLET, .data.fd = lfd };
epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
printf("TPROXY C 프록시 시작: 포트 %d\n", port);
struct epoll_event events[MAX_EVENTS];
for (;;) {
int n = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < n; i++) {
int fd = events[i].data.fd;
if (fd == lfd) {
/* 새 클라이언트 accept */
struct sockaddr_in caddr;
socklen_t clen = sizeof(caddr);
int cfd = accept4(lfd, (struct sockaddr *)&caddr,
&clen, SOCK_NONBLOCK | SOCK_CLOEXEC);
if (cfd < 0) continue;
conn_t *c = calloc(1, sizeof(*c));
if (!c) { close(cfd); continue; }
c->fd_client = cfd;
c->client = caddr;
c->pipe_c2u[0] = c->pipe_c2u[1] = -1;
c->pipe_u2c[0] = c->pipe_u2c[1] = -1;
get_orig_dst(cfd, &c->orig_dst);
printf("[+] %s:%d → %s:%d\n",
inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port),
inet_ntoa(c->orig_dst.sin_addr), ntohs(c->orig_dst.sin_port));
if (pipe2(c->pipe_c2u, O_NONBLOCK) || pipe2(c->pipe_u2c, O_NONBLOCK)) {
conn_free(c); continue;
}
fcntl(c->pipe_c2u[0], F_SETPIPE_SZ, PIPE_BUF_SZ);
fcntl(c->pipe_u2c[0], F_SETPIPE_SZ, PIPE_BUF_SZ);
c->fd_upstream = connect_upstream(&c->orig_dst, &c->client);
if (c->fd_upstream < 0) { conn_free(c); continue; }
/* epoll에 클라이언트·upstream 등록 */
struct epoll_event cev = {
.events = EPOLLIN | EPOLLOUT | EPOLLET | EPOLLRDHUP,
.data.ptr = c
};
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &cev);
epoll_ctl(epfd, EPOLL_CTL_ADD, c->fd_upstream, &cev);
} else {
/* 기존 연결 데이터 중계 */
conn_t *c = events[i].data.ptr;
if (!c) continue;
if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
epoll_ctl(epfd, EPOLL_CTL_DEL, c->fd_client, NULL);
epoll_ctl(epfd, EPOLL_CTL_DEL, c->fd_upstream, NULL);
conn_free(c);
continue;
}
if (events[i].events & EPOLLIN) {
/* 클라이언트 → upstream */
do_splice(c->fd_client, c->pipe_c2u, c->fd_upstream);
/* upstream → 클라이언트 */
do_splice(c->fd_upstream, c->pipe_u2c, c->fd_client);
}
}
}
}
return 0;
}
Makefile
# Makefile
CC = gcc
CFLAGS = -O2 -Wall -Wextra
TARGET = tproxy_proxy
SRC = tproxy_proxy.c
$(TARGET): $(SRC)
$(CC) $(CFLAGS) -o $@ $^
clean:
rm -f $(TARGET)
빌드 및 실행
# 빌드
make
# root 권한으로 실행 (IP_TRANSPARENT = CAP_NET_ADMIN 필요)
sudo ./tproxy_proxy 8080
# 예상 출력:
# TPROXY C 프록시 시작: 포트 8080
# [+] 192.168.100.2:54321 → 10.0.0.2:80
# 클라이언트에서 테스트
curl http://10.0.0.2/
# 성능 측정 (wrk 사용 시)
wrk -t4 -c100 -d10s http://10.0.0.2/
Lab 6: eBPF TC hook TPROXY
목표: Linux TC(Traffic Control) ingress hook에 BPF 프로그램을 붙여 패킷을 소켓으로 리다이렉트합니다. iptables 없이 BPF 수준에서 TPROXY를 구현합니다. 커널 5.7 이상에서
bpf_sk_assign()을 사용합니다.
요구 사항: Linux 5.7 이상,
CONFIG_NET_SCH_INGRESS=y (또는 m), CONFIG_NET_CLS_BPF=y, clang, bpftool 필요.
BPF C 소스: tproxy_kern.c
/* tproxy_kern.c — TC ingress TPROXY BPF 프로그램
* 빌드: clang -O2 -target bpf -c tproxy_kern.c -o tproxy_kern.o
*/
#include <linux/bpf.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_endian.h>
#define TPROXY_PORT 8080
#define TARGET_PORT 80 /* 가로챌 포트 */
#define FWMARK 0x1
/* 로컬 소켓 맵: IP:PORT → 소켓 */
struct {
__uint(type, BPF_MAP_TYPE_SOCKHASH);
__uint(max_entries, 1024);
__type(key, struct sock_common);
__type(value, __u64);
} tproxy_sock_map SEC(".maps");
SEC("tc")
int tproxy_ingress(struct __sk_buff *skb)
{
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
/* 이더넷 헤더 검증 */
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) return TC_ACT_OK;
if (bpf_ntohs(eth->h_proto) != ETH_P_IP) return TC_ACT_OK;
/* IP 헤더 검증 */
struct iphdr *iph = (void *)(eth + 1);
if ((void *)(iph + 1) > data_end) return TC_ACT_OK;
if (iph->protocol != IPPROTO_TCP) return TC_ACT_OK;
/* TCP 헤더 검증 */
struct tcphdr *tcph = (void *)iph + (iph->ihl * 4);
if ((void *)(tcph + 1) > data_end) return TC_ACT_OK;
/* 대상 포트 필터 */
if (bpf_ntohs(tcph->dest) != TARGET_PORT) return TC_ACT_OK;
/* fwmark 설정 (정책 라우팅과 연동) */
skb->mark = FWMARK;
/* 소켓 조회: 로컬 TPROXY 포트의 리슨 소켓을 찾아 할당 */
struct bpf_sock_tuple tuple = {};
tuple.ipv4.saddr = iph->saddr;
tuple.ipv4.daddr = iph->daddr;
tuple.ipv4.sport = tcph->source;
tuple.ipv4.dport = tcph->dest;
struct bpf_sock *sk = bpf_skc_lookup_tcp(skb, &tuple,
sizeof(tuple.ipv4), BPF_F_CURRENT_NETNS, 0);
if (!sk) {
/* 리슨 소켓 조회 (TPROXY_PORT) */
struct bpf_sock_tuple listen_tuple = {};
listen_tuple.ipv4.saddr = 0;
listen_tuple.ipv4.daddr = iph->daddr;
listen_tuple.ipv4.sport = 0;
listen_tuple.ipv4.dport = bpf_htons(TPROXY_PORT);
sk = bpf_skc_lookup_tcp(skb, &listen_tuple,
sizeof(listen_tuple.ipv4), BPF_F_CURRENT_NETNS, 0);
}
if (sk) {
int ret = bpf_sk_assign(skb, sk, 0);
bpf_sk_release(sk);
if (ret == 0) return TC_ACT_OK;
}
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";
빌드 및 로드
# BPF 오브젝트 빌드
clang -O2 -target bpf -c tproxy_kern.c -o tproxy_kern.o
# TC qdisc 생성 (ingress)
sudo tc qdisc add dev eth0 clsact
# BPF 프로그램을 TC ingress에 붙이기
sudo tc filter add dev eth0 ingress \
bpf direct-action obj tproxy_kern.o sec tc
# 확인
sudo tc filter show dev eth0 ingress
# bpftool로 프로그램 목록 확인
sudo bpftool prog list | grep -A2 tproxy
# 정책 라우팅은 동일하게 필요
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100
# C 프록시 실행
sudo ./tproxy_proxy 8080
검증
# BPF 맵 상태 확인
sudo bpftool map list
# TC 통계 확인
sudo tc -s filter show dev eth0 ingress
# 기능 동작 확인
curl http://10.0.0.2/
정리 (clean-up)
sudo tc filter del dev eth0 ingress
sudo tc qdisc del dev eth0 clsact
sudo ip rule del fwmark 0x1 lookup "num">100
sudo ip route del local "num">0.0."num">0.0/"num">0 dev lo table "num">100
sudo pkill -f tproxy_proxy
Lab 7: 실제 도구 연동 (Squid / Envoy / HAProxy)
목표: 프로덕션에서 실제로 사용하는 Squid, Envoy, HAProxy의 TPROXY 완성 설정을 제공합니다. 각 도구의 핵심 설정 파라미터와 iptables 연동 방법을 다룹니다.
7-1. Squid TPROXY 설정
# Squid 설치
sudo apt-get install -y squid
# /etc/squid/squid.conf 핵심 설정
cat <<'EOF' | sudo tee /etc/squid/squid.conf
# TPROXY 포트 설정 (tproxy 옵션 필수)
http_port 3129 tproxy
# 투명 프록시 허용 ACL
acl localnet src 192.168.100.0/24
http_access allow localnet
# upstream 연결 시 클라이언트 IP 유지 (IP_TRANSPARENT 사용)
follow_x_forwarded_for allow localnet
forwarded_for on
# 캐시 설정
cache_mem 256 MB
cache_dir ufs /var/spool/squid 1000 16 256
# 로그
access_log /var/log/squid/access.log combined
cache_log /var/log/squid/cache.log
EOF
# iptables 규칙 (Squid용 포트 3129)
sudo iptables -t mangle -A PREROUTING \
-p tcp --dport 80 \
-j TPROXY --tproxy-mark 0x1/0x1 --on-port 3129
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100
# Squid 시작
sudo systemctl start squid
sudo systemctl enable squid
# 동작 확인
sudo squid -k check
sudo tail -f /var/log/squid/access.log
7-2. Envoy YAML 설정
# envoy-tproxy.yaml — Envoy TPROXY 리스너 설정
cat <<'EOF' > envoy-tproxy.yaml
static_resources:
listeners:
- name: tproxy_listener
address:
socket_address:
address: 0.0.0.0
port_value: 15001
listener_filters:
- name: envoy.filters.listener.original_dst
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.listener.original_dst.v3.OriginalDst
filter_chains:
- filters:
- name: envoy.filters.network.tcp_proxy
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
stat_prefix: tproxy
cluster: original_dst_cluster
clusters:
- name: original_dst_cluster
type: ORIGINAL_DST # 원본 목적지로 upstream 연결
connect_timeout: 5s
lb_policy: CLUSTER_PROVIDED
upstream_bind_config:
source_address:
address: 0.0.0.0
port_value: 0
admin:
address:
socket_address:
address: 127.0.0.1
port_value: 9901
EOF
# iptables 규칙 (Envoy용 포트 15001)
sudo iptables -t mangle -A PREROUTING \
-p tcp --dport 80 \
-j TPROXY --tproxy-mark 0x1/0x1 --on-port 15001
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100
# Envoy 실행 (Docker 사용 시)
docker run -d \
--name envoy-tproxy \
--network host \
--cap-add NET_ADMIN \
-v $(pwd)/envoy-tproxy.yaml:/etc/envoy/envoy.yaml \
envoyproxy/envoy:v1.28-latest
# Admin API로 통계 확인
curl http://localhost:9901/stats | grep tproxy
7-3. HAProxy 설정
# /etc/haproxy/haproxy.cfg
cat <<'EOF' | sudo tee /etc/haproxy/haproxy.cfg
global
log /dev/log local0
maxconn 50000
user haproxy
group haproxy
defaults
log global
mode tcp
timeout connect 5s
timeout client 30s
timeout server 30s
# TPROXY 프론트엔드
frontend tproxy_front
bind *:8080 transparent # transparent 키워드 = IP_TRANSPARENT
mode tcp
default_backend tproxy_back
backend tproxy_back
mode tcp
option originalto # 원본 목적지를 X-Original-To 헤더로 전달
server s1 0.0.0.0:0 check source 0.0.0.0 usesrc clientip
# usesrc clientip: 클라이언트 원본 IP로 upstream 연결
EOF
# iptables 규칙 (HAProxy용 포트 8080)
sudo iptables -t mangle -A PREROUTING \
-p tcp --dport 80 \
-j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100
# HAProxy 시작
sudo systemctl restart haproxy
# 통계 페이지 확인
curl http://localhost:8404/stats 2>/dev/null || echo "stats 미설정"
Lab 8: 트러블슈팅 실습
목표: TPROXY 설정 시 자주 발생하는 4가지 실패 시나리오를 의도적으로 재현하고, 진단 명령으로 원인을 파악하고 해결합니다.
시나리오 1: 정책 라우팅 누락 (패킷이 프록시에 도달하지 않음)
# [의도적 실패] 정책 라우팅 없이 TPROXY 규칙만 설정
sudo iptables -t mangle -A PREROUTING \
-p tcp --dport 80 -j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080
# ip rule / ip route 추가 안 함!
# 증상: curl이 응답 없이 멈춤
# 진단 1: 정책 라우팅 확인
ip rule list | grep fwmark
# fwmark 0x1 항목이 없으면 → 문제 확인
# 진단 2: fwmark 패킷의 라우팅 시뮬레이션
ip route get 10.0.0.2 mark 0x1
# RTNETLINK answers: Network unreachable → 정책 라우팅 누락
# 진단 3: conntrack 상태 확인
sudo conntrack -L -p tcp --dport 80 2>/dev/null | head -5
# 해결: 정책 라우팅 추가
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local 0.0.0.0/0 dev lo table 100
# 재확인
ip route get 10.0.0.2 mark 0x1
# local 10.0.0.2 dev lo ... 가 나와야 함
시나리오 2: IP_TRANSPARENT 옵션 누락 (EACCES 또는 bind 실패)
# [의도적 실패] IP_TRANSPARENT 없이 프록시 소켓에 다른 IP로 bind 시도
python3 -c "
import socket
s = socket.socket()
s.bind(('10.0.0.2', 0)) # 로컬에 없는 IP
print('bind 성공')
" 2>&1
# OSError: [Errno 99] Cannot assign requested address
# 진단: CAP_NET_ADMIN 확인
capsh --print | grep net_admin
# 또는 프로세스의 capability 확인
cat /proc/$$/status | grep Cap
# 진단: IP_TRANSPARENT 설정 여부 확인 (strace 사용)
sudo strace -e trace=setsockopt python3 tcp_tproxy.py 2>&1 | grep IP_TRANSPARENT
# setsockopt(3, SOL_IP, IP_TRANSPARENT, [1], 4) = 0 이 있어야 함
# 해결: root 또는 CAP_NET_ADMIN으로 실행
sudo python3 tcp_tproxy.py
# 검증: IP_TRANSPARENT 소켓으로 정상 bind 확인
python3 -c "
import socket
s = socket.socket()
s.setsockopt(socket.IPPROTO_IP, 19, 1) # IP_TRANSPARENT
s.bind(('10.0.0.2', 0))
print('IP_TRANSPARENT bind 성공:', s.getsockname())
s.close()
"
시나리오 3: 루프 발생 (프록시 트래픽이 자신으로 돌아옴)
# [의도적 실패] 프록시 자신의 트래픽까지 TPROXY 규칙에 걸림
# 증상: 프록시가 upstream 연결 시도 → 다시 TPROXY에 걸림 → 루프
# 진단 1: 루프 패킷 확인 (tcpdump)
sudo tcpdump -i lo -n 'port 8080' -c 20
# 같은 패킷이 반복적으로 나타나면 루프
# 진단 2: fwmark 패킷 필터 확인
sudo iptables -t mangle -L PREROUTING -n -v
# 해결: 프록시 프로세스 uid/gid 또는 mark로 예외 처리
# 방법 1: UID 기반 예외 (프록시를 별도 사용자로 실행)
sudo useradd -r tproxy-user 2>/dev/null || true
sudo iptables -t mangle -I PREROUTING 1 \
-m owner --uid-owner tproxy-user \
-j RETURN # 프록시 자신의 트래픽은 TPROXY 건너뜀
# 방법 2: 출력 인터페이스 기반 예외
sudo iptables -t mangle -I PREROUTING 1 \
-i lo -j RETURN
# 규칙 순서 확인
sudo iptables -t mangle -L PREROUTING -n -v --line-numbers
시나리오 4: UDP conntrack 충돌 (DNS 응답 드롭)
# [의도적 실패] UDP TPROXY에서 NOTRACK 없이 설정
# 증상: DNS 요청 후 응답이 돌아오지 않음 (dig timeout)
# 진단 1: conntrack 상태 확인
sudo conntrack -L -p udp --dport 53 2>/dev/null
# INVALID 상태이거나 응답 패킷이 ESTABLISHED 매칭 실패
# 진단 2: 패킷 드롭 카운터 확인
sudo iptables -t filter -L FORWARD -n -v
sudo iptables -t filter -L INPUT -n -v
# 진단 3: nf_conntrack 로그 (INVALID 드롭 확인)
sudo iptables -t filter -I INPUT 1 \
-m conntrack --ctstate INVALID -j LOG --log-prefix "[INVALID] "
sudo dmesg | tail -20 | grep INVALID
# 해결: raw 테이블에서 NOTRACK 추가
sudo iptables -t raw -I PREROUTING 1 -p udp --dport 53 -j NOTRACK
sudo iptables -t raw -I OUTPUT 1 -p udp --sport 53 -j NOTRACK
# 검증: NOTRACK 후 DNS 동작 확인
dig @10.0.0.2 example.com
# 진단용 임시 규칙 정리
sudo iptables -t filter -D INPUT 1 # INVALID 로그 규칙 제거
공통 진단 명령 모음
# 1. iptables mangle 체인 전체 확인
sudo iptables -t mangle -L -n -v --line-numbers
# 2. 정책 라우팅 전체 확인
ip rule list
ip route show table all | grep -v "^default" | head -30
# 3. 소켓 상태 확인 (IP_TRANSPARENT 포함)
ss -tnp --info | grep -A1 tproxy_proxy
# 4. conntrack 테이블 확인
sudo conntrack -L 2>/dev/null | head -20
# 5. 특정 패킷 경로 추적 (nftrace)
sudo nft add table ip debug_trace
sudo nft add chain ip debug_trace prerouting '{ type filter hook prerouting priority -300; }'
sudo nft add rule ip debug_trace prerouting tcp dport 80 meta nftrace set 1
sudo nft monitor trace 2>/dev/null | head -30
# 확인 후 삭제
sudo nft delete table ip debug_trace
# 6. 커널 TPROXY 오류 메시지 확인
sudo dmesg | grep -i tproxy | tail -20
# 7. BPF 프로그램 로드 확인 (eBPF Lab)
sudo bpftool prog list 2>/dev/null | grep tc
참고 자료
- TPROXY (투명 프록시) — 커널 내부 구조, nf_tproxy_get_sock, IP_TRANSPARENT 소켓 옵션, 정책 라우팅 심화
- Netfilter 프레임워크 — mangle 테이블, iptables/nftables 체인, 훅 시스템
- eBPF 보안 정책 — TC hook, bpf_sk_assign, bpf_sk_lookup 상세
- 네트워크 네임스페이스 — veth 쌍, ip netns, 컨테이너 네트워킹
- 라우팅 서브시스템 — ip rule, ip route, 정책 라우팅 테이블
다음 학습 추천:
- 대용량 트래픽 처리가 필요하다면 TPROXY 성능 튜닝 항목 참조
- eBPF로 더 세밀한 소켓 제어가 필요하다면 eBPF 보안 정책 학습 권장
- Kubernetes/컨테이너 환경에서는 네트워크 네임스페이스와 결합하여 활용
관련 문서
필수 관련 문서:
- 네트워크 스택 (Network Stack) — sk_buff, 소켓 계층, TCP 내부 구현, NAPI 기초, 라우팅, TC/qdisc
- IP 프로토콜 (IPv4/IPv6) — Linux 커널 IP 프로토콜 심화: IPv4 라우팅/FIB/ARP/단편화, IPv6 ND
- Netfilter Flowtable 심화 — Netfilter Flowtable SW/HW 오프로드 메커니즘, conntrack 대비
- IPVS L4 로드밸런싱 — Linux IPVS 아키텍처, 스케줄링 알고리즘(rr/wrr/lc/wlc/lblcr/sed