🏠 목록 Sidecar 트래픽 캡처 — iptables/nftables와 15001·15006 📄 MD 원본 🌓 테마
istiosidecariptablesnftablestraffic-captureenvoydataplane

Sidecar 트래픽 캡처 — iptables/nftables와 15001·15006

ℹ 이 문서가 다루는 것

mesh의 모든 기능(mTLS·route·authz·telemetry)은 "app의 모든 패킷이 Envoy를 지나간다"는 단 하나의 전제 위에 서 있다. 그 전제를 app 코드 수정 없이 강제하는 장치가 커널 netfilter(iptables/nftables) redirection이다. 이 문서는 그 redirection을 — 왜 필요한지(배경)부터, 누가 어떻게 rule을 거는지(pilot-agent istio-iptablesIptablesConfigurator.Runiptables-restore --noflush), REDIRECT mode의 핵심 체인과 proxy UID/GID 1337 무한 루프 방지 불변식, 실제 *nat 블록·--dry-run·native nftables 경로·검증 명령까지 — 한 줄기로 따라간다. 결론: app은 평소처럼 service:port로 connect하지만 모든 트래픽이 app 모르게 투명하게 Envoy를 통과한다.

⚠ 범위

본 문서는 sidecar mode 전용이다. Ambient mode는 per-pod 15001/15006 모델이 아니라 CNI + ztunnel 기반 redirection을 쓰므로 datapath가 완전히 다르다 — 본 문서 범위 밖.


00. 배경 — 왜 "투명 캡처"가 필요한가

mesh가 약속하는 것들 — 두 pod 사이 자동 mTLS, VirtualService route, AuthorizationPolicy 차단, 요청 단위 telemetry — 은 전부 "트래픽이 어딘가의 proxy를 거쳐야" 가능하다. proxy가 패킷을 손에 쥐어야 암호화하고, 경로를 바꾸고, 거부하고, 세는 것이 가능하기 때문이다.

문제는 app이 그 proxy의 존재를 몰라야 한다는 점이다. "트래픽을 proxy로 보내라"를 app에게 요구하면(예: 환경변수 HTTP_PROXY, SDK 주입, 호스트네임 변경) mesh는 더 이상 인프라가 아니라 애플리케이션 변경이 된다. 수백 개 서비스에 코드/설정 변경을 강제할 수 없다. 그래서 Istio가 푸는 진짜 문제는 이것이다.

app은 service:9080으로 평소처럼 connect하는데, 그 패킷이 app도 모르게 같은 pod 안의 Envoy로 빨려 들어가게 만들 수 있는가?

답은 L4 아래, 커널에서 가로채기다. app의 socket이 보낸 패킷은 커널 network stack을 반드시 통과하고, 커널에는 패킷의 목적지를 바꾸는 hook 지점(netfilter)이 이미 있다. Istio는 그 hook에 rule을 심어, app이 의도한 목적지 대신 로컬 Envoy 포트로 목적지를 바꿔치기(REDIRECT) 한다. app socket 입장에서는 여전히 service:9080에 연결했다고 믿지만, 실제 패킷은 127.0.0.1:15001(Envoy)로 들어간다. 이것이 "투명(transparent)"의 의미다.

선행 개념 — netfilter REDIRECT 한 가지만: Linux netfilter의 nat 테이블에는 PREROUTING(들어오는 패킷이 라우팅되기 전), OUTPUT(로컬 프로세스가 내보내는 패킷) 같은 hook이 있고, REDIRECT target은 패킷의 목적지 주소/포트를 로컬 머신의 다른 포트로 갈아끼운다. iptables는 그 rule을 거는 전통 도구이고, nftables는 후속 세대다. Istio가 하는 일은 결국 이 hook에 "app 트래픽이면 로컬 Envoy 포트로 REDIRECT"라는 rule 한 묶음을 pod의 network namespace 안에 박는 것이 전부다.

[ 트래픽 캡처가 없으면 ]
  app socket → 커널 → NIC → 진짜 목적지       (Envoy를 건너뜀 = mesh 무력화)

[ 트래픽 캡처가 있으면 ]
  app socket → 커널 netfilter REDIRECT → Envoy 15001/15006 → ... → 진짜 목적지
              └─ app은 이 우회를 전혀 모름 (transparent) ─┘

이 한 우회가 mTLS/route/authz/telemetry 전부의 전제다. 캡처가 안 걸리면 Envoy 설정이 아무리 완벽해도 트래픽이 Envoy를 안 지나가니 mesh 기능이 통째로 죽는다. 그래서 "왜 내 정책이 안 먹지?"의 1차 용의자는 항상 캡처가 실제로 박혔는지다.


01. 멘탈모델 앵커 — 두 개의 입구, 하나의 불변식

★ 한 문장 멘탈모델

Istio sidecar 캡처의 본질은 단 세 가지다 — (1) app이 밖으로 나가는 패킷은 커널이 Envoy의 outbound 입구 15001로 REDIRECT한다, (2) pod로 들어오는 패킷은 Envoy의 inbound 입구 15006으로 REDIRECT한다, (3) 단 Envoy(UID/GID 1337) 자신이 만든 패킷은 다시 잡지 않는다(무한 루프 방지). 이 세 줄에서 나머지 모든 체인·포트·예외가 따라 나온다.

이 그림 하나만 머리에 박으면 된다.

                       pod network namespace
  ┌──────────────────────────────────────────────────────────────┐
  │                                                                │
  │  app ──connect svc:9080──> [OUTPUT] ─ ISTIO_OUTPUT             │
  │                                          │                     │
  │                              uid/gid 1337? ─yes─> RETURN ──────┼─> 진짜 목적지
  │                                          │ no                  │   (Envoy가 나간 것)
  │                                          v                     │
  │                                    REDIRECT :15001 ─> Envoy out │
  │                                                                │
  │  remote ──> podIP:9080 ─ [PREROUTING] ─ ISTIO_INBOUND          │
  │                                          │                     │
  │                          15008/15020/15021/15090? ─yes─> RETURN│
  │                                          │ no                  │
  │                                          v                     │
  │                                    REDIRECT :15006 ─> Envoy in ─┼─> app :9080
  │                                                                │
  └──────────────────────────────────────────────────────────────┘

방향을 가르는 것이 핵심이다. outbound는 OUTPUT hook(로컬 app이 내보냄)에서, inbound는 PREROUTING hook(밖에서 들어옴)에서 잡힌다. 이건 netfilter의 hook 의미상 자연스럽다 — 나가는 패킷은 OUTPUT을, 들어오는 패킷은 PREROUTING을 지나니까.

왜 outbound 15001 / inbound 15006을 분리했나

포트 숫자 자체엔 네트워크 표준 의미가 없다. Istio 내부 convention일 뿐이고, 소스 기본값도 그냥 박혀 있다.

ProxyPort:          "15001"   // outbound capture port
InboundCapturePort: "15006"   // inbound capture port
InboundTunnelPort:  "15008"   // tunnel/HBONE inbound tunnel port

중요한 건 왜 하나로 안 합쳤나다. 같은 패킷이라도 "내 pod에서 나가는 요청"과 "내 pod로 들어오는 요청"은 Envoy 안에서 적용되는 것이 완전히 다르다 — filter chain, trafficDirection, mTLS를 거는 위치(나갈 땐 client측 origination, 들어올 땐 server측 termination), AuthorizationPolicy 평가 위치, telemetry 방향. 입구에서부터 물리적으로 다른 listener로 갈라놓아야 이 둘을 섞지 않고 정책을 건다. 즉 15001/15006 분리는 방향별 정책 적용의 전제다(정책 평가 지점 자체는 AuthorizationPolicy 멘탈모델).

공식 디버깅 모델로도 모든 sidecar에는 outbound용 virtualOutbound(0.0.0.0:15001)와 inbound용 virtualInbound(0.0.0.0:15006) listener가 존재한다. virtualOutbound(15001)는 capture된 요청을 original destination에 맞는 virtual listener로 넘긴다 — 예컨대 원래 9080으로 향했던 outbound HTTP는 15001로 capture된 뒤 Envoy 내부 0.0.0.0:9080 virtual listener로 handoff된다. 이후로 이 명칭(virtualOutbound/virtualInbound)을 일관되게 쓴다.

✓ 숫자 대신 의미로
15001 = app이 밖으로 나갈 때 처음 빨려 들어가는 Envoy 입구 (outbound)
15006 = 밖에서 app으로 들어올 때 처음 빨려 들어가는 Envoy 입구 (inbound)

잡지 않는 포트들 (15008 / 15020 / 15021 / 15090)

캡처가 "app 트래픽만" 잡아야 하므로, Envoy/Istio 자체 인프라 포트로 향하는 트래픽은 redirect하지 않고 RETURN(=원래 흐름대로 통과)한다. 이걸 안 빼면 health probe·metrics scrape가 Envoy 입구로 잘못 빨려 들어가 깨진다.

포트 역할 redirection
15001 outbound capture (virtualOutbound listener) OUTPUT → REDIRECT 대상
15006 inbound capture (virtualInbound listener) PREROUTING → REDIRECT 대상
15008 HBONE/tunnel inbound tunnel port INBOUND에서 RETURN
15020 pilot-agent: merged Prometheus metrics + health probe rewrite INBOUND에서 RETURN
15021 sidecar status / readiness probe 포트(/healthz/ready) — kubelet이 proxy readiness를 probe해 'Envoy ready ⟺ pod ready'가 되게 함 INBOUND에서 RETURN
15090 Envoy Prometheus telemetry(merged metrics) 포트 INBOUND에서 RETURN

pilot-agent istio-iptables 플래그에서도 --envoy-port/-p 기본값이 15001, --inbound-capture-port/-z 기본값이 15006으로 명시된다.


02. 누가 rule을 거나 — pilot-agent istio-iptables

rule을 거는 주체는 app 컨테이너도, istiod도 아니다. pod 시작 시 실행되는 pilot-agent istio-iptables(init container 또는 Istio CNI plugin)가 pod의 network namespace 안에서 rule을 programming한다. istiod는 Envoy의 설정(xDS)을 주지만, 트래픽을 Envoy로 밀어넣는 커널 rule은 pod 로컬에서 이 도구가 박는다 — 둘은 다른 layer다.

소스 흐름

pilot-agent istio-iptables
  ↓
Config 로드
  ↓
NativeNftables 여부 판단
  ↓  (false면 기본 iptables 경로)
ProgramIptables
  ↓
capture.NewIptablesConfigurator(...)
  ↓
IptablesConfigurator.Run()
  ↓
IptablesRuleBuilder.AppendRule(...)        // rule 한 줄씩 누적
  ↓
BuildV4Restore()                           // *nat / rule / COMMIT 형태 input 생성
  ↓
iptables-restore --noflush 실행            // 한 번에 atomic 적용

여기서 왜 이 구조인가가 핵심이다. rule을 iptables 명령 하나하나로 실행하지 않는다. Istio는 rule들을 메모리에서 다 누적해(AppendRule) iptables-restore 포맷(*nat ... COMMIT) 한 덩어리를 만든 뒤 iptables-restore --noflush한 번에 atomic하게 적용한다. 한 줄씩 박으면 중간 상태(rule이 절반만 들어간 순간)에 트래픽이 흘러 일부만 capture되거나 누락되는 partial state가 생긴다. atomic 적용은 그 창을 없앤다. --noflush는 "기존 rule을 비우지 말고 추가만"이라는 뜻으로, 호스트/CNI가 이미 박아둔 rule을 보존한다.

소스에서 NativeNftables가 true면 ProgramNftables 경로를, 아니면 기본 ProgramIptables 경로를 탄다. --envoy-port/-p, --inbound-capture-port/-z, --istio-service-cidr/-i, --istio-inbound-ports/-b, --network-namespace, --native-nftables 같은 플래그를 받는다.

istio-init vs Istio CNI — 박는 주체·시점·권한의 차이

결과로 박히는 *nat rule은 같다. 다르게 하는 건 누가, 어느 시점에, 어떤 권한으로 박느냐다.

istio-init (init container 방식)
  = pod마다 NET_ADMIN/NET_RAW 권한을 가진 init container가 떠서
    그 pod의 netns 안에 iptables rule을 박고 종료

Istio CNI plugin 방식
  = CNI chain에 Istio CNI가 끼어들어 pod network 셋업 시점에 rule을 박음
  = pod에 NET_ADMIN을 주지 않아도 됨 (보안상 권장)

CNI 방식은 app pod의 권한 표면을 줄인다. 모든 app pod에 NET_ADMIN을 주는 건 PodSecurity 관점에서 큰 공격 면이므로, production에서는 CNI 방식이 선호된다. 캡처 동작만 같다고 둘을 같다고 보면 안 된다 — 권한 측면이 다르다.


03. REDIRECT mode 핵심 체인 — 그림에서 rule로

00·01에서 세운 그림을 그대로 rule로 옮기면 된다. REDIRECT mode는 4개의 custom chain + 2개 진입점으로 구성된다.

진입점:
  PREROUTING → ISTIO_INBOUND    (밖에서 들어오는 패킷)
  OUTPUT     → ISTIO_OUTPUT     (app이 밖으로 내보내는 패킷)

redirect 종착 체인:
  ISTIO_REDIRECT     → REDIRECT --to-ports 15001   (outbound capture)
  ISTIO_IN_REDIRECT  → REDIRECT --to-ports 15006   (inbound capture)

소스상 ISTIO_REDIRECTcfg.cfg.ProxyPort(=15001)로, ISTIO_IN_REDIRECTcfg.cfg.InboundCapturePort(=15006)로 redirect한다. 진입점 chain(ISTIO_INBOUND/ISTIO_OUTPUT)은 "잡을지 말지 분기"하고, 종착 chain(ISTIO_*REDIRECT)은 "실제로 Envoy 포트로 보낸다". 분기와 실행을 chain으로 쪼개둔 덕에 예외(RETURN)를 진입점에서 깔끔히 처리할 수 있다.

inbound 분기 (ISTIO_INBOUND)

PREROUTING -p tcp -j ISTIO_INBOUND

if InboundPortsInclude == "*":
  exclude port(15008/15020/15021/15090 등) → RETURN
  나머지 inbound TCP → ISTIO_IN_REDIRECT
else:
  명시된 port 목록만 → ISTIO_IN_REDIRECT

outbound 분기 (ISTIO_OUTPUT) — 불변식이 사는 곳

OUTPUT -j ISTIO_OUTPUT

ISTIO_OUTPUT:
  outbound exclude port면          → RETURN
  loopback/self-call 특수 처리      → RETURN
  proxy UID/GID(1337)가 만든 트래픽 → RETURN   ★ 무한 루프 방지
  outbound include CIDR가 "*"면     → ISTIO_REDIRECT
⚠ proxy UID/GID 1337 RETURN — 무한 루프 방지의 핵심

Envoy가 upstream으로 새 connection을 맺으면 그 패킷도 OUTPUT chain을 다시 통과한다. 만약 이 패킷까지 15001로 redirect하면 Envoy → 15001 → Envoy → 15001 ... 무한 루프에 빠진다. 그래서 Envoy(istio-proxy)는 UID/GID 1337로 실행되고, -m owner --uid-owner 1337 / --gid-owner 1337에 걸리는 트래픽은 RETURN되어 재-capture되지 않는다. "app이 만든 트래픽만 잡고, proxy가 만든 트래픽은 통과" — 이것이 redirection의 가장 중요한 불변식이다. 캡처가 "방향"으로 안 갈리고 "누가 만들었나(UID)"로 갈린다는 점이 핵심: 같은 OUTPUT hook을 app도 Envoy도 지나지만, UID로 둘을 구분해 app 것만 redirect한다.

-d 127.0.0.1/32 -j RETURN 같은 localhost 예외도 self-call(같은 pod 내 app ↔ proxy loopback)이 잘못 redirect되는 것을 막는다.


04. 적용된 실제 rule — *nat 블록과 dry-run

위 그림을 실제로 박으면 이렇게 생긴다. 아래는 대표적인 REDIRECT mode *nat 블록이다. 실제 출력은 Istio 버전, CNI 사용 여부, DNS capture, include/exclude annotation, IPv6, TPROXY 여부에 따라 달라진다.

*nat
-N ISTIO_REDIRECT
-N ISTIO_IN_REDIRECT
-N ISTIO_INBOUND
-N ISTIO_OUTPUT

# inbound packet 진입점
-A PREROUTING -p tcp -j ISTIO_INBOUND

# outbound packet 진입점
-A OUTPUT -j ISTIO_OUTPUT

# outbound는 Envoy 15001로
-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001

# inbound는 Envoy 15006으로
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006

# tunnel/status/metrics 등 제외 (RETURN)
-A ISTIO_INBOUND -p tcp --dport 15008 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15020 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15021 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15090 -j RETURN

# 나머지 inbound app traffic capture
-A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT

# Envoy 자신이 만든 traffic은 다시 capture하지 않음 (무한 루프 방지)
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN

# localhost/self-call 예외
-A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN

# 나머지 outbound capture
-A ISTIO_OUTPUT -j ISTIO_REDIRECT

COMMIT

BuildV4Restore()가 바로 이 *nat / rule line / COMMIT 형태의 restore input을 구성하고, 실행 경로는 iptables-restore --noflush로 한 번에 적용한다. (※ inbound capture는 wildcard(-b '*')면 위처럼 모든 app port를 잡고, 특정 port 목록이면 -A ISTIO_INBOUND -p tcp --dport 9080 -j ISTIO_IN_REDIRECT처럼 해당 port만 redirect한다.) 단 wildcard inbound라도 status/metrics 포트 외에 loopback이나 특정 source에 대한 RETURN 예외가 추가로 끼어들 수 있으므로, 위 블록을 "모든 inbound가 무조건 15006으로 간다"로 과단순화하면 안 된다.

적용 전에 생성될 rule 미리 보기 — --dry-run

실제 rule을 적용하지 않고 어떤 rule이 만들어질지 확인하려면 --dry-run을 쓴다. 캡처 트러블슈팅의 첫 수: "내가 기대한 rule이 정말 생성되는가"를 부작용 없이 본다.

kubectl exec -n <ns> <pod-name> -c istio-proxy -- \
  pilot-agent istio-iptables \
    --dry-run \
    -p 15001 \
    -z 15006 \
    -u 1337 \
    -g 1337 \
    -m REDIRECT \
    -b '*' \
    -d '15090,15021,15020' \
    -i '*'

플래그 의미:

-p 15001  = outbound redirect target (--envoy-port)
-z 15006  = inbound redirect target (--inbound-capture-port)
-u 1337   = 이 UID의 traffic은 redirect 제외 (--proxy-uid)
-g 1337   = 이 GID의 traffic은 redirect 제외 (--proxy-gid)
-m REDIRECT = inbound capture mode (REDIRECT | TPROXY | NONE)
-b '*'    = 모든 inbound port capture (--istio-inbound-ports)
-d ...    = inbound capture 제외 port (--istio-inbound-ports-exclude)
-i '*'    = 모든 outbound IP range capture (--istio-service-cidr)

기대 출력은 04 첫머리의 *nat ... COMMIT 블록과 동형이어야 한다. dry-run 출력에 --to-ports 15001/15006, --uid-owner 1337 -j RETURN, exclude 포트 RETURN 라인이 다 보이면 정상이다.

✓ `-m`은 inbound 전용 mode

-minbound capture mode다. MeshConfig 기준 outbound는 항상 iptables REDIRECT를 쓰고, inbound만 기본 REDIRECT / 고급 TPROXY / redirection 없음 NONE 중 고른다. TPROXY는 original source IP를 보존해야 할 때 쓰지만 운영 복잡도가 올라간다.


05. nftables 경로 — 같은 그림, 다른 표현

현재 sidecar mode의 기본은 여전히 iptables 경로다. 다만 --native-nftables를 켜면 Istio가 native nftables rule 경로(ProgramNftables)를 쓴다. 구조는 iptables와 거의 동형이다 — outbound는 redirect to :15001, inbound는 redirect to :15006. 같은 멘탈모델(두 입구 + 1337 RETURN)이 다른 문법으로 표현될 뿐이다.

table inet istio_proxy_nat {
  chain prerouting {
    type nat hook prerouting priority dstnat; policy accept;
    meta l4proto tcp counter jump ISTIO_INBOUND
  }

  chain output {
    type nat hook output priority dstnat; policy accept;
    counter jump ISTIO_OUTPUT
  }

  chain ISTIO_REDIRECT {
    meta l4proto tcp counter redirect to :15001
  }

  chain ISTIO_IN_REDIRECT {
    meta l4proto tcp counter redirect to :15006
  }

  chain ISTIO_INBOUND {
    meta l4proto tcp tcp dport 15008 counter return
    meta l4proto tcp counter jump ISTIO_IN_REDIRECT
  }

  chain ISTIO_OUTPUT {
    skuid 1337 counter return     # proxy UID RETURN
    skgid 1337 counter return     # proxy GID RETURN
    counter jump ISTIO_REDIRECT
  }
}

hook prerouting/hook output은 iptables의 PREROUTING/OUTPUT 진입점에, skuid 1337/skgid 1337-m owner --uid-owner/--gid-owner 1337에 대응한다. native nftables 경로는 성공하면 (TPROXY 사용 시) TPROXY route도 함께 설정한다.

⚠ 함정 — iptables backend(iptables-nft) vs native nftables를 혼동하지 말 것

둘 다 결국 커널 netfilter를 쓰지만 의미가 다르다.

iptables 경로
  = Istio가 iptables/iptables-restore 명령을 호출함.
    단, OS의 iptables binary가 내부적으로 iptables-nft backend일 수 있음
    (요즘 distro 기본). 이 경우에도 "Istio 입장에선 iptables 경로"임.

native nftables 경로
  = Istio가 nftables rule builder 경로(ProgramNftables)를 사용함.
    --native-nftables가 켜진 경우에만.

즉 호스트가 nftables를 쓴다고 Istio가 native nftables 경로를 쓰는 게 아니다. 운영자가 보는 명령(iptables-save vs nft list ruleset)과 rule 표현이 달라지므로, 진단 전에 어느 경로인지 먼저 확인해야 한다.


06. 떴는지 확인 — 검증 명령

실제 pod에서 rule이 어떻게 박혔는지 확인하는 방법이다. 핵심은 app pod의 netns를 정확히 들여다보는 것이다(iptables rule은 pod netns 단위로 존재).

iptables rule 확인

# debug container(netshoot)를 app container의 netns에 붙여서 확인
kubectl debug -n <ns> -it pod/<pod-name> \
  --image=nicolaka/netshoot \
  --target=<app-container> \
  --profile=netadmin \
  -- iptables-save -t nat

# 또는 istio-proxy 컨테이너에 도구가 있으면 직접
kubectl exec -n <ns> <pod-name> -c istio-proxy -- iptables-save -t nat

iptables-save -t nat 출력에서 capture가 적용됐다면 다음 핵심 라인들이 보여야 한다.

-A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001
-A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006
-A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN
-A ISTIO_INBOUND -p tcp --dport 15020 -j RETURN

이 라인들이 안 보이면 ① capture 자체가 미적용(CNI/init container 실패)이거나 ② 지금 보는 것이 app pod의 netns가 아닌(=--target 누락) 다른 netns라는 뜻이다. kubectl debug로 iptables를 보려면 debug 컨테이너에 NET_ADMIN이 필요할 수 있으므로 --profile=netadmin을 붙여야 할 수 있다.

nftables ruleset 확인

kubectl debug -n <ns> -it pod/<pod-name> \
  --image=nicolaka/netshoot \
  --target=<app-container> \
  -- nft list ruleset
✓ `--target`의 의미

kubectl debug ... --target=<app-container>는 debug 컨테이너를 app 컨테이너와 같은 network namespace에 join시킨다. iptables rule은 pod netns 단위로 존재하므로, target을 지정해야 app pod가 실제로 보는 rule을 관찰할 수 있다. target 없이 띄우면 다른 netns를 보게 되어 rule이 안 보일 수 있다.

캡처가 실제로 동작하는지(트래픽이 Envoy를 통과하는지)는 Envoy 설정 쪽에서 교차 검증한다 — istioctl proxy-config listener <pod>virtualOutbound(0.0.0.0:15001) / virtualInbound(0.0.0.0:15006) listener 존재를 확인하면 된다(→ xDS 계층과 진단). rule(커널 측)과 listener(Envoy 측) 둘 다 있어야 캡처가 진짜 닫힌 고리로 동작한다.


07. packet flow 다이어그램

outbound flow

app: connect svc:9080nat/OUTPUTISTIO_OUTPUTuid/gid 1337?localhost?RETURN그대로 통과ISTIO_REDIRECTREDIRECT :15001Envoy virtualOutbound→ virtual listener 9080예: RETURN아니오
그림 1. outbound 캡처: app이 svc:9080 연결 → nat/OUTPUT의 ISTIO_OUTPUT에서 uid/gid 1337(Envoy 자신)·localhost면 RETURN(통과), 아니면 ISTIO_REDIRECT→:15001로 redirect → Envoy가 virtual listener 9080으로 handoff.

inbound flow

remote peer → podIP:9080nat/PREROUTINGISTIO_INBOUNDexclude port?15008/20/21/90RETURNredirect 안 함ISTIO_IN_REDIRECTREDIRECT :15006virtualInbound 15006mTLS종료/RBAC/telemetry → app예: RETURN아니오
그림 2. inbound 캡처: 외부→podIP:9080 → nat/PREROUTING의 ISTIO_INBOUND에서 제외 포트(15008/15020/15021/15090)면 RETURN, 아니면 :15006으로 redirect → virtualInbound가 mTLS 종료·RBAC·telemetry 후 app(:9080) 전달.

캡처가 끝나는 지점은 Envoy 입구(15001/15006)까지다. 그 다음 listener→route(RDS)→cluster(CDS)→endpoint(EDS)로 이어지는 처리는 별도 xDS 영역이다(→ xDS 계층과 진단).


핵심 정리

What you might be missing