본문 바로가기
devops/cilium

[Cilium ServiceMesh] TProxy

by gnobaaaar 2026. 2. 15.
Cilium ServiceMesh 테스트전 개념정리

 

TProxy

Cilium TProxy는 L7 정책을 적용할 때 트래픽을 목적지 IP를 유지한 채 노드의 Envoy로 가로채는 메커니즘입니다.
일반적인 NAT REDIRECT 가 아닌 투명 프록시 방식으로 커널 레벨에서 트래픽을 Envoy로 보냅니다.
 

Envoy

Cloud Native Computing Foundation(CNCF) 공식 프로젝트
구글 IBM 리프트(Lyft)가 중심이 되어 개발하고 있는 오픈 소스 소프트웨어
C++ 로 구현된 고성능 Proxy 입니다.
 

 
아래와 같은 특징을 가집니다.

  • 중앙에서 동적 설정 관리 필요
  • 풍부한 API 지원
  • 필터 체인 구조
  • mTLS, L7 라우팅
  • hot reload
  • xDS 기반 제어

 
다만, Istio 에서의 Envoy와 Cilium에서 Envoy는 차이를 가집니다.
 
 

Istio에서 Envoy

Istio의 Sidecar 모드에서는 mesh에 참여하는 Pod 간 트래픽이 Envoy를 통과합니다.
Envoy는 L4(TCP)와 L7(HTTP/gRPC) 프록시 역할을 모두 수행하며, Control Plane인 Istiod가 xDS를 통해 Envoy 설정을 동적으로 관리합니다.
다만 Kubernetes Service 레벨의 L4 로드밸런싱은 kube-proxy가 처리합니다.

https://gnobaaaaar.tistory.com/125

[istio] envoy proxy

istio in action 3장과 블로그들을 참조했습니다. 사내에서 istio 관련 기능을 몇 가지 테스트해볼 기회가 있었습니다.이해를 위해 흐름을 간단하게 정리해보았습니다. istio envoy proxyistio envoy proxy는 파

gnobaaaaar.tistory.com

 
 

Cilium에서 Envoy

https://docs.cilium.io/en/latest/security/network/proxy/envoy/
 
네트워크의 모든 트래픽 제어가 목적인 Istio 의 Envoy와 달리,
Cilium은 L7 정책이 있을 때만 Envoy 사용됩니다.

Cilium의 기본 데이터플레인은 eBPF이며,
L3/L4 네트워크 기능(Service LB, 정책 등)은 Envoy 없이 처리됩니다.
L7 정책이나 HTTP/gRPC 기반 기능이 활성화된 경우에만
Node-level Envoy가 사용되며, 이때 Transparent Proxy(TPROXY) 메커니즘이 동작합니다.
 
 
 

동작흐름

Client가 아래와 같은 IP를 가진 특정 목적지(ex. 웹서버)로 접근했다고 하겠습니다.

Clinet -> 10.1.2.20:80

 
Istio Envoy가 트래픽을 제어하는 경우에는 아래와 같이 
application 컨테이너의 목적지는 envoy의 outbound capture 포트인 15001으로 REDIRECT 됩니다 (NAT)

App
 ↓
iptables REDIRECT
 ↓
127.0.0.1:15001 (Envoy sidecar)

 
반면 Cilium은 L7 정책이 활성화된 경우
NAT 없이 목적지 IP 유지한 채 Node Envoy로 전달합니다.
(사이드카 형태의 Istio 와 달리 Cilium 은 노드 단위 파드형태로 동작합니다)

Client → 10.1.2.20:80
        ↓
eBPF intercept
        ↓
Transparent socket redirect
        ↓
Node Envoy (IP 그대로)

 
 
위 흐름도에서 보이는 Transparent Proxy 에 대해 확인해보겠습니다.
아래 글을 참조했습니다. 원래 글을 한번 읽어보시길 추천드립니다.
https://nuguni.tistory.com/138

[cilium] TProxy 이해해보기

Cilium 문서에서도 나오듯 TPROXY는 클라이언트 트래픽의 목적지 주소를 바꾸지 않고 프록시로 “투명하게” 가로채는 방식입니다.NAT 없이 원래 목적지(IP/포트)를 보존하므로, 앱에서 정책/로깅을

nuguni.tistory.com

 

Non-local Socket Binding

아까 클라이언트가 10.1.2.20:80 으로 접근하는 경우를 이야기하였습니다.

Clinet -> 10.1.2.20:80

 
해당 트래픽을 가져가도 프록시 호스트에는 10.1.2.20 이라는 IP주소가 없으므로
해당 주소로 bind() 가 실패하게 됩니다.
 

bind()는 특정 소켓을 로컬 IP:port에 연결해서
해당 IP:port로 들어오는 트래픽을 이 소켓이 받도록 커널에 등록하는 동작입니다.

리눅스는 내가 가진 IP에만 bind가 가능합니다.
ex. ip addr -> 내가 가진 네트워크 인터페이스에 IP 목록 확인가능합니다.

 
 
Envoy는 Node 네임스페이스에는 Pod IP(10.1.2.20)가 존재하지 않으므로 기본적으로 bind()가 실패합니다.
그래서 우선 비로컬 바인딩허용을 해줘서 로컬에 없는 IP도 bind()를 가능하게 해줍니다.

# IPv4에서 비로컬 바인딩 허용
sysctl -w net.ipv4.ip_nonlocal_bind=1
 
# 영구 반영 (예: /etc/sysctl.conf 에 추가)
echo "net.ipv4.ip_nonlocal_bind=1" >> /etc/sysctl.conf
sysctl -p

 
bind() 뿐만 아니라 통신을 위해 소켓(네트워크 통신을 위한 파일 디스크립터, TCP통신용 문)도 설정이 필요합니다.

소켓은 나중에 다른 글로 좀더 자세하게 다뤄보겠습니다.

 
어플리케이션에서 소켓을 열때 해당 소켓은 투명소켓이라고 커널에 알려줍니다.
 

int fd = socket(AF_INET, SOCK_STREAM, 0);
int optval = 1;
setsockopt(fd, SOL_IP, IP_TRANSPARENT, &optval, sizeof(optval));

 
 
애플리케이션이 소켓을 생성한 뒤
IP_TRANSPARENT 옵션을 설정함으로써
그 소켓이 투명 프록시용으로 동작할 수 있도록 커널에 요청합니다.
 
 

Iptables 기반 TPROXY

흐름은 아래와 같습니다.
자세하게 확인해보겠습니다.

PREROUTING
  ↓
TPROXY → skb->sk 세팅
  ↓
MARK=1
  ↓
ip rule → table 100
  ↓
local route → lo
  ↓
LOCAL_IN
  ↓
ip_local_deliver_finish()
  ↓
tcp_v4_rcv()
  ↓
이미 연결된 소켓(skb->sk)으로 바로 전달

 
 
위 링크 원본글을 참조하시길 바랍니다
 

패턴 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 하기

들어오는 트래픽을 특정포트 (ex.로컬 50080 포트)로 보냅니다.

-j TPROXY --on-port 50080

 
 

라우팅 정책: 규칙 기반 라우팅(PBR)

iptables에서 TPROXY를 걸었다고 끝이 아닙니다.
커널입장에서 아래 목적지를 보고 라우팅을 태우면 그만입니다.

# 언급했듯이 로컬IP 주소가 아닙니다 
목적지=10.1.2.20

 
이 패킷은 로컬 애플리케이션으로 보내라고 설정해야합니다.
fwmark + ip rule 으로 진행됩니다.
 

iptables에서 fwmark 찍기

이 패킷은 특별 대상이라고 찍습니다.

-j MARK --set-mark 1

 

 ip rule로 경로 바꾸기

mark=1이면 일반 라우팅 테이블 말고 table 100을 쓰라고 설정해 둡니다.

ip rule add fwmark 1 lookup 100

 

table100 에서 로컬로 강제 인입

모든 목적지를 로컬, 루프백(lo)로 처리합니다.

ip route add local 0.0.0.0/0 dev lo table 100

 
 
 

# 패킷이 NIC를 통해 커널에 들어오면 가장 먼저 통과하는 netfilter hook
PREROUTING
  ↓
TPROXY → skb->sk 세팅
  ↓
MARK=1
  ↓
ip rule → table 100
  ↓
local route → lo
  ↓
LOCAL_IN
  ↓
ip_local_deliver_finish()
  ↓
# TCP 패킷 처리
tcp_v4_rcv()
  ↓
이미 연결된 투명소켓(skb->sk)으로 바로 전달

 
 

Cilium에서..

Cilium 기본동작에서,
Service(LoadBalancer/NodePort/HostNetwork) 포트에 트래픽이 도착하면 eBPF가 가로챕니다.
 
Cilium L7 모드에서는 eBPF가 TC ingress에서
bpf_sk_lookup_  : 커널 소켓 테이블에서 투명소켓을 찾는 과정 (iptables TPROXY의 “-m socket”과 유사한 역할)
로 Envoy 소켓을 조회하고,
bpf_sk_assign() : 이 패킷을 이 소켓으로 전달하라고 지정 (iptables의 skb->sk 설정과 동일한 eBPF 동작)
로 패킷을 해당 소켓에 연결합니다.

이후 fwmark와 정책 라우팅을 이용해 패킷을 로컬 스택으로 인입시켜,
Envoy로 전달되는 과정에서는 DNAT/REDIRECT가 발생하지 않으며 목적지 IP가 유지됩니다.
 
 
 
블로그 글을 참고해서 확인했던 iptables TPROXY는 netfilter와 정책 라우팅을 통해 이를 구현하지만, 
Cilium은 동일한 개념을 eBPF 기반 데이터패스로 재구성합니다.
두 방식의 차이는 어디에서 트래픽을 제어하느냐에 있습니다.
Cilium은 이를 eBPF 기반으로, NAT에 의존하지 않는 L7 제어를 구현합니다.
또한 유지되는 목적지 IP를 통해 로그 상 목적지 IP 왜곡 없이, 리전별 트래픽 구분을 명확하게 할 수 있는 장점이 있습니다.
 
 
 

참조) skb->sk

• skb = 커널이 네트워크 패킷을 들고 다니는 객체 (struct sk_buff)
• sk = 커널의 소켓 객체 (struct sock)
 
일반적으로 NIC에 패킷이 들어오면 아래와 같은 순서로 동작됩니다.

1. PREROUTING(초기 훅)
2. 라우팅 결정 (이 패킷이 로컬로 갈지, 포워딩될지)
3. LOCAL_IN → 프로토콜 처리(TCP면 tcp_v4_rcv())

 
이때 tcp_v4_rcv()는 원래 dst IP:port 기준으로 누가 받을 소켓인지를 찾아야 합니다.
이 과정에서 소켓 lookup 이 이루어 집니다 (커널 소켓 테이블에서 매칭합니다)
즉, 일반 흐름에서는 skb->sk가 미리 정해져 있지 않습니다. (= NULL인 경우가 많습니다)
 
하지만, Tproxy는 사전에 원래 목적지 소켓이 아닌, 투명소켓 (프록시 소켓) 으로 보내라고 설정됩니다.
이를, TPROXY → skb->sk 세팅 으로 위에서는 표현되었습니다.
https://thinking-bank.tistory.com/20

[리눅스 커널] sk_buff 구조체란?

1. sk_buff 구조체란?sk_buff(Socket Buffer)는 Linux 커널 네트워크 스택에서 패킷(프레임 포함)을 표현하는 데 사용되는 핵심 구조체입니다.이 구조체는 네트워크 계층 간에 데이터를 전달할 때 사용되며

thinking-bank.tistory.com

 
 
 

뭔가

쿠버네티스와 네트워크를 보는데 점점 리눅스를 많이 보게 되는 느낌...