IT/Network&Security

[k8s] Tracing the path of network traffic

Hayley Shim 2023. 10. 29. 00:35

쿠버네티스 내의 트래픽 흐름이 궁금하여 찾아보다 k8s에서 네트워크 트래픽의 경로를 tracing하는 article을 참고하여 정리한 글입니다. 해당 article에 관련 내용이 상세하게 설명되어있어 k8s 내의 네트워크 트래픽 흐름을 이해하는데 도움이 되시길 바랍니다.

Pod에서 Linux network namespace 동작 방식

1. Pod는 Node에서 자체 network namespace를 가져옵니다.

2. Pod에 IP 주소가 할당되고 두 컨테이너 간에 port가 공유됩니다.

3. 두 컨테이너는 동일한 네트워킹 namespace를 공유하며 localhost에서 서로를 볼 수 있습니다.

물리적 네트워크 인터페이스는 root network namespace를 보유합니다.

Linux network namespace를 사용하여 격리된 네트워크를 만들 수 있습니다. 각 네트워크는 독립적이며 별도로 구성하지 않는 한 다른 네트워크와 통신하지 않습니다.

network namespace는 ip-netns management tool로 관리할 수 있으며 ip netns list를 사용하여 호스트의 namespace를 나열할 수 있습니다.

$ ip netns list
cni-0f226515-e28b-df13-9f16-dd79456825ac (id: 3)
cni-4e4dfaac-89a6-2034-6098-dd8b2ee51dcd (id: 4)
cni-7e94f0cc-9ee8-6a46-178a-55c73ce58f2e (id: 2)
cni-7619c818-5b66-5d45-91c1-1c516f559291 (id: 1)
cni-3004ec2c-9ac2-2928-b556-82c7fb37a4d8 (id: 0)

Pod를 생성하고 해당 Pod가 Node에 할당되면 CNI는 다음을 수행합니다.

  1. IP 주소를 할당합니다.
  2. 컨테이너를 네트워크에 연결합니다.

Pod를 생성할 때 먼저 컨테이너 런타임은 컨테이너에 대한 network namespace를 생성합니다.

그런 다음 CNI가 IP 주소를 할당합니다.

그리고 마지막으로 CNI는 컨테이너를 나머지 네트워크에 연결합니다.

lsns 호스트에서 사용 가능한 모든 namespace를 나열하는 명령입니다.

$ lsns -t net
        NS TYPE NPROCS   PID USER     NETNSID NSFS                           COMMAND
4026531992 net     171     1 root  unassigned /run/docker/netns/default      /sbin/init noembed norestore
4026532286 net       2  4808 65535          0 /run/docker/netns/56c020051c3b /pause
4026532414 net       5  5489 65535          1 /run/docker/netns/7db647b9b187 /pause

Pause container는 Pod에 network namespace 생성

Node의 모든 프로세스를 나열하고 Nginx 컨테이너를 찾을 수 있는지 확인하겠습니다.

$ lsns
        NS TYPE   NPROCS   PID USER            COMMAND
# truncated output
4026532414 net         5  5489 65535           /pause
4026532513 mnt         1  5599 root            sleep 1d
4026532514 uts         1  5599 root            sleep 1d
4026532515 pid         1  5599 root            sleep 1d
4026532516 mnt         3  5777 root            nginx: master process nginx -g daemon off;
4026532517 uts         3  5777 root            nginx: master process nginx -g daemon off;
4026532518 pid         3  5777 root            nginx: master process nginx -g daemon off;

컨테이너는 mount(mnt), Unix time-sharing(uts) 및 PID(pid) namespace에 나열되지만 networking namespace(net)에는 나열되지 않습니다.

lsns는 각 프로세스에 대해 가장 낮은 PID만 표시하지만 프로세스 ID를 기반으로 추가로 필터링 할 수 있습니다.

$ sudo lsns -p 5777
       NS TYPE   NPROCS   PID USER  COMMAND
4026531835 cgroup    178     1 root  /sbin/init noembed norestore
4026531837 user      178     1 root  /sbin/init noembed norestore
4026532411 ipc         5  5489 65535 /pause
4026532414 net         5  5489 65535 /pause
4026532516 mnt         3  5777 root  nginx: master process nginx -g daemon off;
4026532517 uts         3  5777 root  nginx: master process nginx -g daemon off;
4026532518 pid         3  5777 root  nginx: master process nginx -g daemon off;

pause 는 다시 처리하고 이번에는 network namespace를 잡고 있습니다.

클러스터의 모든 Pod에는 pause라는 백그라운드에서 실행되는 추가 숨겨진 컨테이너가 있습니다.

Node에서 실행 중인 컨테이너를 나열하고 pause 컨테이너를 가져오는 경우:

Pod를 생성할 때 컨테이너 런타임은 sleep 컨테이너가 있는 network namespace를 생성합니다.

Pod의 다른 모든 컨테이너는 이 컨테이너에서 생성한 기존 network namespace에 join합니다.

이 시점에서 CNI는 IP 주소를 할당하고 컨테이너를 네트워크에 연결합니다.

컨테이너가 시작되자마자 CNI는 다음을 수행합니다.

  1. Busybox 컨테이너가 이전 network namespace에 참여하도록 합니다.
  2. IP 주소를 할당합니다.
  3. 컨테이너를 네트워크에 연결합니다.

Pod에 단일 IP 주소 할당

Pod network namespace 내부에 인터페이스가 생성되고 IP 주소가 할당됩니다.

$ kubectl get pod multi-container-pod -o jsonpath={.status.podIP}
10.244.4.40

클러스터에서 Pod 간 트래픽 검사

  1. Pod 트래픽은 동일한 Node의 Pod로 향합니다.
  2. Pod 트래픽은 다른 Node에 있는 Pod로 향합니다.

pod network namespace는 ethernet bridge에 연결

이 bridge를 통해 virtual pairs 간에 트래픽이 흐르고 공통 root namespace를 통과할 수 있습니다.

이더넷 브리지는 OSI 네트워킹 모델의 레이어 2에 있습니다.

브리지를 서로 다른 네임스페이스 및 인터페이스에서 연결을 수락하는 가상 스위치로 생각할 수 있습니다.

이더넷 브리지를 사용하면 동일한 노드에서 사용 가능한 여러 네트워크를 연결할 수 있습니다.

따라서 이 설정을 사용하고 두 인터페이스, 즉 pod 네임스페이스의 veth를 노드의 다른 pod veth로 연결할 수 있습니다.

동일한 node에서 pod 간 트래픽 추적

동일한 노드에 두 개의 Pod가 있고 Pod-A가 Pod-B에 메시지를 보내려고 한다고 가정해 보겠습니다.

목적지가 네임스페이스의 컨테이너 중 하나가 아니기 때문에 Pod-A는 패킷을 기본 인터페이스 eth0으로 보냅니다. 이 인터페이스는 veth pair의 한쪽 끝에 연결되어 터널 역할을 합니다. 이를 통해 패킷이 노드의 root namespace로 전달됩니다.

가상 스위치 역할을 하는 이더넷 브리지는 대상 Pod IP(Pod-B)를 MAC 주소로 확인해야 합니다.

ARP 프로토콜이 구출됩니다. 프레임이 브리지에 도달하면 연결된 모든 장치에 ARP 브로드캐스트가 전송됩니다.

Pod-B를 연결하는 인터페이스의 MAC 주소로 응답을 받은 후 이 정보를 브리지 ARP 캐시(lookup table)에 저장합니다.

IP 및 MAC 주소 매핑이 저장되면 브리지가 테이블을 조회하고 패킷을 올바른 끝점으로 전달합니다. 패킷은 root namespace의 Pod-B veth에 도달하고 거기에서 Pod-B 네임스페이스 내의 eth0 인터페이스에 빠르게 도달합니다.

이로써 Pod-A와 Pod-B 간의 통신이 성공적으로 이루어졌습니다.

다른 노드에서 포드 간 통신 추적

서로 다른 노드 간에 통신해야 하는 팟(Pod)의 경우 통신에 추가 hop이 필요합니다.

처음 몇 단계는 패킷이 root namespace에 도착하고 Pod-B로 전송되어야 하는 시점까지 동일하게 유지됩니다.

목적지 IP가 로컬 네트워크에 없으면 패킷은 해당 노드의 default gateway로 전달됩니다. 노드의 출구 또는 default gateway는 일반적으로 노드를 네트워크에 연결하는 물리적 인터페이스인 eth0 인터페이스에 있습니다.

이번에는 출발지와 목적지 IP가 서로 다른 네트워크에 있기 때문에 ARP 해석이 일어나지 않습니다.

검사는 Bitwise 연산을 사용하여 수행됩니다.

목적지 IP가 현재 네트워크에 없으면 노드의 default gateway로 전달됩니다.

Bitwise 연산이 이루어진 후 ARP는 default gateway의 MAC 주소에 대한 조회 테이블을 확인합니다.

항목이 있으면 즉시 패킷을 전달합니다.

그렇지 않으면 게이트웨이의 MAC 주소를 결정하기 위해 먼저 브로드캐스트를 수행합니다.

이제 패킷이 다른 노드의 기본 인터페이스로 라우팅됩니다. 이를 노드 B라고 해봅시다.

역순으로. 패킷은 이제 Node-B의 root namespace에 있으며 다른 ARP 확인이 수행되는 브리지에 도달합니다.

Pod-B를 연결하는 인터페이스의 MAC 주소로 응답을 받습니다.

이번에는 브리지가 Pod-B veth 장치를 통해 프레임을 전달하고 자체 네임스페이스에서 Pod-B에 도달합니다.

이제 Pod 간의트래픽 흐름 방식에 익숙해졌으므로 CNI가 위의 항목을 생성하는 방법을 살펴보겠습니다.

컨테이너 네트워크 인터페이스 — CNI

CNI는 Kubernetes 네트워크 요구 사항 중 일부를 해결하기 위해 네트워킹 플러그인이 따라야 하는 일련의 규칙으로 생각할 수 있습니다.

그러나 이것은 Kubernetes나 특정 네트워크 플러그인에만 한정되지 않습니다.

모든 CNI를 사용할 수 있습니다.

모두 동일한 CNI 표준을 구현합니다.
CNI가 없으면 수동으로 다음을 수행해야 합니다.

  • 인터페이스를 만듭니다.
  • veth pair를 만듭니다.
  • 네임스페이스 네트워킹을 설정합니다.
  • 고정 경로를 설정합니다.
  • 이더넷 브리지를 구성합니다.
  • IP 주소를 할당합니다.
  • NAT 규칙을 만듭니다.

그리고 과도한 수작업을 필요로 하는 많은 다른 것들을 해야합니다. Pod를 삭제하거나 다시 시작해야 할 때 위의 모든 항목을 삭제하거나 조정하는 것 등 많은 작업이 있습니다.

CNI는 다음 네 가지 작업을 지원해야 합니다.

  • ADD — 네트워크에 컨테이너를 추가합니다.
  • DEL — 네트워크에서 컨테이너를 삭제합니다.
  • CHECK — 컨테이너의 네트워크에 문제가 있으면 오류를 반환합니다.
  • VERSION —플러그인의 버전을 표시합니다.

포드가 특정 노드에 할당되면 kubelet 자체가 네트워킹을 초기화하지 않습니다. 대신 이 작업을 CNI로 오프로드합니다. 그러나 구성을 지정하고 JSON 형식으로 CNI 플러그인에 보냅니다. 노드에서/etc/cni/net.d로 이동하고 다음을 사용하여 현재 CNI 구성 파일을 확인할 수 있습니다.

각 CNI 플러그인은 네트워크 설정에 대해 서로 다른 유형의 구성을 사용합니다.

를 들어 Calico는 BGP 라우팅 프로토콜과 쌍을 이루는 레이어 3 네트워킹을 사용하여 포드를 연결합니다.

Cilium은 레이어 3~7에서 eBPF로 오버레이 네트워크를 구성합니다.

Calico와 함께 Cilium은 트래픽을 제한하는 네트워크 정책 설정을 지원합니다.

CNI에는 주로 두 그룹이 있습니다.

첫 번째 그룹에서는 기본 네트워크 설정(flat network라고도 함)을 사용하고 클러스터의 IP Pool에서 포드에 IP 주소를 할당하는 CNI를 찾을 수 있습니다.

사용 가능한 모든 IP 주소를 빠르게 소진할 수 있으므로 이는 부담이 될 수 있습니다. 대신 오버레이 네트워킹을 사용하는 또 다른 방법이 있습니다.
간단히 말해서 오버레이 네트워크는 기본(underlay) 네트워크 위에 있는 보조 네트워크입니다.

오버레이 네트워크는 다른 노드의 포드로 향하는 언더레이 네트워크에서 발생하는 모든 패킷을 캡슐화하여 작동합니다.

오버레이 네트워크에 널리 사용되는 기술은 L3 네트워크를 통해 L2 도메인을 터널링할 수 있는 VXLAN입니다.

So which one is better?

정답은 없으며 일반적으로 요구 사항에 따라 결정됩니다.

  • 수만 개의 노드가 있는 대규모 클러스터를 구축하고 있나요? 오버레이가 더 나을 수도 있습니다.
  • 더 간단한 설정과 중첩된 네트워크에서 손실되지 않고 네트워크 트래픽을 검사할 수 있는 기능을 중요하게 생각하나요? 평면 네트워크가 적합합니다.

CNI에 대해 논의했으므로 이제 Pod-to-Service 통신이 작동하는 방식을 살펴보겠습니다.

Pod to Service 트래픽 검사

Kubernetes 환경에서 포드의 동적 특성으로 인해 할당된 IP 주소는 고정되지 않습니다. 그것들은 임시적이며 포드가 생성되거나 삭제될 때마다 변경됩니다. 이 서비스는 이 문제를 해결하고 포드 세트에 연결하기 위한 안정적인 메커니즘을 제공합니다.

기본적으로 Kubernetes에서 서비스를 생성하면 가상 IP가 예약되어 할당됩니다.

여기에서 selector를 사용하여 서비스를 target pod에 연결합니다.

  • 포드가 삭제되고 새 포드가 추가되면 어떻게 되나요? 서비스의 virtual IP는 고정되어 변경되지 않습니다.

그러나 트래픽은 개입 없이 새로 생성된 포드에 도달합니다.

즉, Kubernetes의 서비스는 로드 밸런서와 유사합니다.

어떻게 작동하나요?

Netfilter 및 Iptables로 트래픽 Intercepting 및rewriting

Kubernetes의 서비스는 두 가지 Linux 커널 구성 요소를 기반으로 합니다.

  1. Netfilter
  2. iptables

Netfilter는 패킷 필터링을 구성하고, NAT 또는 포트 변환 규칙을 생성하고, 네트워크의 트래픽 흐름을 관리할 수 있는 프레임워크입니다.

또한 서비스에 도달하기 위해 원치 않는 연결(unsolicited connections)을 보호하고 방지합니다.

반면에 Iptables는 Linux 커널 방화벽의 IP 패킷 필터 규칙을 구성할 수 있는 사용자 공간 유틸리티 프로그램입니다.

iptables는 다른 Netfilter 모듈로 구현됩니다.

iptables CLI를 사용하여 필터링 규칙을 즉시 변경하고 netfilters hooking points에 insert합니다.

필터는 네트워크 트래픽 패킷을 처리하기 위한 체인이 포함된 여러 테이블로 구성됩니다.

프로토콜마다 다른 커널 모듈과 프로그램이 사용됩니다.

iptables가 언급되면 일반적으로 IPv4용이라는 의미입니다. IPv6 규칙의 경우 CLI를 ip6tables라고 합니다.

iptables에는 5가지 유형의 체인이 있으며 각 체인은 Netfilter의 hook에 직접 매핑됩니다.

iptables의 관점에서 보면 다음과 같습니다.

  • PRE_ROUTING
  • INPUT
  • FORWARD
  • OUTPUT
  • POST_ROUTING

그리고 그에 따라 Netfilter hook에 매핑됩니다.

  • NF_IP_PRE_ROUTING
  • NF_IP_LOCAL_IN
  • NF_IP_FORWARD
  • NF_IP_LOCAL_OUT
  • NF_IP_POST_ROUTING

패킷이 도착하면 어떤 단계인지에 따라 특정 iptables 필터링을 적용하는 Netfilter hook를 ‘트리거’합니다.

이것이 우리가 Kubernetes를 사용하는 이유입니다. 위의 모든 것은 서비스 사용을 통해 추상화되며 간단한 YAML 정의가 이러한 규칙을 자동으로 설정합니다.
iptables 규칙을 확인하는 데 관심이 있는 경우 노드에 연결하고 다음을 실행할 수 있습니다.

우리는 Pod가 동일하고 다른 노드에 있을 때 Pod-to-Pod 통신이 어떻게 발생하는지 설명했습니다.

Pod-to-Service에서 통신의 전반부는 동일하게 유지됩니다.

요청이 Pod-A에서 시작되고 Pod-B에 도달하려고 할 때(이 경우 서비스 ‘behind’) 전송 중간에 추가 변경이 발생합니다.

원래 요청은 Pod-A 네임스페이스의eth0 인터페이스를 통해 종료됩니다.

거기에서 veth pair를 거쳐 root namespace ethernet bridge에 도달합니다.

Bridge에 도착하면 패킷은 default gateway를 통해 즉시 전달됩니다.

Pod-to-Pod 섹션에서와 같이 호스트는 비트 단위로 비교하고 서비스의 vIP는 노드의 CIDR의 일부가 아니기 때문에 패킷은 default gateway를 통해 즉시 전달됩니다.

lookup table에 이미 존재하지 않는 경우 기본 게이트웨이의 MAC 주소를 찾기 위해 동일한 ARP 확인이 발생합니다.

해당 패킷이 노드의 라우팅 프로세스를 거치기 직전에 NF_IP_PRE_ROUTING Netfilter hook가 트리거되고 iptables 규칙이 적용됩니다. 규칙은 DNAT 변경을 수행하고 Pod의 A 패킷 대상 IP를 다시 작성합니다.

이전 서비스 vIP 대상은 Pod의 B IP 주소로 다시 작성됩니다.

거기에서 라우팅은 Pod-to-Pod를 직접 통신하는 것과 동일합니다.

그러나 이 모든 통신 사이에 또 다른 세 번째 기능이 사용됩니다.

이 기능을 conntrack 또는 연결 추적(connection tracking)이라고 합니다.

Conntrack은 패킷을 연결에 연결하고 Pod-B에서 응답을 다시 보낼 때 원본을 추적합니다.

NAT는 작동하기 위해 conntrack에 크게 의존합니다.

연결 추적(connection tracking)이 없으면 응답이 포함된 패킷을 어디로 다시 보낼지 모릅니다.

conntrack을 사용하면 동일한 소스 또는 대상 NAT 변경으로 패킷의 반환 경로를 쉽게 설정할 수 있습니다.

나머지 절반은 이제 역순입니다.

Pod-B는 요청을 수신하고 처리했으며 이제 Pod-A에 데이터를 다시 보냅니다.

서비스로부터의 응답 검사

이제 Pod-B는 IP 주소를 소스로 설정하고 Pod의 A IP 주소를 대상으로 설정하여 응답을 보냅니다.

패킷이 Pod-A가 있는 노드의 인터페이스에 도달하면 또 다른 NAT가 발생합니다.

이번에는 conntrack을 사용하여 소스 IP 주소가 변경되고 iptables 규칙이 SNAT를 수행하고 Pod의 B 소스 IP를 원래 서비스의 vIP로 swap합니다.

Pod-A의 경우 수신 응답이 Pod-B가 아닌 서비스에서 발생하는 것처럼 보입니다.

나머지는 동일합니다. SNAT가 완료되면 패킷은 root namespace의 ethernet bridge에 도달하고 veth pair을 통해 Pod-A로 전달됩니다.

[원문] Tracing the path of network traffic in Kubernetes

 

Tracing the path of network traffic in Kubernetes

Learn how packets flow inside and outside a Kubernetes cluster. Starting from the initial web request and down to the container hosting the application

learnk8s.io

 

blog migration project

written in 2022.11.2

https://medium.com/techblog-hayleyshim/k8s-tracing-the-path-of-network-traffic-c76fb5df2440

 

 

'IT > Network&Security' 카테고리의 다른 글

[nw] BGP(Border Gateway Protocol)  (1) 2024.04.20
[k8s] CoreDNS  (0) 2023.10.29
[nw] VPN(AWS Client VPN, Site-to-Site VPN etc)  (0) 2023.10.28
[nw] HTTP/3 QUIC  (0) 2023.10.28
[nw] Traffic Monitoring using sflow/netflow  (0) 2023.10.28