기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만 있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문 종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다. SSOT 결함 둘을 고쳤다. - kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이 「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽 `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다. - virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데 원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다. 기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은 둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더 든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다). 계약을 셋 고쳤다. - kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/** 28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는 것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온 것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수 대조했을 때 가장 가까운 커밋도 28개가 어긋났다). - virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고 재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다. - kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고 넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다. style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져 (실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고, engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고 있었다(Question 기록에서 11.94 → 3.86). verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
14 KiB
id, kind, slug, title, topic, topicName, project, status, questionStatus, studio, sourceRevision, source
| id | kind | slug | title | topic | topicName | project | status | questionStatus | studio | sourceRevision | source | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 600a2621-f6d8-42df-9629-7db65011545d | QUESTION | actual-packet-path-nginx-to-keycloak | Host Nginx 에서 Keycloak 까지 packet 은 실제로 어디를 지나는가 | network-virtualization | 네트워크 가상화 | virtualization | 초안 | OPEN | https://hyeonworks.com/studio/documents/600a2621-f6d8-42df-9629-7db65011545d/edit | no-commit · 밖에서 반입한 문서 한 편. 고정할 저장소 리비전이 없다 |
|
Host Nginx 에서 Keycloak 까지 packet 은 실제로 어디를 지나는가
경로가 한 번 바뀌었다. §179 는 nginx 를 물리 호스트에서 엣지 게스트로 옮기고 호스트에는 커널 DNAT(Destination NAT, 목적지 주소 변환) 만 두었다고 적었다. §116 이 그린 그림은 옮기기 전 모양이고, 네 지점에 tcpdump 를 걸어 본 기록은 아직 없다.
관계
- Guest 의 packet 이 Host Physical NIC 에 닿기까지 — virtio-net · virtqueue · vhost-net · TAP · Bridge §94 와 §125 가 그린 기준 경로를 이 개념이 설명한다. 이 물음은 그 경로가 이 호스트에서도 같은지를 묻는다.
- packet 이 어디서 끊겼는지는 계층마다 capture 해서 가른다 §119 가 든 계층별 캡처 방법을 규칙으로 편 기준이다. 요청이 중간에서 끊기면 그쪽이 받는다.
- 이 호스트의 가상 머신 네트워크는 Bridge 인가 NAT 인가 Routing 인가 구성이 셋 중 무엇이냐에 따라 캡처를 걸 지점이 달라진다.
- VM1 과 VM2 의 TAP/vnet interface 는 무엇이고 어디에 붙어 있는가 tcpdump 를 걸 인터페이스의 이름을 그쪽이 댄다.
- Keycloak 실험 결과를 애플리케이션 원인으로 읽기 전에 network 계층을 따로 검증한다 §120 이 요구한 별도 검증에서 이 경로 확인이 첫 항목이 된다.
사실
- §116 이 그린 테스트 환경의 요청 경로 Client → Host Physical NIC → Host Nginx → Host Network → VM1 / VM2 → K3s → Keycloak Node 1 / 2
- §116 이 가상 머신 네트워크까지 펼친 경로 Client → Physical NIC → Host Network Stack / Bridge / Route / NAT → TAP(vm1) / TAP(vm2) → vhost-net → virtqueue → virtio-net → Guest Network Stack → K3s networking → Keycloak
- §116 은 그 뒤에 K3s 내부의 CNI · Service · Pod network 가 추가되므로 별도 계층으로 분석한다고 적었다.
- §94 는 이 문서가 기준으로 삼은 구조를 virtio-net + vhost-net + TAP + Linux Bridge 로 놓고 수신 방향과 송신 방향을 각각 그렸다. 실제 환경은 Bridge · NAT · Routed Network · macvtap · SR-IOV · VFIO passthrough · Open vSwitch · Kubernetes CNI 에 따라 달라질 수 있다고 밝혔다.
- §125 는 같은 경로를 vhost-net 을 쓸 때와 QEMU backend 를 쓸 때로 나눠 다시 그렸다. 두 그림은 TAP 다음이 vhost-net 인지 QEMU virtio backend 인지에서 갈리고 나머지 구간은 같다.
- §119 는 경로를 확인하는 방법으로 호스트의 physical NIC · bridge · tap 또는 vnet 세 곳과 게스트의 guest interface 한 곳에 sudo tcpdump -ni 를 걸고, 어디까지 보이는지로 의심 구간을 좁히라고 적었다.
- §119 가 든 판정 예 셋 Physical NIC O · Bridge O · TAP X : Guest 내부보다 먼저 Host Bridge/TAP mapping 을 의심한다 TAP O · Guest NIC X : virtio/vhost/Guest NIC 계층을 의심한다 Guest NIC O · Socket X : Guest routing/firewall/listen 상태를 의심한다
- §122 OQ-6 은 Host NIC · Bridge · TAP · Guest NIC 에서 tcpdump 로 추적한다고만 적었다. 어느 이름의 인터페이스인지는 적혀 있지 않다.
- §116 의 그림은 앞부분과 뒷부분의 근거가 다르다. 호스트 Nginx 아래에 가상 머신 두 대가 있고 그 안에서 K3s 와 Keycloak 노드가 돈다는 앞부분은 §89 가 밝힌 실험 구성이다. 브리지와 라우팅·NAT, TAP, vhost-net 으로 펼친 뒷부분은 OQ-1 과 OQ-2, OQ-3 를 확인하기 전의 가정이다.
- §179 와 §254 는 같은 nginx 를 물리 호스트에서 엣지 게스트로 옮겼다고 적고 전후를 나란히 그렸다.
전 :
tailnet:443→ 호스트 nginx → Traefik(게스트 .11/.12) 후 :tailnet:443→ 호스트 커널 DNAT → 엣지 nginx(.10) → Traefik(.11/.12) - §179 는 그렇게 옮겨도 L7 홉 수는 2홉 그대로라고 관측으로 적었다. 늘어난 것은 커널이 하는 L4 전달 한 번이다. 지금 호스트에는
:443을 듣는 리스너가 없다. - §179 는 옮긴 이유를 성능이 아니라 더러워지는 층의 격리로 적었다. nginx 설정과 인증서, certbot, deploy 훅은 자주 갈아엎는 것들인데 호스트에 있으면 초기화가 불가능하고, 엣지 장애 실험이 SSH 까지 위험하게 만든다. 그 대가로 일곱 가지가 새로 필요해졌고 §179 는 그중 DNAT 와 libvirt 방화벽 구멍 둘을 이 이동의 본질로 꼽았다. 그 둘을 본질로 본 것은 추론이라고 밝혔다.
- §254 는 호스트에서 게스트로 가는 요청이 OUTPUT 경로라 필터를 타지 않는다고 적었다. 밖에서 게스트로 들어오는 요청은 FORWARD 경로라 libvirt 의 guest_input 체인을 지난다.
- §207 은 DNAT 파일에 forward 체인을 두지 않은 이유를 그 파일의 주석으로 적어 두었다. libvirt 의 guest_input 체인은 virbr0 으로 나가는 프레임을 reject 하며 끝난다. nftables 에서는 앞 base 체인의 accept 가 뒤 체인의 reject 를 멈추지 못하므로 구멍을 libvirt 체인 맨 앞에 뚫었고, 그래서 그 파일에 forward 체인이 없는 것은 실수가 아니다.
- §207 은 이 DNAT 가 PREROUTING nat 에 있어 라우팅 결정보다 먼저 돌고 호스트의 로컬 소켓보다 이긴다고 적었다. 그래서 엣지를 띄운 채 전환하고, 되돌릴 때는 테이블 하나를 지운다.
- §207 은 DNAT 만 하고 SNAT 는 하지 않는다고 적었다. 게스트의 기본 경로가 호스트여서 응답이 이 경로를 다시 지나고 conntrack 이 변환을 알아서 되돌린다. masquerade 를 붙이면 엣지가 모든 클라이언트를 192.168.122.1 로 보게 된다.
- §208 은 DNAT 유닛의 ExecStartPost 앞에 붙은 하이픈을 설명했다. libvirt_network 테이블은 가상 네트워크가 떠 있어야 존재하는데, 부팅 순서에 따라 이 유닛이 먼저 돌 수 있다. 하이픈이 없으면 그때 규칙 삽입이 실패하면서 DNAT 까지 같이 안 실린다. 하이픈을 두면 DNAT 는 실리고 구멍만 빠진 상태가 되어 systemctl restart 한 번으로 다시 뚫린다.
- §208 은 그 상태에서 유닛이 active 이고 아무 오류도 없다고 적고, 유닛이 active 라는 것은 DNAT 가 실렸다는 뜻이지 구멍이 뚫렸다는 뜻이 아니라고 추론으로 밝혔다. 그 상태를 실제로 재현해 보지는 않았다고 적혀 있다.
- §180 은 구멍이 빠졌을 때의 출력을 관측으로 남겼다. 호스트에서 게스트 주소로 친 curl 은 404 였고, 밖에서 tailnet 주소로 친 curl 은 connection refused 였다. reject 규칙의 카운터는 packets 4 bytes 240 으로 밖에서 친 횟수와 맞았다.
- §254 는 안쪽과 바깥쪽을 한 번씩 쳐서 같은 값이 나오는지 보는 확인을 적었다. 둘 다 404 면 경로가 이어진 것이고, 안쪽만 404 면 nginx 설치 · DNAT · libvirt 구멍 셋 가운데 하나가 빠진 것이다.
- 이 호스트에서 계층마다 패킷을 잡아 본 기록은 없다. §116 이 펼친 경로는 확인한 결과가 아니라 §94 의 기준 구조를 이 환경에 얹은 그림이다.
가정
- 밖에서 tailnet 주소로 Keycloak 요청을 한 번 보낼 수 있고, 그 요청이 두 k3s 노드 가운데 어느 쪽으로 갔는지 확인하는 쪽이 알 수 있다고 본다.
- 네 지점에 동시에 캡처를 걸어 둘 수 있다고 전제한다. 지점마다 따로 걸면 같은 요청을 본 것인지 갈리지 않는다.
- §116 이 그린 뒷부분 가운데 TAP 다음 구간은 지금도 같다고 전제한다. 앞부분은 §179 가 바꿔 놓았고, TAP 뒤쪽 서술이 그 이동으로 달라졌는지는 적힌 것이 없다.
- 오프로드 설정이 캡처하는 동안 바뀌지 않는다.
미지수
- 밖에서 온 요청이 호스트 커널 DNAT 를 지난 뒤 virbr0 에서 어느 TAP 으로 나가 엣지 게스트로 들어가는지.
- 엣지 nginx 가 k3s 노드로 넘기는 두 번째 홉이 virbr0 안에서 L2 로만 도는지, 호스트의 L3/Netfilter 를 지나는지. §119 의 네 지점은 밖에서 게스트로 들어오는 한 홉만 본다.
- 그 두 번째 홉의 TAP 이 kc-lab-1 과 kc-lab-2 가운데 어느 쪽의 인터페이스인지.
- 게스트 안의 인터페이스에서 같은 요청이 보이는지.
- 네 지점의 결과가 §116 이 펼친 그림과 같은지, 다르다면 어느 지점부터 다른지.
- 지금 오프로드 설정이 무엇인지. 캡처에서 본 패킷 크기와 체크섬을 wire 값으로 읽어도 되는지가 이것으로 갈린다.
제약
- 캡처 지점의 이름을 먼저 확정해야 한다. 어느 브리지와 어느 TAP 인지 모르면 tcpdump 를 걸 곳을 고르지 못한다. 그 이름은 네트워크 구성을 묻는 물음과 인터페이스를 묻는 물음이 댄다.
- K3s 안에서 Keycloak Pod 까지 가는 구간은 이 물음이 닫는 범위 밖이다. §116 이 CNI · Service · Pod network 를 별도 계층으로 미뤄 두었다.
- 요청이 중간 지점에서 끊기면 그것은 이 물음의 답이 아니라 장애다. §119 가 든 세 판정 예를 따라 의심 구간을 적고 계층별 캡처 기준으로 넘긴다.
- §116 의 그림은 엣지를 옮기기 전 모양이다. 지금 구성으로 캡처하면 호스트 nginx 였던 칸에 커널 DNAT 와 엣지 게스트가 들어가므로 캡처 지점이 그만큼 늘어난다.
- 이 호스트에서 잰 값이 없어 §116 의 그림을 확인된 경로로 삼지 않는다. 이 환경이 실제로 그렇다는 주장이라 재기 전에는 Case 도 Concept 도 아니라고 보고 근거가 모자란 후보로 남겨 두었다. OQ-1 과 OQ-2, OQ-3, OQ-6 이 답하면 다시 판정한다.
선택지
1. 네 지점에 한 번에 걸고 요청을 한 번 보낸다
§119 가 든 호스트 세 지점과 게스트 한 지점에 동시에 tcpdump 를 걸어 두고, 밖에서 tailnet 주소로 Keycloak 요청을 한 번 보낸다. 같은 요청 하나가 네 지점에서 각각 보이는지로 경로가 확정되기 때문에 실행이 한 번으로 끝난다.
지점 이름을 미리 확정해 두어야 하고 네 곳을 동시에 열어 두어야 한다.
2. 바깥에서 안쪽으로 한 지점씩 옮겨 간다
물리 NIC 에서 시작해 브리지, TAP, 게스트 인터페이스 순으로 한 지점씩 옮기며 같은 요청을 반복해서 보낸다. 여러 곳을 동시에 열지 않아도 되고 어디서부터 안 보이는지가 바로 드러난다.
요청을 여러 번 보내야 해서 매번 같은 경로로 갔다고 전제해야 한다. 두 가상 머신에 요청이 번갈아 가면 그 전제가 깨진다.
3. 두 가상 머신 쪽 TAP 을 모두 열어 놓고 한 번 보낸다
VM1 과 VM2 의 TAP 을 둘 다 열어 두면 요청이 어느 쪽으로 갔는지까지 같은 실행에서 나온다. §120 이 든 「Node1 요청만 지연」 같은 증상을 나중에 볼 때 어느 경로를 먼저 열어야 하는지도 이 기록에서 나온다.
캡처 지점이 다섯으로 늘어난다.
4. 제외 — 호스트 Nginx 설정에서 경로를 읽는다
Nginx 가 어느 주소로 요청을 넘기는지 설정만 읽어도 어느 가상 머신으로 가는지는 알 수 있다. 그러나 이 물음은 설정이 말하는 경로가 아니라 패킷이 실제로 지나는 계층을 묻는다. §116 이 그린 그림이 확인된 것이 아니라는 데서 물음이 시작했으므로, 설정을 읽으면 같은 종류의 근거가 하나 더 늘어난다.
다음 검증
- 캡처를 걸 지점의 이름을 먼저 확정한다. 어느 브리지와 어느 TAP 인지는 네트워크 구성을 묻는 물음과 인터페이스를 묻는 물음이 답한다.
- 호스트에서 sudo tcpdump -ni 뒤에 physical NIC 이름 · bridge 이름 · tap 또는 vnet 이름을 넣어 세 곳에 걸고, 게스트에서 같은 명령을 guest interface 이름에 건다 (§122 OQ-6 · §119). 엣지 게스트의 TAP 과 k3s 노드의 TAP 을 함께 열어야 §179 가 적은 두 홉이 한 실행에서 잡힌다.
- 밖에서 tailnet 주소로 Keycloak 요청을 한 번 보낸다. §179 대로라면 그 요청은 호스트 커널 DNAT 를 지나 엣지 nginx 로 가고, 거기서 다시 k3s 노드로 넘어간다.
- 네 지점에서 그 요청이 보였는지를 §119 처럼 O 와 X 로 적는다.
- 캡처하는 동안의 오프로드 상태를 함께 적는다. §113 은 오프로드가 켜져 있으면 tcpdump 에서 보이는 패킷 크기나 체크섬이 실제 wire 에서 보이는 것과 다르게 보일 수 있다고 적었다.
닫는 조건 : 네 지점의 캡처 결과로 실제 경로를 한 줄로 적으면 닫는다. §116 이 펼친 그림과 같으면 그 그림을 이 호스트에서 확인된 경로로 올리고, §116 을 근거로 삼는 후보를 다시 판정한다. 다르면 어느 지점부터 다른지를 적고 §119 의 판정 예대로 그 구간을 의심 구간으로 넘긴다. 요청이 중간 지점에서 끊기면 그것은 이 물음의 답이 아니므로 계층별 캡처 기준으로 넘긴다.