TT Lab
요금제

한국어

시작하기
블로그

블로그

Cilium 읽기: 에이전트가 BPF 맵을 채우고, 커널 훅이 그 맵만 보고 판단하는 구조

상태: 초안 (2026-10-17), 일부 실측. 기준 판 cilium/cilium v1.20.1(커밋 7d68cfb394f2). 작은 클러스터가 바로 이 판(에이전트· operator-generic·hubble-relay v1.20.1, cilium-envoy 1.37 계열)을 돌리고 있어서, 실측은 2026-10-17 그 클러스터에서 읽기로만 했습니다(에이전트 파드 하나에 cilium-dbg·bpftool·hubble observe 읽기 명령, ConfigMap·정책 개수 읽기, relay 로그 읽기). DNS 패턴의 접속 결과는 기여를 준비하며 2026-10-16~17 사이에 잰 기록(PR 초안)을 인용합니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.

목차

1. 한 줄 요약

Cilium 을 한 줄로 줄이면 「노드마다 있는 사무실(에이전트)이 규칙표(BPF 맵)를 고쳐 쓰고, 커널 곳곳의 검문소(BPF 프로그램)는 규칙표만 보고 통과·차단·주소 변환을 결정한다」 입니다. 검문소는 사무실에 전화를 걸지 않습니다. 패킷 하나마다 사용자 공간으로 올라오면 느리기 때문에, 판단에 필요한 것은 전부 미리 맵에 적어 둡니다. 그래서 Cilium 코드를 읽을 때 가장 먼저 물어야 할 질문은 언제나 「이 정보는 누가 어느 맵에 쓰고, 어느 훅이 읽는가」 입니다.

구성요소 하는 일 어디서 도나
cilium-agent 쿠버네티스 상태를 받아 정책·서비스·identity 를 계산하고 BPF 맵에 쓴다. BPF 프로그램을 컴파일·적재한다. DNS 프록시와 Hubble 관찰기도 이 프로세스 안에 있다 노드마다(DaemonSet)
BPF 프로그램 (bpf/) 파드 veth·호스트 장치·터널 장치·소켓 cgroup 에 붙어 패킷과 connect() 를 처리 커널 안
cilium-operator CRD 등록, IPAM(모드에 따라), 남은 identity·CiliumEndpoint 정리, LB-IPAM 클러스터에 하나(리더 선출)
cilium-envoy HTTP 같은 L7 정책과 Ingress·Gateway 의 프록시. 에이전트에게 xDS 로 설정을 받는다 노드마다(별도 DaemonSet)
Hubble 에이전트 안 관찰기가 BPF 가 내는 이벤트를 흐름으로 해석해 링버퍼에 담고, relay 가 노드들의 흐름을 모은다 관찰기는 에이전트 안, relay 는 클러스터에 하나

2. 구조도

Cilium 구성요소와 BPF 프로그램이 붙는 자리 kube-apiserver Service·CNP·CRD 를 감시 cilium-operator 클러스터에 1개 · IPAM·CRD·GC 노드마다 (DaemonSet 두 개) cilium-agent — hive 셀 묶음 infra · controlplane · datapath 정책·셀렉터 캐시 로드밸런서 표 DNS 프록시 loader (clang) Hubble 관찰기 IPAM·ipcache cilium-envoy L7 프록시 (HTTP·gRPC) 에이전트에게 xDS 로 설정 받음 BPF 맵 (/sys/fs/bpf 에 고정) 쓰기 events 서비스 정책 ipcache conntrack 훅 자리 → 붙는 프로그램 (bpf/ 의 파일) 읽기 cgroup 루트(소켓) bpf_sock.c → cil_sock4_connect 등 파드 veth (lxc*) bpf_lxc.c → cil_from_container cilium_host·net bpf_host.c → cil_from/to_host cilium_vxlan bpf_overlay.c → cil_from/to_overlay 물리 NIC (tc) bpf_host.c → cil_from/to_netdev 물리 NIC (XDP) bpf_xdp.c → cil_xdp_entry (꺼짐) hubble-relay · hubble-ui (클러스터에 1개) 에이전트마다 :4244(TLS)로 흐름을 모은다
그림: Cilium 구성요소와 BPF 프로그램이 붙는 자리

3. 코드 지도

저장소는 크지만 처음 볼 곳은 정해져 있습니다.

경로 무엇 처음 읽을 파일
daemon/ 에이전트 진입점과 셀 묶음 daemon/cmd/cells.go
bpf/ 커널에 올라가는 C 프로그램. 진입 파일 bpf_*.c 와 공용 헤더 bpf/lib/ bpf/bpf_lxc.c
pkg/datapath/ BPF 컴파일·적재(loader), 장치·라우트·sysctl 조정, 맵 정의 pkg/datapath/loader/endpoint.go
pkg/loadbalancer/ 1.18 이후 새로 짠 서비스 부하 분산 제어부(StateDB 표 + reconciler) pkg/loadbalancer/README.md
pkg/policy/ 정책 저장소, selector cache, 엔드포인트별 정책 계산 pkg/policy/selectorcache.go
pkg/fqdn/ DNS 프록시, 이름→IP 캐시, matchPattern 정규식 변환 pkg/fqdn/matchpattern/matchpattern.go
pkg/hubble/, hubble-relay/ 흐름 해석·링버퍼·gRPC 서버, 그리고 relay pkg/hubble/observer/local_observer.go
operator/ operator 진입점과 셀 operator/cmd/root.go

에이전트와 operator 는 둘 다 hive 라는 의존성 주입 틀로 조립됩니다. 기능 하나가 「셀(cell)」 하나이고, 셀은 무엇을 제공하고 무엇이 필요한지만 선언합니다. 에이전트 main 은 daemon/main.go:11 에서 hive.New(cmd.Agent) 한 줄이고, 그 Agent 가 daemon/cmd/cells.go:103 에서 Infrastructure·ControlPlane·datapath.Cell 세 묶음으로 나뉩니다. ControlPlane(cells.go:194) 주석이 나눈 이유를 직접 말합니다. 제어부는 「순수한 업무 논리」라서 커널 권한 없이 통합 시험을 돌릴 수 있게 하려는 것입니다. BPF 를 실제로 만지는 일은 pkg/datapath/cells.go:52 의 datapath 모듈로 몰았습니다. 실측한 cilium-dbg status --verbose 의 모듈 트리도 controlplane / datapath / infra 셋으로 갈렸습니다(6절).

4. 핵심 경로 따라가기

4.1 에이전트가 BPF 를 컴파일·적재하는 길

Cilium 은 BPF 오브젝트를 이미지에 미리 구워 두지 않고, 노드에서 clang 으로 컴파일합니다(에이전트 이미지에 clang 19.1.7 이 들어 있음, 실측). 노드마다 커널 기능과 설정(터널 여부, 마스커레이드, IPv6 등)이 달라서, 쓰지 않는 기능을 #ifdef 로 아예 빼야 검증기를 통과하고 명령어 수 한도에도 걸리지 않기 때문입니다.

  1. 기반 데이터패스. pkg/datapath/orchestrator/orchestrator.go:167 reconciler 가 노드 설정이 바뀔 때마다 Reinitialize 를 부르고, pkg/datapath/loader/base.go:298 Reinitialize 가 터널 장치용 bpf_overlay.c, XDP, 그리고 소켓 LB 가 켜져 있으면 bpf_sock.c 를 컴파일해 (base.go:406) cgroup 에 붙입니다 (pkg/socketlb/socketlb.go:83 Enable).
  2. 엔드포인트(파드)마다. pkg/datapath/loader/endpoint.go:83 ReloadDatapath 는 먼저 cache.go:176 fetchOrCompile 을 부릅니다. 여기가 설계의 요점입니다. 엔드포인트의 설정에서 파드마다 다른 값(엔드포인트 ID, IP, ifindex 등)을 뺀 나머지로 해시를 만들고, 같은 해시의 오브젝트가 이미 있으면 컴파일하지 않고 복사해 씁니다. 실제 컴파일은 compile.go:160 compile 이 clang 을 실행합니다.
  3. 값 꽂고 붙이기. endpoint.go:195 reloadEndpoint 가 템플릿에 파드별 값을 상수로 덮어쓰며(Constants: endpointConfiguration(...)) 적재하고, 정책 프로그램을 tail call 맵에 먼저 넣은 뒤 cil_from_container 를 veth 의 ingress 에 붙입니다. 순서에 주석이 달려 있습니다. 입구를 먼저 열면 반쯤 준비된 프로그램으로 패킷이 들어가기 때문입니다. 파드 쪽 egress 프로그램(cil_to_container)은 RequireEgressProg() 가 참일 때만 붙습니다.

실측이 이유를 보여 줍니다. 노드 하나의 엔드포인트 13개가 템플릿 해시 하나를 나눠 썼고, 템플릿은 bpf_lxc.o(약 1.0MB)와 bpf_host.o(약 1.4MB) 둘뿐이었습니다. 컴파일은 한 번, 나머지는 상수만 바꿔 적재하니 파드 생성 지연에서 clang 이 빠집니다.

4.2 kube-proxy 대체: Service 가 connect() 에서 백엔드로 바뀌기까지

kube-proxy 는 Service 를 iptables(또는 IPVS) 규칙으로 바꿉니다. Cilium 은 같은 정보를 BPF 해시 맵에 넣고, 가능하면 패킷이 생기기도 전에 소켓의 목적지 주소를 바꿉니다. 1.18 에서 이 제어부를 StateDB 기반으로 새로 짰고, 그 구조는 pkg/loadbalancer/README.md 의 그림 그대로입니다.

  1. 반사(reflect). pkg/loadbalancer/reflectors/k8s.go:208 runServiceEndpointsReflector 가 Service 와 EndpointSlice 변경을 잠깐 모았다가 한 트랜잭션으로 씁니다(대량 롤아웃 때 맵을 수천 번 따로 고치지 않으려는 것).
  2. 표에 쓰기. pkg/loadbalancer/writer/writer.go:326 UpsertServiceAndFrontends 등이 서비스·프런트엔드·백엔드 세 표의 참조를 맞춥니다. 출처가 여럿이어도 이 Writer 로만 들어옵니다.
  3. 맵으로 맞추기. pkg/loadbalancer/reconciler/bpf_reconciler.go:725 Update 가 프런트엔드 표를 지켜보다가 updateFrontend(842행) 에서 맵을 씁니다. 맵 cilium_lb4_services_v2(bpf/lib/lb.h:284) 는 키가 (주소, 포트, 프로토콜, 슬롯) 이고, 슬롯 0 이 「머리 칸」(백엔드 수·플래그), 슬롯 1..n 이 백엔드 ID 입니다. 재시도는 reconciler 가 맡아 데이터 출처는 맵 오류를 몰라도 됩니다.
  4. 소켓에서 풀기. bpf/bpf_sock.c:467 cil_sock4_connect 가 cgroup connect4 훅에서 __sock4_xlate_fwd(298행) 를 부르고, 거기서 lb4_lookup_service 로 머리 칸을 찾은 뒤 sock_select_slot() % count + 1 로 슬롯을 골라 소켓 주소를 백엔드로 바꿉니다. 응용은 ClusterIP 에 연결했다고 믿지만 커널 소켓은 처음부터 백엔드에 붙어 있어서, 패킷마다 NAT 할 일이 없습니다.

그런데 4단계에는 문이 하나 있습니다.

	if (is_defined(ENABLE_SOCKET_LB_HOST_ONLY) && !in_hostns)
		return -ENXIO;

	if (!udp_only && !sock_proto_enabled(protocol))
		return -ENOTSUP;

	/* In case a direct match fails, we try to look-up surrogate
	 * service entries via wildcarded lookup for NodePort and
	 * HostPort services.
	 */
	svc = lb4_lookup_service(&key, true);
	if (!svc)
		svc = sock4_wildcard_lookup_full(&key, in_hostns);
	if (!svc)
		return -ENXIO;

출처: cilium v1.20.1 bpf/bpf_sock.c 322–335행, GPL-2.0-only OR BSD-2-Clause

socketLB.hostNamespaceOnly 를 켜면 파드 네임스페이스의 소켓은 여기서 그냥 돌아갑니다. 그러면 서비스 주소 변환은 누가 하나요? bpf_lxc.c 가 컴파일 때 스스로 판단합니다.

#if !defined(ENABLE_SOCKET_LB_FULL) || \
    defined(ENABLE_SOCKET_LB_HOST_ONLY) || \
    defined(ENABLE_L7_LB)               || \
    defined(ENABLE_SCTP)                || \
    defined(ENABLE_CLUSTER_AWARE_ADDRESSING)
# define ENABLE_PER_PACKET_LB 1
#endif

출처: cilium v1.20.1 bpf/bpf_lxc.c 90–96행, GPL-2.0-only OR BSD-2-Clause

소켓에서 다 풀 수 없는 경우가 하나라도 있으면 파드 veth 의 tc 프로그램에 패킷 단위 LB (__per_packet_lb_svc_xlate_4, 152행)가 들어갑니다. 공식 문서는 이 우회를 Istio 사이드카처럼 원래 ClusterIP 가 필요한 경우와, KubeVirt·Kata·gVisor 처럼 소켓 LB 가 효과가 없는 파드를 위한 것이라고 설명합니다(kubeproxy-free 문서 「Socket LoadBalancer Bypass in Pod Namespace」). 실측한 작은 클러스터가 정확히 이 모드였습니다 (Socket LB Coverage: Hostns-only). TT Lab 클러스터는 KubeVirt VM 을 돌리므로 이 선택에는 이유가 있습니다 (KubeVirt 실습 VM 시리즈). 결과적으로 파드에서 서비스로 가는 연결은 cil_from_container 안에서 풀리고, 소켓 역변환 맵(cilium_lb4_reverse_sk)에는 호스트 네임스페이스 소켓 13개만 있었습니다. 「kube-proxy 대체 = 소켓 LB」 라고 외워 두면 틀리는 지점입니다.

4.3 toFQDNs 정책이 DNS 프록시를 거쳐 「이 IP 허용」이 되는 길

BPF 는 IP 와 포트만 봅니다. 그런데 사람은 *.docker.com 처럼 이름으로 정책을 쓰고 싶어 합니다. Cilium 의 답은 DNS 응답을 가로채서 이름→IP 를 배우고, 배운 IP 에 이름에서 나온 identity 를 붙이는 것입니다. 이름은 BPF 까지 내려가지 않고, BPF 는 늘 하던 대로 「이 identity 허용?」 만 묻습니다.

toFQDNs 가 「이 IP 허용」 이 되기까지 강조한 두 칸(③·⑤)이 matchpattern 정규식을 쓰는 곳 ① 정책이 FQDN 셀렉터를 등록 toFQDNs → NameManager.RegisterFQDNSelector ② 파드의 DNS 질의를 프록시로 돌린다 bpf_lxc 가 53번을 노드의 DNS 프록시로(투명 프록시) ③ 질의 이름 검사 — rules.dns ServeDNS → CheckAllowed, 안 맞으면 REFUSED ④ 위로 보내고 응답을 받는다 DNSClients.Exchange → 응답의 이름·IP·TTL ⑤ 이름이 어느 toFQDNs 셀렉터에 맞나 UpdateGenerateDNS → deriveLabelsForName ⑥ IP 에 fqdn: 라벨 → identity → 정책 맵 ipcache 갱신 → SelectorCache.UpdateIdentities ⑦ 반영을 기다린 뒤 응답을 돌려준다 WaitForRevision(최대 100ms) → WriteMsg ⑧ 파드가 그 IP 로 연결 → bpf_lxc 가 정책 맵에서 허용을 찾는다 matchpattern 변환 (escapeRegexpCharacters) *.docker.com — * 는 점을 넘지 않는다 ^[-a-zA-Z0-9_]*[.]docker[.]com[.]$ **.docker.com — 앞머리 **. 만 여러 칸 ^(라벨([.]라벨)*)[.]docker[.]com[.]$ 질의 이름 * 패턴 **. 패턴 hub.docker.com 맞음 맞음 production.cloudflare.docker.com 아님 맞음 docker.com 아님 아님
그림: toFQDNs 가 IP 허용이 되기까지
  1. 셀렉터 등록. 정책에 toFQDNs 가 있으면 pkg/fqdn/namemanager/manager.go:140 RegisterFQDNSelector 가 셀렉터를 받아 두고, 이미 캐시에 있는 이름 중 맞는 것이 있으면 바로 IP 를 표시합니다. 셀렉터의 정규식은 pkg/policy/api/fqdn.go:115 ToRegex 가 matchpattern 으로 만듭니다.
  2. 질의 가로채기. 같은 정책의 rules.dns(포트 53) 때문에 bpf_lxc 가 그 파드의 DNS 패킷을 노드의 DNS 프록시로 돌립니다. pkg/fqdn/dnsproxy/proxy.go:895 ServeDNS 가 CheckAllowed(763행) 로 질의 이름을 검사합니다. 그 정규식은 GeneratePattern(1439행) 이 rules.dns 의 패턴들을 | 로 묶어 만든 것입니다. 맞지 않으면 거절 응답을 돌려줍니다(작은 클러스터 설정은 refused).
  3. 응답 → 이름 캐시. 허용되면 위로 보내고, 응답을 받으면 클라이언트에 돌려주기 전에 NotifyOnDNSMsg 를 부릅니다. 이것이 pkg/fqdn/messagehandler/message_handler.go:277 UpdateOnDNSMsg 를 거쳐 namemanager/manager.go:214 UpdateGenerateDNS 로 갑니다.
  4. 이름 매칭. manager.go:422 deriveLabelsForName 이 응답 이름을 모든 FQDN 셀렉터의 정규식에 대 보고, 맞는 셀렉터마다 fqdn:<패턴> 라벨을 모읍니다.
  5. IP 에 identity. manager.go:315 updateMetadata 가 응답의 IP 마다 그 라벨을 ipcache 에 올리고, ipcache 가 라벨 묶음에 맞는 identity 를 할당합니다. 새 identity 는 pkg/policy/selectorcache.go:768 UpdateIdentities 로 셀렉터 캐시에 들어가고, 그 셀렉터를 쓰는 엔드포인트의 정책 맵에 허용 칸이 생깁니다.
  6. 기다렸다 돌려주기. UpdateGenerateDNS 는 ipcache 가 그 변경을 맵까지 반영할 때까지(WaitForRevision) 기다리는 채널을 돌려줍니다. 핸들러는 tofqdns-proxy-response-max-delay(작은 클러스터 100ms) 동안만 기다리고 응답을 파드에 씁니다. 파드가 응답을 받자마자 연결해도 정책 맵이 이미 열려 있게 하려는 순서입니다.

여기서 matchpattern 이 두 번 쓰인다는 점이 중요합니다(그림의 ③·⑤). 변환 규칙은 짧습니다.

func escapeRegexpCharacters(pattern string) string {
	// Convert '.' in the match pattern as literal '.' for regex pattern.
	pattern = strings.ReplaceAll(pattern, ".", "[.]")

	// '**.' in match pattern prefix is a subdomain wildcard specifier which matches one ore more
	// entire labels.
	pattern = subdomainWildcardSpecifierPrefix.ReplaceAllString(pattern, dnsWildcardREGroup)

	// Base case: * becomes .*, but only for DNS valid characters
	// `*` wildcard matches all DNS characters within the subdomain boundary(doesn't include '.' literal)
	pattern = wildcardSpecifier.ReplaceAllString(pattern, allowedDNSCharsREGroup+"*")

	return pattern
}

출처: cilium v1.20.1 pkg/fqdn/matchpattern/matchpattern.go 131–144행, Apache-2.0

* 는 [-a-zA-Z0-9_]*(17행)로 바뀌어 점을 넘지 못합니다. 여러 칸을 받는 것은 맨 앞의 **. 하나뿐이고(37행의 ^[*]{2,}\[\.\], 23행의 dnsWildcardREGroup), 가운데의 ** 는 마지막 줄에서 그냥 * 처럼 바뀝니다. 끝에 점을 붙이는 정규화는 Sanitize(87행) 가 합니다.

실측 사례. 기여를 준비하며 작은 클러스터에서 잰 기록(2026-10-16~17, PR 초안)입니다. 이미지 레지스트리용 정책이 toFQDNs: matchPattern: "*.docker.com" 이고 DNS 는 * 로 열린 파드에서 세 이름을 두 번씩 접속했습니다.

이름 DNS 해석 접속 이유
hub.docker.com 됨 연결(HTTP 200) 한 칸 → *.docker.com 에 맞음
production.cloudflare.docker.com 됨 시간 초과(curl 28) 두 칸 → * 가 점을 못 넘음
docker.com 됨 시간 초과 *. 앞에 한 칸이 있어야 함

둘째 줄은 도커 허브의 이미지 계층을 내려주는 CDN 이름이라, 정책은 맞아 보이는데 이미지 받기만 실패하는 증상이 됩니다. 같은 기록에서 main 의 matchpattern 을 Go 로 직접 돌린(Validate(Sanitize(p))) 결과도 표와 같았고(**.docker.com 은 둘째 줄에 맞음), 글을 쓰며 변환 규칙을 파이썬으로 옮겨 다시 돌린 결과도 같았습니다(그림 아래 표). DNS 는 모두 해석되었으므로 막힌 곳은 ③이 아니라 ⑤입니다. 같은 성질이 실습 파드의 DNS 출구 정책에서 어떤 문제를 만들었는지는 DNS 로 새는 길 막기 에 따로 적었습니다. 이 글은 그 성질이 코드의 어느 줄에서 나오는지까지만 다룹니다.

4.4 Hubble: perf 링버퍼에서 relay 까지

  1. BPF 가 이벤트를 낸다. 패킷이 전달·차단·정책 판정될 때 bpf/lib/trace.h:210 _send_trace_notify 등이 cilium_events(bpf/lib/events.h:13, PERF_EVENT_ARRAY)로 짧은 기록을 씁니다. 모든 패킷을 쓰면 비싸므로 MONITOR_AGGREGATION 으로 줄입니다. 실측한 엔드포인트의 ep_config.h 는 MONITOR_AGGREGATION 3(활성 연결의 추적을 간추림, ConfigMap 의 monitor-aggregation: medium)이었습니다.
  2. 에이전트가 읽는다. pkg/monitor/agent/agent.go:324 handleEvents 가 perf 리더를 열고, 레코드마다 notifyPerfEventLocked(428행) 로 소비자들에게 나눕니다. Hubble 은 그 소비자 중 하나입니다(pkg/hubble/monitor/consumer.go:73).
  3. 해석해 링버퍼에. pkg/hubble/observer/local_observer.go:116 Start 의 루프가 이벤트를 Decode 해 흐름(Flow)으로 만들고, 지표 훅을 거쳐 container/ring.go:168 Ring.Write 로 고정 크기 링에 넣습니다. 크기는 2의 거듭제곱 빼기 1(기본 4095)이라 인덱스를 나눗셈 대신 마스크로 구합니다. GetFlows(260행)는 이 링을 읽기만 합니다.
  4. relay 가 모은다. relay 는 각 에이전트의 Hubble 서버(:4244, TLS)에 붙어 둡니다(pkg/hubble/relay/pool/manager.go:51). 요청이 오면 pkg/hubble/relay/observer/server.go:62 GetFlows 가 모든 피어에 같은 요청을 보내고(observer.go:37), 돌아온 흐름을 시간순으로 다시 정렬해(sortFlows, 67행) 내보냅니다.

설계의 뜻은 「저장하지 않는다」입니다. 흐름은 노드 메모리의 링에만 있고 relay 도 보관하지 않습니다. 실측한 노드는 링이 4095/4095 로 찬 채 초당 58.5개가 들어오고 있었으니, 한 노드가 기억하는 과거는 약 70초입니다. 오래 남기려면 지표나 exporter 를 따로 씁니다.

5. 사용자 기여와의 연결

글쓴이는 4.3의 성질을 문서에 바로잡는 변경을 준비해 두었습니다(원장 5절 「준비만 된 것」). 아직 게시하지 않았습니다. Cilium 은 AI 가 상당 부분 쓴 소통(PR 본문 등)을 받지 않는 정책이라, 본문은 글쓴이가 직접 써야 합니다. 그래서 여기서도 「준비만 했다」 까지만 적습니다.

문서 위치(v1.20.1) 지금 내용 코드 근거
Documentation/security/policy/layer7.rst:159 L7 DNS 의 matchPattern 설명에 **. 가 없음 GeneratePattern 도 ToUnAnchoredRegexp → 같은 escapeRegexpCharacters
layer7.rst:166, :227 한 칸만 맞는 예를 「any subdomains」 라고 부름 * → [-a-zA-Z0-9_]*(141행)
layer3.rst:548 **. 설명은 있으나 「앞머리에서만」이 없음 37행 정규식이 ^ 로 고정, 시험의 test.**.cilium.io. 사례

근거는 4.3의 실측 표와 Go 재현 결과입니다. 문서 세 파일과 matchpattern.go 는 v1.20.1 과 main 이 같다는 것을 준비 때 확인했다고 초안에 적혀 있습니다.

6. 실측

2026-10-17, 작은 클러스터(노드 8대, kube-proxy DaemonSet 없음)에서 컨트롤 플레인 노드 하나의 에이전트 파드에만 읽기 명령을 보냈습니다. 원자료는 따로 보관했고, 아래는 이름·주소를 뺀 값입니다.

명령 본 것
cilium-dbg status KubeProxyReplacement: True, Routing: Network: Tunnel [vxlan] Host: BPF, Attach Mode: TCX, Device Mode: veth, Masquerading: BPF, Host firewall: Enabled, 컨트롤러 90/90, 모듈 125 OK, 클러스터 건강 8/8
cilium-dbg status --verbose Socket LB: Enabled, Socket LB Coverage: Hostns-only, Backend Selection: Random, XDP Acceleration: Disabled, IPAM 은 노드당 /24 에서 15개 할당
cilium-dbg service list 프런트엔드 211개(ClusterIP 171, NodePort 20, LoadBalancer 20)
cilium-dbg bpf lb list, map list cilium_lb4_services_v2 573칸 = 머리 칸 358 + 백엔드 슬롯 215. 머리 칸 중 127개는 포트 0/ANY 와일드카드 칸(용도 세부는 미확인). 백엔드 맵 163, 역변환 맵 231, 소켓 역변환 13
bpftool net show lxc 장치 13개 모두 tcx/ingress cil_from_container 만, egress 없음. 물리 NIC cil_from/to_netdev, cilium_host·cilium_net cil_to_host·cil_from_host, cilium_vxlan cil_from/to_overlay, XDP 비어 있음
bpftool prog show Cilium 프로그램 231개(tc 218, cgroup sock_addr 8, cgroup sock 3, 소켓 종료용 tracing 2). tc 쪽은 엔드포인트마다 대략 진입 3개 + tail call 10개
bpftool cgroup tree cgroup 루트에 cil_sock4_connect·sendmsg4·recvmsg4·getpeername4·post_bind4·sock_release 등 11개
상태 디렉터리 ls 템플릿 2개(bpf_lxc.o 1,086,040바이트, bpf_host.o 1,452,768바이트). 엔드포인트 13개의 template.txt 해시가 모두 같음
hubble observe --last 500 500개가 10.5초 분량, FORWARDED 489 · DROPPED 11(모두 UNSUPPORTED_L3_PROTOCOL). 링 4095/4095, 58.5 흐름/초
cilium-dbg fqdn cache list, fqdn names 이 노드는 비어 있음(FQDN 셀렉터 0) — 이 노드에는 DNS 규칙이 걸린 파드가 없었음
정책 개수(클러스터 전체) CNP 22개 중 rules.dns 가 있는 것 5, toFQDNs 가 있는 것 2
relay 로그 피어 8개에 :4244, TLS 로 연결

읽을 것이 셋입니다.

7. 어디부터 읽을까

  1. daemon/cmd/cells.go:103 — 셀 목록을 훑으며 에이전트에 무엇이 들어 있는지 지도를 만듭니다. 주석이 친절합니다.
  2. pkg/loadbalancer/README.md 와 bpf_reconciler.go:842 — 「표 → reconciler → 맵」 이라는 Cilium 의 새 제어부 문법을 가장 짧게 배우는 곳입니다.
  3. bpf/bpf_sock.c:298 — 맵을 읽는 쪽. 2번에서 쓴 칸이 어떻게 읽히는지 맞춰 봅니다.
  4. pkg/datapath/loader/endpoint.go:195 — 템플릿·상수·tcx 부착 순서.
  5. pkg/fqdn/dnsproxy/proxy.go:895 에서 namemanager/manager.go:214 까지 — 사용자 공간 프록시와 BPF 정책이 만나는 곳.
  6. bpf/bpf_lxc.c:1780 cil_from_container — 마지막에 읽습니다. tail call 과 #ifdef 가 많아서, 앞의 다섯을 알고 봐야 길을 잃지 않습니다.

8. 첫 기여하기 좋은 곳

소스를 읽으며 실제로 걸린 것들입니다(게시하지 않았고, main 에서 그대로인지는 미확인).

9. 한계

티티 한마디. Cilium 의 검문소는 전화를 안 받아요. 미리 받은 규칙표만 보죠. 그러니까 이상하면 검문소 말고 누가 그 표를 썼는지를 찾아가 보세요. 그리고 * 는 점 하나도 못 넘는 겁쟁이라는 것, 잊지 마세요.

참고

모든 자료는 2026-10-17 에 열람했습니다. 소스는 cilium/cilium v1.20.1 태그(커밋 7d68cfb394f2)입니다.

인용한 코드: bpf/bpf_sock.c·bpf/bpf_lxc.c 는 GPL-2.0-only OR BSD-2-Clause, pkg/fqdn/matchpattern/matchpattern.go 는 Apache-2.0 (Copyright Authors of Cilium).

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다