Cilium 문서에서도 나오듯 TPROXY는 클라이언트 트래픽의 목적지 주소를 바꾸지 않고 프록시로 “투명하게” 가로채는 방식입니다.
NAT 없이 원래 목적지(IP/포트)를 보존하므로, 앱에서 정책/로깅을 정확하게 할 수 있습니다.
Non-local Socket Binding
TPROXY를 쓰려면 먼저 로컬에 없는 IP로도 리슨할 수 있어야 합니다.
클라이언트가 특정 서버로 요청을 보냈을 때, 중간 프록시가 그 주소 그대로 받아야 하기 때문입니다. 이걸 비로컬 소켓 바인딩(Non-local Socket Binding) 이라 부릅니다.
예시
- 사용자가 100.0.0.1:80 으로 접속
- 중간 프록시는 그 주소 그대로 받고 싶음
- 문제: 프록시 호스트엔 보통 100.0.0.1 이 없어서 기본 규칙에선 해당 주소로 bind() 가 실패
- 해결: 비로컬 바인딩을 허용하면 로컬에 없는 IP로도 리슨 가능. 여기에 IP_TRANSPARENT 를 더하면 커널이 패킷을 그 “투명 소켓”에 붙여줍니다.
비로컬 소켓 바인딩(Non-local Socket Binding) 기능을 켜면 로컬에 존재하지 않는 IP에도 소켓을 열 수 있고, 여기에 IP_TRANSPARENT 옵션을 함께 사용하면 커널이 들어오는 패킷을 그 “투명 소켓”에 연결해 줄 수 있습니다.
1) sysctl: 비로컬 바인딩 허용
# IPv4에서 비로컬 바인딩 허용
sysctl -w net.ipv4.ip_nonlocal_bind=1
# 영구 반영 (예: /etc/sysctl.conf 에 추가)
echo "net.ipv4.ip_nonlocal_bind=1" >> /etc/sysctl.conf
sysctl -p
2) 애플리케이션: IP_TRANSPARENT 으로 소켓 오픈
int fd = socket(AF_INET, SOCK_STREAM, 0);
int optval = 1;
setsockopt(fd, SOL_IP, IP_TRANSPARENT, &optval, sizeof(optval));
이제 TPROXY 를 적용할 준비가 됐습니다.
iptables 기반 TPROXY: 투명 소켓 붙이기 vs on-port 바인딩
비로컬 소켓 바인딩이 가능해졌다면 iptables에서 TPROXY 흐름을 구성할 수 있습니다. 방식은 크게 두 가지인데, 하나는 이미 떠 있는 투명 소켓을 찾아 붙이는 방법이고, 다른 하나는 특정 포트로 바로 바인딩하는 방법입니다. 두 방식 모두 목적지는 그대로 유지된다는 점이 같습니다.
예시는 리눅스 공식 문서에 나온 설정을 참고했습니다.
패턴 A: 떠 있는 투명 소켓에 붙이기
아래 예시는 비로컬 바인딩과 IP_TRANSPARENT로 열린 리스닝 소켓이 있을 때, 커널이 그 소켓을 자동으로 찾아 패킷을 넘겨주도록 만드는 흐름입니다.
# DIVERT 체인 생성
iptables -t mangle -N DIVERT
# PREROUTING 단계에서 transparent 소켓이 있는 경우 DIVERT로 이동
iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j DIVERT
# mark 값 세팅
iptables -t mangle -A DIVERT -j MARK --set-mark 1
# 해당 패킷은 로컬에서 수락
iptables -t mangle -A DIVERT -j ACCEPT
패턴 B) 특정 포트로 바로 TPROXY 하기
이 방식은 들어오는 트래픽을 지정한 로컬 포트로 곧장 바인딩합니다. 프록시가 해당 포트에서 리슨 중이어야 하며, 마찬가지로 나중 단계에서 이 패킷을 로컬로 끌어들이기 위해 마크를 사용합니다.
# 예: TCP/80을 프록시 포트 50080으로 가로채기
iptables -t mangle -A PREROUTING -p tcp --dport 80 -j TPROXY \
--on-port 50080 --tproxy-mark 0x1/0x1
- --on-port = 프록시가 리슨 중인 로컬 포트
- --tproxy-mark = 이후 정책 라우팅에서 사용할 마크
라우팅 정책: 규칙 기반 라우팅(PBR)
여기까지 설정했다고 해서, 커널이 자동으로 “이 패킷은 로컬 애플리케이션으로 보내야지” 하고 알아서 처리해 주지는 않습니다. PREROUTING에서 붙잡은 패킷을 실제로 로컬 네트워크 스택으로 밀어 넣는 역할은 규칙 기반 라우팅(Policy-Based Routing, ip rule) 이 맡습니다.
# 마크=1 → table 100
ip rule add fwmark 1 lookup 100
# table 100: 모든 목적지를 로컬로 처리
ip route add local 0.0.0.0/0 dev lo table 100
핵심은 간단합니다. iptables에서 마크(fwmark) 를 찍고, ip rule로 특정 테이블(table 100) 로 보내며, 그 테이블에 local 라우트를 넣어 루프백(lo) 으로 강제로 인입시키는 구조입니다. 이 경로를 타야 커널이 투명 소켓을 찾아 프록시 애플리케이션으로 패킷을 넘겨줍니다. 만약 이 단계가 없다면, 패킷은 기존 라우팅 테이블을 따라 외부로 포워딩되거나 중간에 드롭될 수 있습니다.
커널에서 발생하는 일
좀 더 상세하게 커널에서 어떻게 동작하는지도 살펴보겠습니다.
일반적으로 수신 패킷은 라우팅 결정에서 로컬로 판정된 뒤 LOCAL_IN 훅을 지나, ip_local_deliver_finish()에서 프로토콜 핸들러가 호출됩니다.
TCP는 tcp_v4_rcv(), UDP는 udp_v4_rcv()가 해당하며, 이 단계에서 커널은 목적지에 맞는 소켓을 조회합니다. 소켓이 발견되면 해당 소켓의 수신 큐로 패킷을 넘깁니다.
TPROXY를 사용하면 이 흐름에 미리 소켓을 붙여두는 단계가 추가됩니다. PREROUTING(mangle) 단계에서 -m socket --transparent 또는 -j TPROXY가 대상 소켓을 찾아 skb->sk에 연결하고, 동시에 fwmark를 설정합니다. 이어서 규칙 기반 라우팅(ip rule) 으로 fwmark 1 → table 100 → local 0.0.0.0/0 dev lo 경로를 타게 해 로컬 스택으로의 인입을 보장합니다. 그 뒤 LOCAL_IN을 지나 ip_local_deliver_finish()에 도달하면 TCP/UDP 핸들러가 호출되는데, 이 시점에는 이미 skb->sk가 설정되어 있으므로(핸들러 내부의 skb_steal_sock() 경로) 별도의 소켓 탐색 없이 그 소켓으로 바로 배달됩니다. 결과적으로, 클라이언트가 원래 목적지로 보낸 트래픽을 NAT 변경 없이 프록시 애플리케이션이 처리하게 됩니다.
cilium에서의 TProxy
트래픽이 Service(LoadBalancer/NodePort 또는 HostNetwork) 포트에 도착하면, 노드의 eBPF 코드가 그 자리에서 패킷을 가로채 Envoy로 투명 전달(TPROXY) 합니다. 이 과정에서 목적지 IP/포트는 그대로 유지됩니다.
내부에서 무슨 일이 일어나나
- eBPF가 소켓을 먼저 고름
bpf_sk_lookup_tcp/udp()로 Envoy 소켓을 찾고, bpf_sk_assign(ctx, sk, 0)으로 패킷에 소켓을 연결합니다(= skb->sk 설정). - 마크를 찍어 로컬 진입 보장
ctx->mark 로 ip rule → table 100 → local(lo) 경로를 타게 해서 스택에 확실히 인입시킵니다.
그래서 DNAT/REDIRECT 없이 Envoy가 원 목적지 보존 상태로 패킷을 받습니다.
참고: bpf_sk_assign()은 tc ingress 에서만 유효합니다. host egress 등 다른 경로는 별도 우회(예: cilium-host 리다이렉트 + 마크 유지)를 사용합니다.
externalTrafficPolicy와의 관계
일반 컨트롤러는 externalTrafficPolicy: Local/Cluster 에 따라 어느 노드로 라우팅할지와 클라이언트 IP가 보이는지가 크게 달라집니다.
Cilium Ingress/Gateway는 예외가 있습니다. Envoy를 노출하는 Service로 들어온 Ingress 트래픽은 항상 “로컬 노드”에서 eBPF로 가로챈 뒤 TPROXY로 Envoy에 전달합니다. 이 때문에 일반적인 Local/Cluster의 기대와 클라이언트 IP 가시성 동작이 다를 수 있습니다. (kube-proxy 환경에서의 “Local=소스 IP 보존” 같은 규칙이 그대로 적용된다고 단정하지 말고, Cilium Ingress 문서의 가이드에 맞춰 확인하는 것이 안전합니다.)
'K8S > 🔥 network study🔥' 카테고리의 다른 글
[cilium] Mutual Authentication 테스트 해보기 (진행 중) (0) | 2025.08.22 |
---|---|
[cilium] gateway API로 트래픽 분할하기 (0) | 2025.08.22 |
[cilium] L7 Traffic Shifting 테스트 (2) | 2025.08.22 |
[cilium] Service Mesh 구성하기 (shared 모드) (0) | 2025.08.22 |
[cilium] containerlab으로 lb ippool 테스트 해보기 (6) | 2025.08.17 |