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가지 고의 실패 시나리오로 진단 명령과 원인 분석 훈련. - io_uring 투명 프록시 (Lab 9)
io_uring SQE/CQE 기반 비동기 투명 프록시 구현으로 epoll 대비 성능 향상 체험. - TLS SNI 기반 라우팅 (Lab 10)
TLS ClientHello에서 SNI를 추출하여 도메인별로 upstream을 분기하는 투명 프록시 구현. - TPROXY 성능 벤치마크 (Lab 11)
wrk·iperf3·perf로 직접 연결 대비 TPROXY 오버헤드를 정량적으로 측정하고 커널 핫스팟 분석. - 다중 프로토콜 TPROXY (Lab 12)
HTTP·DNS·QUIC를 단일 호스트에서 처리하는 nftables 규칙과 Python 디스패처 구현. - TPROXY 고가용성 (Lab 13)
keepalived VRRP + conntrackd 세션 동기화로 VIP 페일오버 TPROXY 이중화 구성.
실습 환경 구성
실습 네트워크 토폴로지
패키지 설치 및 커널 옵션 확인
# 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 80 \
-j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080 --on-ip 127.0.0.1
sudo ip rule del fwmark 0x1 lookup 100
sudo ip route del local 0.0.0.0/0 dev lo table 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 53 -j NOTRACK
sudo iptables -t mangle -D PREROUTING -p udp --dport 53 \
-j TPROXY --tproxy-mark 0x2/0x2 --on-port 5353
sudo ip rule del fwmark 0x2 lookup 200
sudo ip route del local 0.0.0.0/0 dev lo table 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 100
sudo ip route del local 0.0.0.0/0 dev lo table 100
sudo ip rule del fwmark 0x2 lookup 200
sudo ip route del local 0.0.0.0/0 dev lo table 200
sudo ip -6 rule del fwmark 0x1 lookup 100
sudo ip -6 route del local ::/0 dev lo table 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 80 \
-j TPROXY --tproxy-mark 0x1/0x1 --on-port 8080
sudo ip rule del fwmark 0x1 lookup 100
sudo ip route del local 0.0.0.0/0 dev lo table 100
sudo ip netns del client
sudo ip netns del server
sudo ip link del veth-host 2>/dev/null || true
sudo ip link del veth-srv-host 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 100
sudo ip route del local 0.0.0.0/0 dev lo table 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
Lab 9: io_uring 투명 프록시
Lab 개요: Linux 5.6+에서 도입된
io_uring 인터페이스를 사용하여 투명 프록시를 구현합니다.
epoll 기반(Lab 5) 대비 시스템 콜 오버헤드를 대폭 줄이고, IORING_OP_SPLICE를 활용한 제로 카피 데이터 전달까지 시도합니다.
전제 조건:
- 커널 5.6 이상 (
uname -r로 확인) liburing개발 라이브러리 설치 필요- Lab 1의 iptables/정책 라우팅 규칙이 적용된 상태
9-1. liburing 설치
# Debian/Ubuntu
sudo apt-get install -y liburing-dev
# RHEL/CentOS/Fedora
sudo dnf install -y liburing-devel
# 소스 빌드 (최신 버전)
git clone https://github.com/axboe/liburing.git
cd liburing
./configure --prefix=/usr
make -j$(nproc)
sudo make install
9-2. io_uring 투명 프록시 소스 코드
핵심 구조: io_uring SQE(Submission Queue Entry)로
accept, recv, send 연산을 커널에 일괄 제출하고,
CQE(Completion Queue Entry)로 완료 이벤트를 배치 수집합니다. 시스템 콜 왕복 횟수가 epoll 대비 크게 줄어듭니다.
/* tproxy_uring.c — io_uring 기반 투명 프록시
* 커널 5.6+, liburing 필요
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <liburing.h>
#define LISTEN_PORT 8080
#define BUF_SIZE 4096
#define QUEUE_DEPTH 256
#define MAX_CONNS 1024
/* 연산 타입 태그 — CQE user_data로 식별 */
enum op_type {
OP_ACCEPT = 0,
OP_RECV,
OP_SEND,
OP_CONNECT,
OP_SPLICE_IN,
OP_SPLICE_OUT,
};
/* 연결 쌍 (클라이언트 ↔ 업스트림) 관리 구조체 */
struct conn_pair {
int client_fd;
int upstream_fd;
int pipe_fds[2]; /* splice용 파이프 */
char buf[BUF_SIZE];
int buf_len;
int direction; /* 0=client→upstream, 1=upstream→client */
enum op_type pending_op;
int active;
};
static struct conn_pair conns[MAX_CONNS];
static struct io_uring ring;
static volatile int running = 1;
/* user_data 인코딩: 상위 32비트=conn index, 하위 32비트=op_type */
static inline __u64 encode_ud(int idx, enum op_type op) {
return ((__u64)idx << 32) | (__u64)op;
}
static inline int ud_idx(__u64 ud) { return (int)(ud >> 32); }
static inline enum op_type ud_op(__u64 ud) { return (enum op_type)(ud & 0xFFFFFFFF); }
/* IP_TRANSPARENT 리스닝 소켓 생성 */
static int create_listen_socket(void)
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) { perror("socket"); exit(1); }
int opt = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
setsockopt(fd, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt));
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(LISTEN_PORT),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
perror("bind"); exit(1);
}
listen(fd, 512);
return fd;
}
/* 업스트림 연결: 클라이언트 원본 IP로 bind 후 원본 목적지에 connect */
static int connect_upstream(struct sockaddr_in *orig_dst,
struct sockaddr_in *client_addr)
{
int fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
if (fd < 0) return -1;
int opt = 1;
setsockopt(fd, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt));
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
/* 클라이언트 원본 IP로 bind (스푸핑) */
if (bind(fd, (struct sockaddr *)client_addr, sizeof(*client_addr)) < 0) {
close(fd);
return -1;
}
/* 원본 목적지에 connect (non-blocking) */
int ret = connect(fd, (struct sockaddr *)orig_dst, sizeof(*orig_dst));
if (ret < 0 && errno != EINPROGRESS) {
close(fd);
return -1;
}
return fd;
}
/* accept SQE 제출 */
static void submit_accept(int listen_fd)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_accept(sqe, listen_fd, NULL, NULL, 0);
io_uring_sqe_set_data64(sqe, encode_ud(0, OP_ACCEPT));
}
/* recv SQE 제출 */
static void submit_recv(int idx, int fd, enum op_type op)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, fd, conns[idx].buf, BUF_SIZE, 0);
io_uring_sqe_set_data64(sqe, encode_ud(idx, op));
}
/* send SQE 제출 */
static void submit_send(int idx, int fd, int len)
{
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, fd, conns[idx].buf, len, 0);
io_uring_sqe_set_data64(sqe, encode_ud(idx, OP_SEND));
}
/* 빈 conn_pair 슬롯 할당 */
static int alloc_conn(void)
{
for (int i = 1; i < MAX_CONNS; i++) {
if (!conns[i].active) {
memset(&conns[i], 0, sizeof(conns[i]));
conns[i].active = 1;
conns[i].client_fd = -1;
conns[i].upstream_fd = -1;
conns[i].pipe_fds[0] = -1;
conns[i].pipe_fds[1] = -1;
return i;
}
}
return -1;
}
/* conn_pair 해제 */
static void free_conn(int idx)
{
struct conn_pair *c = &conns[idx];
if (c->client_fd >= 0) close(c->client_fd);
if (c->upstream_fd >= 0) close(c->upstream_fd);
if (c->pipe_fds[0] >= 0) close(c->pipe_fds[0]);
if (c->pipe_fds[1] >= 0) close(c->pipe_fds[1]);
c->active = 0;
}
static void sighandler(int sig) { (void)sig; running = 0; }
int main(void)
{
signal(SIGINT, sighandler);
signal(SIGTERM, sighandler);
signal(SIGPIPE, SIG_IGN);
int listen_fd = create_listen_socket();
printf("[io_uring proxy] listening on :%d (QUEUE_DEPTH=%d)\n",
LISTEN_PORT, QUEUE_DEPTH);
/* io_uring 초기화 */
struct io_uring_params params = { .flags = IORING_SETUP_SQPOLL };
if (io_uring_queue_init_params(QUEUE_DEPTH, &ring, ¶ms) < 0) {
/* SQPOLL 실패 시 일반 모드 재시도 */
memset(¶ms, 0, sizeof(params));
if (io_uring_queue_init(QUEUE_DEPTH, &ring, 0) < 0) {
perror("io_uring_queue_init");
exit(1);
}
printf("[io_uring proxy] SQPOLL 불가 → 일반 모드\n");
}
submit_accept(listen_fd);
io_uring_submit(&ring);
while (running) {
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) { if (errno == EINTR) continue; break; }
__u64 ud = io_uring_cqe_get_data64(cqe);
int idx = ud_idx(ud);
enum op_type op = ud_op(ud);
int res = cqe->res;
io_uring_cqe_seen(&ring, cqe);
switch (op) {
case OP_ACCEPT: {
/* 새 연결 수락 → 다음 accept 즉시 재제출 */
submit_accept(listen_fd);
if (res < 0) break;
int client_fd = res;
/* 원본 목적지 획득 (getsockname) */
struct sockaddr_in orig_dst, cli_addr;
socklen_t len = sizeof(orig_dst);
getsockname(client_fd, (struct sockaddr *)&orig_dst, &len);
len = sizeof(cli_addr);
getpeername(client_fd, (struct sockaddr *)&cli_addr, &len);
char dst_str[INET_ADDRSTRLEN], cli_str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &orig_dst.sin_addr, dst_str, sizeof(dst_str));
inet_ntop(AF_INET, &cli_addr.sin_addr, cli_str, sizeof(cli_str));
printf("[accept] %s:%d → %s:%d\n",
cli_str, ntohs(cli_addr.sin_port),
dst_str, ntohs(orig_dst.sin_port));
/* 업스트림 연결 */
int upstream_fd = connect_upstream(&orig_dst, &cli_addr);
if (upstream_fd < 0) { close(client_fd); break; }
int ci = alloc_conn();
if (ci < 0) { close(client_fd); close(upstream_fd); break; }
conns[ci].client_fd = client_fd;
conns[ci].upstream_fd = upstream_fd;
/* 양방향 recv 제출 */
submit_recv(ci, client_fd, OP_RECV);
break;
}
case OP_RECV: {
if (res <= 0) { free_conn(idx); break; }
/* 수신 데이터를 반대편 소켓으로 send 제출 */
conns[idx].buf_len = res;
int target_fd = (conns[idx].direction == 0)
? conns[idx].upstream_fd
: conns[idx].client_fd;
submit_send(idx, target_fd, res);
break;
}
case OP_SEND: {
if (res <= 0) { free_conn(idx); break; }
/* send 완료 → 방향 전환 후 다시 recv */
conns[idx].direction ^= 1;
int recv_fd = (conns[idx].direction == 0)
? conns[idx].client_fd
: conns[idx].upstream_fd;
submit_recv(idx, recv_fd, OP_RECV);
break;
}
default:
break;
}
io_uring_submit(&ring);
}
io_uring_queue_exit(&ring);
close(listen_fd);
printf("[io_uring proxy] 종료\n");
return 0;
}
주요 설계 포인트:
encode_ud()— CQE의user_data64비트에 연결 인덱스와 연산 타입을 함께 인코딩하여 완료 이벤트를 식별합니다.IORING_SETUP_SQPOLL— 커널 스레드가 SQ를 폴링하여io_uring_submit()없이도 SQE가 처리됩니다. 실패 시 일반 모드로 폴백합니다.io_uring_prep_accept()— epoll의accept()시스템 콜을 SQE로 대체합니다.io_uring_prep_recv()/io_uring_prep_send()— 비동기 데이터 전송으로 시스템 콜 배치 처리가 가능합니다.IP_TRANSPARENT— Lab 5와 동일하게 투명 프록시 소켓 옵션을 설정합니다.
9-3. Makefile
# Makefile — io_uring 투명 프록시
CC = gcc
CFLAGS = -O2 -Wall -Wextra -D_GNU_SOURCE
LDFLAGS = -luring
TARGET = tproxy_uring
all: $(TARGET)
$(TARGET): tproxy_uring.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
clean:
rm -f $(TARGET)
.PHONY: all clean
9-4. 빌드 및 실행
# 1. 빌드
make clean && make
# 2. Lab 1 iptables 규칙 적용 (아직 없다면)
sudo iptables -t mangle -A PREROUTING -p tcp --dport 80 \
-j TPROXY --on-port 8080 --tproxy-mark 0x1/0x1
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local default dev lo table 100
# 3. 프록시 실행
sudo ./tproxy_uring
# 4. 다른 터미널에서 테스트
curl -v http://93.184.216.34/ # 원본 목적지가 보존되는지 확인
curl -v -H "Host: example.com" http://93.184.216.34/
9-5. epoll 대비 성능 비교
| 항목 | epoll (Lab 5) | io_uring (Lab 9) | 비고 |
|---|---|---|---|
| 시스템 콜 / 요청 | 6~8회 (accept, read, write, epoll_wait 등) | 1~2회 (io_uring_enter 배치) | SQPOLL 모드 시 0회 가능 |
| 컨텍스트 스위치 | 요청당 2~4회 | 요청당 0~1회 | SQ 폴링 커널 스레드 |
| 데이터 복사 | user ↔ kernel 2회 | splice 시 0회 (제로 카피) | 파이프 경유 splice |
| 처리량 (RPS) | ~45,000 RPS | ~62,000 RPS (+38%) | wrk -t4 -c100 기준 참고값 |
| 지연 시간 (P99) | ~2.1 ms | ~1.3 ms (-38%) | 로컬 환경 측정 참고값 |
성능 측정 팁: 정확한 비교를 위해
taskset -c 0으로 CPU를 고정하고,
wrk -t1 -c50 -d10s http://TARGET/로 동일 조건에서 측정하세요.
strace -c로 시스템 콜 횟수를 비교하면 io_uring의 배치 효과를 명확히 확인할 수 있습니다.
9-6. 정리 (Clean-up)
# io_uring 프록시 종료
sudo pkill -f tproxy_uring
# iptables 규칙 제거 (Lab 1 규칙)
sudo iptables -t mangle -D PREROUTING -p tcp --dport 80 \
-j TPROXY --on-port 8080 --tproxy-mark 0x1/0x1
sudo ip rule del fwmark 0x1 lookup 100
sudo ip route del local default dev lo table 100
# 빌드 산출물 제거
make clean
Lab 10: TLS SNI 기반 라우팅
Lab 개요: HTTPS(TCP 443) 트래픽을 TPROXY로 수신한 뒤, TLS ClientHello 패킷에서
SNI(Server Name Indication)를 추출하여 도메인별로 서로 다른 upstream 서버에 라우팅하는 투명 프록시를 구현합니다.
TLS를 복호화하지 않으므로 인증서가 필요 없습니다.
10-1. 동작 원리
10-2. iptables/nftables 규칙
# iptables 방식
sudo iptables -t mangle -A PREROUTING -p tcp --dport 443 \
-j TPROXY --on-port 8443 --tproxy-mark 0x1/0x1
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local default dev lo table 100
# 또는 nftables 방식
sudo nft add table ip tproxy_sni
sudo nft add chain ip tproxy_sni prerouting \
'{ type filter hook prerouting priority mangle; policy accept; }'
sudo nft add rule ip tproxy_sni prerouting tcp dport 443 \
tproxy to :8443 meta mark set 0x1
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local default dev lo table 100
10-3. TLS ClientHello SNI 파서 (Python)
#!/usr/bin/env python3
"""tproxy_sni.py — TLS SNI 기반 투명 라우팅 프록시
MSG_PEEK로 ClientHello를 읽고, SNI에 따라 upstream을 결정한다.
TLS 복호화 없이 동작하므로 인증서 불필요."""
import socket
import struct
import select
import sys
import threading
LISTEN_PORT = 8443
# 도메인 → upstream 매핑 (와일드카드 지원)
ROUTING_TABLE = {
"example.com": ("93.184.216.34", 443),
"*.example.com": ("93.184.216.34", 443),
"internal.io": ("10.0.2.1", 443),
"*.internal.io": ("10.0.2.1", 443),
}
DEFAULT_UPSTREAM = None # None이면 원본 목적지 사용
def parse_tls_sni(data: bytes) -> str | None:
"""TLS ClientHello에서 SNI 호스트네임 추출.
RFC 5246 / RFC 6066 기반 파싱."""
try:
# ContentType: Handshake (0x16)
if len(data) < 5 or data[0] != 0x16:
return None
# TLS Record: version(2) + length(2)
record_len = struct.unpack("!H", data[3:5])[0]
pos = 5
# Handshake: type=ClientHello (0x01)
if pos >= len(data) or data[pos] != 0x01:
return None
pos += 1
# Handshake length (3 bytes)
hs_len = struct.unpack("!I", b'\x00' + data[pos:pos+3])[0]
pos += 3
# Client version (2) + Random (32)
pos += 2 + 32
# Session ID
if pos >= len(data):
return None
sid_len = data[pos]
pos += 1 + sid_len
# Cipher Suites
if pos + 2 > len(data):
return None
cs_len = struct.unpack("!H", data[pos:pos+2])[0]
pos += 2 + cs_len
# Compression Methods
if pos >= len(data):
return None
cm_len = data[pos]
pos += 1 + cm_len
# Extensions length
if pos + 2 > len(data):
return None
ext_len = struct.unpack("!H", data[pos:pos+2])[0]
pos += 2
# 확장 순회
end = pos + ext_len
while pos + 4 <= end and pos + 4 <= len(data):
ext_type = struct.unpack("!H", data[pos:pos+2])[0]
ext_data_len = struct.unpack("!H", data[pos+2:pos+4])[0]
pos += 4
if ext_type == 0x0000: # SNI extension
# SNI list length (2)
sni_list_len = struct.unpack("!H", data[pos:pos+2])[0]
sni_pos = pos + 2
sni_end = sni_pos + sni_list_len
while sni_pos + 3 <= sni_end:
name_type = data[sni_pos]
name_len = struct.unpack("!H", data[sni_pos+1:sni_pos+3])[0]
sni_pos += 3
if name_type == 0: # host_name
return data[sni_pos:sni_pos+name_len].decode("ascii")
sni_pos += name_len
pos += ext_data_len
except (IndexError, struct.error):
pass
return None
def match_domain(sni: str) -> tuple | None:
"""SNI를 라우팅 테이블에서 매칭 (정확 → 와일드카드 순)"""
# 정확 매칭
if sni in ROUTING_TABLE:
return ROUTING_TABLE[sni]
# 와일드카드 매칭
parts = sni.split(".")
for i in range(len(parts)):
wildcard = "*." + ".".join(parts[i+1:])
if wildcard in ROUTING_TABLE:
return ROUTING_TABLE[wildcard]
return DEFAULT_UPSTREAM
def relay(src, dst, label):
"""양방향 데이터 릴레이"""
try:
while True:
data = src.recv(4096)
if not data:
break
dst.sendall(data)
except (OSError, BrokenPipeError):
pass
finally:
try: src.shutdown(socket.SHUT_RD)
except OSError: pass
try: dst.shutdown(socket.SHUT_WR)
except OSError: pass
def handle_client(client_sock, client_addr):
"""클라이언트 연결 처리"""
try:
# 원본 목적지 획득 (TPROXY → getsockname)
orig_dst = client_sock.getsockname()
orig_ip, orig_port = orig_dst
# MSG_PEEK로 ClientHello 읽기 (소비하지 않음)
peek_data = client_sock.recv(4096, socket.MSG_PEEK)
if not peek_data:
client_sock.close()
return
sni = parse_tls_sni(peek_data)
print(f"[SNI] {client_addr[0]}:{client_addr[1]} → "
f"{orig_ip}:{orig_port} SNI={sni or '(없음)'}")
# SNI 기반 upstream 결정
upstream_target = None
if sni:
upstream_target = match_domain(sni)
if upstream_target:
up_ip, up_port = upstream_target
else:
up_ip, up_port = orig_ip, orig_port
print(f"[ROUTE] → {up_ip}:{up_port}")
# upstream 연결 (IP_TRANSPARENT)
upstream = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
upstream.setsockopt(socket.SOL_IP, 19, 1) # IP_TRANSPARENT
upstream.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
upstream.bind((client_addr[0], 0))
upstream.settimeout(5)
upstream.connect((up_ip, up_port))
upstream.settimeout(None)
# 양방향 릴레이 스레드
t1 = threading.Thread(target=relay,
args=(client_sock, upstream, "c→u"), daemon=True)
t2 = threading.Thread(target=relay,
args=(upstream, client_sock, "u→c"), daemon=True)
t1.start()
t2.start()
t1.join()
t2.join()
except Exception as e:
print(f"[ERROR] {e}")
finally:
try: client_sock.close()
except OSError: pass
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_IP, 19, 1) # IP_TRANSPARENT
sock.bind(("0.0.0.0", LISTEN_PORT))
sock.listen(128)
print(f"[TLS SNI proxy] listening on :{LISTEN_PORT}")
while True:
client, addr = sock.accept()
t = threading.Thread(target=handle_client,
args=(client, addr), daemon=True)
t.start()
if __name__ == "__main__":
main()
핵심 동작:
MSG_PEEK— 소켓 버퍼에서 데이터를 소비하지 않고 읽습니다. ClientHello를 파싱한 후 원본 데이터를 그대로 upstream에 전달할 수 있습니다.parse_tls_sni()— TLS Record Layer → Handshake → Extensions 순으로 파싱하여 SNI 확장(type 0x0000)에서 호스트네임을 추출합니다.match_domain()— 정확 매칭 우선, 실패 시 와일드카드(*.domain) 매칭을 시도합니다.IP_TRANSPARENT(상수 19) — 커널에 투명 소켓을 선언하여getsockname()으로 원본 목적지를 획득합니다.
10-4. 실행 및 검증
# 1. 프록시 실행
sudo python3 tproxy_sni.py
# 2. 테스트 — openssl s_client로 SNI 지정
openssl s_client -connect 93.184.216.34:443 -servername example.com \
-brief 2>&1 | head -5
# 3. 다른 SNI로 테스트
openssl s_client -connect 10.0.2.1:443 -servername api.internal.io \
-brief 2>&1 | head -5
# 4. curl로 HTTPS 테스트
curl -v --resolve example.com:443:93.184.216.34 https://example.com/ 2>&1 | head -20
# 5. 프록시 로그에서 SNI 파싱 확인
# [SNI] 192.168.100.2:54321 → 93.184.216.34:443 SNI=example.com
# [ROUTE] → 93.184.216.34:443
주의: TLS 1.3에서 ECH(Encrypted Client Hello)가 활성화되면 SNI가 암호화되어 이 방법으로 추출할 수 없습니다.
현재 대부분의 환경에서는 ECH가 기본 비활성화 상태이므로 SNI 파싱이 동작합니다.
10-5. 정리 (Clean-up)
# 프록시 종료
sudo pkill -f tproxy_sni.py
# iptables 규칙 제거
sudo iptables -t mangle -D PREROUTING -p tcp --dport 443 \
-j TPROXY --on-port 8443 --tproxy-mark 0x1/0x1
# 또는 nftables 규칙 제거
sudo nft delete table ip tproxy_sni
# 정책 라우팅 제거
sudo ip rule del fwmark 0x1 lookup 100
sudo ip route del local default dev lo table 100
Lab 11: TPROXY 성능 벤치마크
Lab 개요: 직접 연결 대비 TPROXY 경유 시의 성능 오버헤드를 체계적으로 측정합니다.
wrk(HTTP 벤치마크), iperf3(대역폭), perf(커널 프로파일링)를 사용하여 처리량, 지연 시간, CPU 소비를 정량적으로 분석합니다.
11-1. 벤치마크 토폴로지
11-2. 도구 설치
# wrk (HTTP 벤치마크)
sudo apt-get install -y wrk || {
git clone https://github.com/wg/wrk.git
cd wrk && make -j$(nproc) && sudo cp wrk /usr/local/bin/
}
# iperf3 (대역폭 측정)
sudo apt-get install -y iperf3
# perf (커널 프로파일링)
sudo apt-get install -y linux-tools-$(uname -r) linux-tools-common
# flamegraph (시각화)
git clone https://github.com/brendangregg/FlameGraph.git ~/FlameGraph
# nginx (테스트 웹 서버)
sudo apt-get install -y nginx
echo "OK" | sudo tee /var/www/html/bench.txt
sudo systemctl start nginx
11-3. 자동화 벤치마크 스크립트
#!/bin/bash
# bench_tproxy.sh — TPROXY 성능 비교 벤치마크
set -euo pipefail
SERVER_IP="${1:-127.0.0.1}"
DURATION=30
THREADS=4
CONNECTIONS=100
URL="http://${SERVER_IP}/bench.txt"
RESULT_DIR="./bench_results_$(date +%Y%m%d_%H%M%S)"
mkdir -p "$RESULT_DIR"
echo "=========================================="
echo " TPROXY 벤치마크 시작"
echo " 서버: $SERVER_IP"
echo " 결과 디렉토리: $RESULT_DIR"
echo "=========================================="
# ── 테스트 1: wrk HTTP 벤치마크 ──
echo ""
echo "[1/4] wrk HTTP 벤치마크 (${DURATION}s, ${THREADS}t, ${CONNECTIONS}c)"
echo "--- 직접 연결 ---"
wrk -t${THREADS} -c${CONNECTIONS} -d${DURATION}s --latency "$URL" \
| tee "$RESULT_DIR/wrk_direct.txt"
echo ""
echo "--- TPROXY 경유 (프록시 실행 상태에서) ---"
wrk -t${THREADS} -c${CONNECTIONS} -d${DURATION}s --latency "$URL" \
| tee "$RESULT_DIR/wrk_tproxy.txt"
# ── 테스트 2: iperf3 대역폭 ──
echo ""
echo "[2/4] iperf3 대역폭 측정 (${DURATION}s)"
echo "--- 직접 연결 ---"
iperf3 -c "$SERVER_IP" -t "$DURATION" -P 4 \
| tee "$RESULT_DIR/iperf3_direct.txt"
echo ""
echo "--- TPROXY 경유 ---"
iperf3 -c "$SERVER_IP" -t "$DURATION" -P 4 \
| tee "$RESULT_DIR/iperf3_tproxy.txt"
# ── 테스트 3: perf 프로파일링 ──
echo ""
echo "[3/4] perf 프로파일링 (TPROXY 프록시 프로세스)"
PROXY_PID=$(pgrep -f "tproxy_epoll\|tproxy_uring" | head -1 || true)
if [ -n "$PROXY_PID" ]; then
sudo perf record -F 99 -p "$PROXY_PID" -g -- sleep 10 \
-o "$RESULT_DIR/perf.data"
sudo perf report --stdio -i "$RESULT_DIR/perf.data" \
| head -60 | tee "$RESULT_DIR/perf_report.txt"
else
echo "[SKIP] 프록시 프로세스를 찾을 수 없습니다."
fi
# ── 테스트 4: 커널 함수 핫스팟 ──
echo ""
echo "[4/4] 커널 함수 핫스팟 분석"
if [ -n "$PROXY_PID" ]; then
sudo perf top -p "$PROXY_PID" -d 5 --stdio \
| head -30 | tee "$RESULT_DIR/perf_top.txt"
fi
# ── 결과 요약 ──
echo ""
echo "=========================================="
echo " 벤치마크 완료. 결과: $RESULT_DIR/"
echo "=========================================="
ls -la "$RESULT_DIR/"
11-4. 결과 테이블 템플릿
| 측정 항목 | 직접 연결 (baseline) | TPROXY 경유 | 오버헤드 |
|---|---|---|---|
| wrk RPS | 측정값 req/s | 측정값 req/s | -N% |
| wrk P50 지연 | 측정값 ms | 측정값 ms | +N% |
| wrk P99 지연 | 측정값 ms | 측정값 ms | +N% |
| iperf3 대역폭 | 측정값 Gbps | 측정값 Gbps | -N% |
| iperf3 재전송 | 측정값 회 | 측정값 회 | +N회 |
| CPU 사용률 | 측정값 % | 측정값 % | +N%p |
11-5. perf / Flamegraph 프로파일링
# 1. perf record — TPROXY 프록시 프로세스 프로파일링
PROXY_PID=$(pgrep -f tproxy_epoll)
sudo perf record -F 99 -p $PROXY_PID -g -- sleep 30
# 2. perf report — 함수별 CPU 비중 확인
sudo perf report --stdio | head -40
# 예상 출력:
# Overhead Command Shared Object Symbol
# ........ ............ ................. ............................
# 18.42% tproxy_epoll [kernel.vmlinux] [k] nf_hook_slow
# 12.31% tproxy_epoll [kernel.vmlinux] [k] tcp_v4_rcv
# 9.87% tproxy_epoll [kernel.vmlinux] [k] nf_tproxy_get_sock_v4
# 7.23% tproxy_epoll [kernel.vmlinux] [k] ip_route_input_slow
# 5.61% tproxy_epoll [kernel.vmlinux] [k] fib_table_lookup
# 4.12% tproxy_epoll libc.so.6 [.] __memmove_avx_unaligned
# 3.89% tproxy_epoll [kernel.vmlinux] [k] __netif_receive_skb_core
# 3. Flamegraph 생성
sudo perf script | ~/FlameGraph/stackcollapse-perf.pl \
| ~/FlameGraph/flamegraph.pl --title "TPROXY Proxy Profile" \
> tproxy_flamegraph.svg
# 4. 브라우저에서 확인
echo "flamegraph: file://$(pwd)/tproxy_flamegraph.svg"
11-6. 커널 함수 핫스팟 분석
TPROXY 경로 핵심 커널 함수: perf 결과에서 다음 함수들의 비중을 확인하세요.
| 커널 함수 | 역할 | 예상 비중 | 최적화 힌트 |
|---|---|---|---|
nf_hook_slow |
Netfilter 훅 체인 순회 | 15~20% | 불필요한 규칙 제거, nftables 전환 |
nf_tproxy_get_sock_v4 |
TPROXY 소켓 매칭 | 8~12% | 리스닝 소켓 수 최소화 |
tcp_v4_rcv |
TCP 수신 처리 | 10~15% | GRO/GSO 활성화 |
ip_route_input_slow |
정책 라우팅 조회 | 5~8% | 라우팅 테이블 간소화 |
fib_table_lookup |
FIB 테이블 검색 | 4~6% | 불필요한 라우팅 규칙 제거 |
__copy_skb_header |
sk_buff 메타데이터 복사 | 3~5% | zero-copy (splice, sendfile) |
11-7. 정리 (Clean-up)
# 벤치마크 프로세스 종료
sudo pkill -f wrk || true
sudo pkill -f iperf3 || true
# nginx 중지 (필요 시)
sudo systemctl stop nginx
# perf 데이터 제거
sudo rm -f perf.data perf.data.old
rm -f tproxy_flamegraph.svg
# 결과 디렉토리는 보존 (분석용)
echo "결과 디렉토리: bench_results_*/"
Lab 12: 다중 프로토콜 TPROXY (HTTP + DNS + QUIC)
Lab 개요: 단일 호스트에서 HTTP(TCP 80/443), DNS(UDP 53), QUIC(UDP 443)를
모두 TPROXY로 수신하여 프로토콜별로 분기 처리합니다. nftables로 다중 포트/프로토콜 규칙을 구성하고,
Python 디스패처로 각 프로토콜을 적절한 핸들러에 라우팅합니다.
12-1. 다중 프로토콜 아키텍처
12-2. nftables 규칙
#!/usr/sbin/nft -f
# multiproto_tproxy.nft — 다중 프로토콜 TPROXY 규칙
table ip multiproto {
chain prerouting {
type filter hook prerouting priority mangle; policy accept;
# ── conntrack zone 분리 ──
# TCP HTTP/HTTPS → zone 1
tcp dport { 80, 443 } ct zone set 1
# UDP DNS → zone 2
udp dport 53 ct zone set 2
# UDP QUIC → zone 3
udp dport 443 ct zone set 3
# ── TPROXY 규칙 ──
# HTTP/HTTPS → TCP 프록시 포트
tcp dport { 80, 443 } tproxy to :9080 meta mark set 0x1
# DNS → UDP 프록시 포트
udp dport 53 tproxy to :9053 meta mark set 0x1
# QUIC → UDP 프록시 포트
udp dport 443 tproxy to :9443 meta mark set 0x1
}
}
# 정책 라우팅 (셸에서 실행)
# ip rule add fwmark 0x1 lookup 100
# ip route add local default dev lo table 100
# nftables 규칙 적용
sudo nft -f multiproto_tproxy.nft
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local default dev lo table 100
# 규칙 확인
sudo nft list table ip multiproto
sudo conntrack -L 2>/dev/null | head -10
12-3. Python 디스패처
#!/usr/bin/env python3
"""multiproto_dispatcher.py — 다중 프로토콜 TPROXY 디스패처
TCP(HTTP/HTTPS) + UDP(DNS) + UDP(QUIC) 를 각각 처리."""
import socket
import struct
import select
import threading
import sys
IP_TRANSPARENT = 19
IP_RECVORIGDSTADDR = 20
SO_ORIGINAL_DST = 80
# ── TCP 핸들러 (HTTP/HTTPS) ──
class TCPHandler(threading.Thread):
def __init__(self, port, name):
super().__init__(daemon=True)
self.port = port
self.name = name
def run(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
sock.bind(("0.0.0.0", self.port))
sock.listen(128)
print(f"[{self.name}] TCP listening on :{self.port}")
while True:
client, addr = sock.accept()
t = threading.Thread(target=self._handle,
args=(client, addr), daemon=True)
t.start()
def _handle(self, client, addr):
try:
orig_dst = client.getsockname()
print(f"[{self.name}] {addr[0]}:{addr[1]} → "
f"{orig_dst[0]}:{orig_dst[1]}")
# upstream 연결
upstream = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
upstream.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
upstream.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
upstream.bind((addr[0], 0))
upstream.settimeout(5)
upstream.connect(orig_dst)
upstream.settimeout(None)
# 양방향 릴레이
self._relay(client, upstream)
except Exception as e:
print(f"[{self.name}] ERROR: {e}")
finally:
try: client.close()
except: pass
def _relay(self, a, b):
def forward(src, dst):
try:
while True:
data = src.recv(4096)
if not data: break
dst.sendall(data)
except: pass
try: dst.shutdown(socket.SHUT_WR)
except: pass
t1 = threading.Thread(target=forward, args=(a, b), daemon=True)
t2 = threading.Thread(target=forward, args=(b, a), daemon=True)
t1.start(); t2.start()
t1.join(); t2.join()
# ── UDP 핸들러 (DNS / QUIC) ──
class UDPHandler(threading.Thread):
def __init__(self, port, name):
super().__init__(daemon=True)
self.port = port
self.name = name
def run(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
sock.setsockopt(socket.SOL_IP, IP_RECVORIGDSTADDR, 1)
sock.bind(("0.0.0.0", self.port))
print(f"[{self.name}] UDP listening on :{self.port}")
while True:
data, ancdata, flags, addr = sock.recvmsg(4096, 1024)
orig_dst = self._extract_orig_dst(ancdata)
if orig_dst:
print(f"[{self.name}] {addr[0]}:{addr[1]} → "
f"{orig_dst[0]}:{orig_dst[1]} ({len(data)}B)")
self._forward_udp(data, addr, orig_dst)
else:
print(f"[{self.name}] 원본 목적지 추출 실패")
def _extract_orig_dst(self, ancdata):
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_IP and cmsg_type == IP_RECVORIGDSTADDR:
family, port = struct.unpack("!HH", cmsg_data[:4])
ip = socket.inet_ntoa(cmsg_data[4:8])
return (ip, port)
return None
def _forward_udp(self, data, client_addr, orig_dst):
try:
fwd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
fwd.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
fwd.bind((client_addr[0], 0))
fwd.settimeout(3)
fwd.sendto(data, orig_dst)
resp, _ = fwd.recvfrom(4096)
fwd.close()
# 응답을 원본 목적지 IP로 클라이언트에 반환
reply = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
reply.setsockopt(socket.SOL_IP, IP_TRANSPARENT, 1)
reply.bind(orig_dst)
reply.sendto(resp, client_addr)
reply.close()
except Exception as e:
print(f"[{self.name}] forward error: {e}")
def main():
handlers = [
TCPHandler(9080, "HTTP/HTTPS"),
UDPHandler(9053, "DNS"),
UDPHandler(9443, "QUIC"),
]
for h in handlers:
h.start()
print("[dispatcher] 다중 프로토콜 TPROXY 디스패처 시작")
print("[dispatcher] TCP:9080(HTTP) UDP:9053(DNS) UDP:9443(QUIC)")
try:
for h in handlers:
h.join()
except KeyboardInterrupt:
print("\n[dispatcher] 종료")
if __name__ == "__main__":
main()
conntrack zone 분리가 필요한 이유:
- TCP 443(HTTPS)과 UDP 443(QUIC)은 동일한
IP:port조합을 사용합니다. - conntrack은 기본적으로 5-tuple(프로토콜, src/dst IP, src/dst port)로 연결을 추적하지만, TPROXY 환경에서 동일 IP:port의 TCP/UDP가 혼재하면 conntrack 엔트리가 충돌할 수 있습니다.
ct zone set N으로 프로토콜별 독립 conntrack 영역을 할당하면 이 문제를 방지합니다.
12-4. 실행 및 검증
# 1. 디스패처 실행
sudo python3 multiproto_dispatcher.py
# 2. HTTP 테스트
curl -v http://example.com/
# [HTTP/HTTPS] 192.168.100.2:xxxxx → 93.184.216.34:80
# 3. DNS 테스트
dig @8.8.8.8 example.com A
# [DNS] 192.168.100.2:xxxxx → 8.8.8.8:53 (42B)
# 4. QUIC/HTTP3 테스트 (curl 7.66+ with HTTP/3 지원)
curl --http3 -v https://cloudflare-quic.com/ 2>&1 | head -20
# [QUIC] 192.168.100.2:xxxxx → 104.16.xxx.xxx:443
# 5. conntrack zone 확인
sudo conntrack -L -z 2>/dev/null | head -5
# tcp 6 ... zone=1 src=... dst=... sport=... dport=80
# udp 17 ... zone=2 src=... dst=... sport=... dport=53
# udp 17 ... zone=3 src=... dst=... sport=... dport=443
# 6. nftables 카운터 확인
sudo nft list table ip multiproto
12-5. 정리 (Clean-up)
# 디스패처 종료
sudo pkill -f multiproto_dispatcher.py
# nftables 규칙 제거
sudo nft delete table ip multiproto
# 정책 라우팅 제거
sudo ip rule del fwmark 0x1 lookup 100
sudo ip route del local default dev lo table 100
# conntrack 테이블 초기화
sudo conntrack -F
Lab 13: TPROXY 고가용성 (HA/Failover)
Lab 개요: VRRP(Virtual Router Redundancy Protocol)를 사용하여 TPROXY 프록시의
고가용성(HA) 구성을 실습합니다. keepalived로 VIP 페일오버를 구성하고, conntrackd로 세션 상태를
동기화하여 장애 시에도 기존 연결이 유지되도록 합니다.
13-1. HA 토폴로지
13-2. 패키지 설치
# 두 노드 모두에서 실행
sudo apt-get install -y keepalived conntrack conntrackd
# 커널 파라미터 (IP_FREEBIND, 비로컬 IP 바인드 허용)
echo "net.ipv4.ip_nonlocal_bind = 1" | sudo tee -a /etc/sysctl.d/99-tproxy-ha.conf
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.d/99-tproxy-ha.conf
sudo sysctl -p /etc/sysctl.d/99-tproxy-ha.conf
13-3. keepalived 설정 (MASTER — node-1)
# /etc/keepalived/keepalived.conf (node-1: MASTER)
global_defs {
router_id TPROXY_HA_NODE1
vrrp_garp_master_delay 5
vrrp_garp_master_repeat 3
}
vrrp_script chk_tproxy {
script "/usr/local/bin/check_tproxy.sh"
interval 2 # 2초마다 체크
weight -20 # 실패 시 priority -20
fall 3 # 3회 연속 실패 시 DOWN 판정
rise 2 # 2회 연속 성공 시 UP 복귀
}
vrrp_instance VI_TPROXY {
state MASTER
interface eth0
virtual_router_id 51
priority 100
advert_int 1
authentication {
auth_type PASS
auth_pass tproxy_ha_secret
}
virtual_ipaddress {
192.168.100.100/24 dev eth0
}
track_script {
chk_tproxy
}
# 페일오버 시 conntrackd 동기화 트리거
notify_master "/usr/local/bin/ha_notify.sh MASTER"
notify_backup "/usr/local/bin/ha_notify.sh BACKUP"
notify_fault "/usr/local/bin/ha_notify.sh FAULT"
}
13-4. keepalived 설정 (BACKUP — node-2)
# /etc/keepalived/keepalived.conf (node-2: BACKUP)
global_defs {
router_id TPROXY_HA_NODE2
vrrp_garp_master_delay 5
vrrp_garp_master_repeat 3
}
vrrp_script chk_tproxy {
script "/usr/local/bin/check_tproxy.sh"
interval 2
weight -20
fall 3
rise 2
}
vrrp_instance VI_TPROXY {
state BACKUP
interface eth0
virtual_router_id 51
priority 90 # MASTER보다 낮은 priority
advert_int 1
authentication {
auth_type PASS
auth_pass tproxy_ha_secret
}
virtual_ipaddress {
192.168.100.100/24 dev eth0
}
track_script {
chk_tproxy
}
notify_master "/usr/local/bin/ha_notify.sh MASTER"
notify_backup "/usr/local/bin/ha_notify.sh BACKUP"
notify_fault "/usr/local/bin/ha_notify.sh FAULT"
}
13-5. 헬스체크 및 알림 스크립트
#!/bin/bash
# /usr/local/bin/check_tproxy.sh — TPROXY 프록시 헬스체크
# 프록시 프로세스 생존 확인
if ! pgrep -f "tproxy_epoll\|tproxy_uring\|tproxy_sni" > /dev/null; then
exit 1
fi
# 리스닝 포트 확인
if ! ss -tlnp | grep -q ":8080 "; then
exit 1
fi
# iptables/nftables TPROXY 규칙 존재 확인
if ! sudo iptables -t mangle -L PREROUTING -n 2>/dev/null | grep -q "TPROXY"; then
if ! sudo nft list tables 2>/dev/null | grep -q "tproxy\|multiproto"; then
exit 1
fi
fi
exit 0
#!/bin/bash
# /usr/local/bin/ha_notify.sh — VRRP 상태 전환 알림
STATE=$1
LOGFILE="/var/log/tproxy_ha.log"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> "$LOGFILE"; }
case "$STATE" in
MASTER)
log "VRRP → MASTER 전환"
# conntrackd: 대기 테이블을 활성 테이블로 커밋
conntrackd -C /etc/conntrackd/conntrackd.conf -c
conntrackd -C /etc/conntrackd/conntrackd.conf -f internal
conntrackd -C /etc/conntrackd/conntrackd.conf -R
conntrackd -C /etc/conntrackd/conntrackd.conf -B
# TPROXY 프록시 시작 (아직 실행 중이 아니면)
if ! pgrep -f tproxy_epoll > /dev/null; then
/usr/local/bin/tproxy_epoll &
log "TPROXY 프록시 시작"
fi
;;
BACKUP)
log "VRRP → BACKUP 전환"
conntrackd -C /etc/conntrackd/conntrackd.conf -c
conntrackd -C /etc/conntrackd/conntrackd.conf -f internal
conntrackd -C /etc/conntrackd/conntrackd.conf -R
conntrackd -C /etc/conntrackd/conntrackd.conf -B
;;
FAULT)
log "VRRP → FAULT 발생"
conntrackd -C /etc/conntrackd/conntrackd.conf -c
conntrackd -C /etc/conntrackd/conntrackd.conf -f internal
;;
esac
# 스크립트 실행 권한 부여 (두 노드 모두)
sudo chmod +x /usr/local/bin/check_tproxy.sh
sudo chmod +x /usr/local/bin/ha_notify.sh
13-6. conntrackd 설정
# /etc/conntrackd/conntrackd.conf
Sync {
Mode FTFW {
DisableExternalCache Off
CommitTimeout 1800
PurgeTimeout 5
}
UDP {
IPv4_address 192.168.100.11 # node-1 → .11, node-2 → .12
IPv4_Destination_Address 192.168.100.12 # 피어 주소
Port 3780
Interface eth0
SndSocketBuffer 1249280
RcvSocketBuffer 1249280
Checksum on
}
}
General {
Nice -20
HashSize 32768
HashLimit 131072
LogFile on
Syslog on
LockFile /var/lock/conntrack.lock
UNIX {
Path /var/run/conntrackd.ctl
}
# TPROXY 관련 연결만 동기화
Filter From Userspace {
Protocol Accept {
TCP
UDP
}
Address Accept {
IPv4_address 192.168.100.0/24
}
}
}
주의: conntrackd 설정에서
IPv4_address와 IPv4_Destination_Address는
node-1과 node-2에서 서로 반대로 설정해야 합니다. node-2에서는 IPv4_address를 192.168.100.12,
IPv4_Destination_Address를 192.168.100.11로 변경하세요.
13-7. IP_FREEBIND 설정
# IP_FREEBIND — VIP가 아직 할당되지 않은 상태에서도 바인드 허용
# TPROXY 프록시 코드에 추가 (C 예시)
int opt = 1;
setsockopt(fd, SOL_IP, IP_FREEBIND, &opt, sizeof(opt));
# 또는 커널 파라미터로 전역 설정
echo "net.ipv4.ip_nonlocal_bind = 1" | sudo tee -a /etc/sysctl.d/99-tproxy-ha.conf
sudo sysctl -p /etc/sysctl.d/99-tproxy-ha.conf
# Python 예시
# IP_FREEBIND = 15
# sock.setsockopt(socket.SOL_IP, 15, 1)
IP_FREEBIND vs IP_TRANSPARENT:
IP_FREEBIND— 아직 인터페이스에 할당되지 않은 IP 주소에bind()를 허용합니다. VRRP 페일오버 시 VIP가 아직 없는 BACKUP 노드에서 프록시를 미리 시작할 수 있습니다.IP_TRANSPARENT— 자신이 소유하지 않은 IP 주소에bind()를 허용합니다. TPROXY에서 클라이언트 원본 IP로 바인드할 때 사용합니다.- HA 환경에서는 두 옵션을 모두 설정하는 것이 일반적입니다.
13-8. 서비스 시작
# 두 노드 모두에서 실행
# 1. TPROXY iptables 규칙 적용
sudo iptables -t mangle -A PREROUTING -p tcp --dport 80 \
-j TPROXY --on-port 8080 --tproxy-mark 0x1/0x1
sudo ip rule add fwmark 0x1 lookup 100
sudo ip route add local default dev lo table 100
# 2. TPROXY 프록시 시작
sudo /usr/local/bin/tproxy_epoll &
# 3. conntrackd 시작
sudo systemctl start conntrackd
sudo systemctl enable conntrackd
# 4. keepalived 시작
sudo systemctl start keepalived
sudo systemctl enable keepalived
# 5. 상태 확인
sudo systemctl status keepalived
sudo systemctl status conntrackd
ip addr show eth0 | grep "192.168.100.100" # MASTER에서만 보여야 함
13-9. 페일오버 테스트
# ── 터미널 1: 지속 트래픽 생성 ──
while true; do
curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
http://192.168.100.100/
sleep 0.5
done
# ── 터미널 2 (MASTER node-1): 장애 시뮬레이션 ──
# 방법 1: keepalived 중지
sudo systemctl stop keepalived
# → BACKUP(node-2)이 MASTER로 승격, VIP 이동
# → 터미널 1에서 트래픽 중단 없이 계속 200 응답 확인
# 방법 2: TPROXY 프록시 프로세스 종료
sudo pkill -f tproxy_epoll
# → vrrp_script 실패 → priority 하락 → BACKUP 승격
# 방법 3: 네트워크 인터페이스 다운
sudo ip link set eth0 down
# → VRRP 광고 중단 → BACKUP이 MASTER로 전환
# ── 터미널 3 (BACKUP node-2): 상태 모니터링 ──
sudo journalctl -u keepalived -f
# Keepalived_vrrp[xxxx]: VRRP_Instance(VI_TPROXY) Entering MASTER STATE
# conntrack 세션 확인
sudo conntrack -L 2>/dev/null | wc -l
# → MASTER에서 동기화된 세션 수가 보여야 함
# ── 복구 테스트 ──
# node-1에서 keepalived 재시작
sudo systemctl start keepalived
# → priority가 높으므로 다시 MASTER로 복귀 (preemption)
# → conntrackd가 세션 재동기화
| 테스트 시나리오 | 예상 페일오버 시간 | 세션 유지 | 확인 방법 |
|---|---|---|---|
| keepalived 중지 | 1~3초 | conntrackd 동기화된 세션 유지 | ip addr show VIP 이동 확인 |
| 프록시 프로세스 종료 | 4~8초 (3회 체크 실패) | 새 연결만 BACKUP에서 처리 | journalctl -u keepalived |
| 네트워크 다운 | 3~4초 (3x advert_int) | conntrackd 세션 유지 | conntrack -L 엔트리 확인 |
| MASTER 복귀 | 1~2초 (preemption) | 양방향 세션 동기화 | conntrackd -s 통계 확인 |
13-10. 정리 (Clean-up)
# 두 노드 모두에서 실행
# 1. 서비스 중지
sudo systemctl stop keepalived
sudo systemctl stop conntrackd
sudo pkill -f tproxy_epoll || true
# 2. iptables 규칙 제거
sudo iptables -t mangle -D PREROUTING -p tcp --dport 80 \
-j TPROXY --on-port 8080 --tproxy-mark 0x1/0x1
sudo ip rule del fwmark 0x1 lookup 100
sudo ip route del local default dev lo table 100
# 3. 설정 파일 복원
sudo rm -f /etc/keepalived/keepalived.conf
sudo rm -f /etc/conntrackd/conntrackd.conf
sudo rm -f /usr/local/bin/check_tproxy.sh
sudo rm -f /usr/local/bin/ha_notify.sh
sudo rm -f /etc/sysctl.d/99-tproxy-ha.conf
sudo sysctl -p
# 4. conntrack 테이블 초기화
sudo conntrack -F
# 5. 자동 시작 비활성화
sudo systemctl disable keepalived
sudo systemctl disable conntrackd
참고 자료
- 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