feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다

기록 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>
This commit is contained in:
DongHyeonka
2026-09-17 11:01:55 +09:00
co-authored by Claude Opus 5
parent d473609e0a
commit 2109f726fe
574 changed files with 159654 additions and 1551 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,289 @@
{
"version": "1.1",
"id": "nftables-forward-hook-chain-order",
"title": "같은 forward 훅에 붙은 base 체인 둘이 우선순위 순으로 이어서 평가되고, 앞 체인의 accept 는 뒤 체인의 reject 를 막지 못한다",
"question": "밖에서 온 packet 하나가 forward 훅에서 어느 규칙을 어떤 순서로 지나 어디서 끝났고, insert 로 넣은 구멍은 그 순서의 어디에 들어가는가",
"type": "network",
"direction": "LR",
"audience": [
"iptables 감각으로 nftables 규칙을 쓰다 밖에서만 막히는 것을 보는 사람",
"libvirt NAT 뒤의 게스트에 밖에서 들어오는 경로를 여는 사람"
],
"summary": "밖에서 온 packet 은 priority filter - 10 인 forward 체인의 ct state new accept 를 지나고도 평가가 끝나지 않아 libvirt_network guest_input 으로 이어지고, 거기서 체인 끝 reject 에 닿아 connection refused 가 되며, insert 로 넣은 구멍은 같은 체인의 맨 앞에 서서 같은 packet 을 엣지 nginx 로 보낸다.",
"alt": "밖에서 온 packet 이 forward 훅에 붙은 base 체인 둘을 우선순위 순으로 지나는 경로도. 앞 체인의 accept 뒤에 libvirt guest_input 이 이어지고, 구멍을 넣기 전 경로는 그 체인 끝 reject 로, 넣은 뒤 경로는 체인 맨 앞의 구멍을 지나 엣지 nginx 로 갈라진다.",
"long_description": "왼쪽에서 오른쪽으로 읽는다. 왼쪽 끝이 밖에서 친 curl 이 만든 inbound packet 이고 forward 훅으로 들어간다. 첫 점선 상자가 DNAT 파일에 둔 base 체인이다. priority filter - 10 이라 먼저 돌고, 그 안의 ct state new accept 가 이 packet 을 통과시킨다. 다음 점선 상자가 libvirt 가 만든 ip libvirt_network 테이블의 guest_input 체인이다. 앞 체인의 accept 는 평가를 끝내지 않으므로 같은 packet 이 이 체인으로 이어진다. 그 안의 상자 둘은 같은 체인의 맨 앞과 맨 끝이다. 구멍을 넣기 전에는 맨 앞이 비어 있어 packet 이 ct state established,related accept 에 걸리지 못한 채 체인 끝 reject 에 닿았고 connection refused 로 끝났다. 그 reject 규칙의 카운터가 4 패킷 240 바이트다. insert 로 구멍을 넣으면 그 규칙이 맨 앞에 서서 같은 packet 을 먼저 받아 192.168.122.10 의 엣지 nginx 로 보낸다. 실선이 구멍을 넣은 뒤의 경로이고 점선이 넣기 전의 경로다. 이 그림은 그 규칙이 libvirt 네트워크를 다시 세우면 사라진다는 것은 말하지 않는다.",
"source_context": {
"document": "docs/virtualization/final/document.md",
"document_sha256": "60d902c7aed218ba637b48603a3bd0e6b193fea59c9de97ffdb8e05d5087aedd",
"anchor": {
"kind": "heading",
"value": "180. nftables 는 앞 체인의 `accept` 로 뒤 체인의 `reject` 를 막지 못한다",
"line": 7805
}
},
"composition": {
"profile": "component-flow",
"diagram_only": true,
"reference_ids": [
"payment-event-flow"
],
"rationale": "이 절이 세운 것은 방향이 있는 경로 하나다 — 밖에서 온 packet 이 base 체인 둘을 우선순위 순으로 지나 두 결말 가운데 하나에 닿는다. 두 체인은 소유가 다른 실제 containment 라 groups 로 둘렀다. comparison 은 관계선을 지워도 뜻이 남는 자리의 문법인데 여기서는 지우면 「앞 체인의 accept 를 지나고도 뒤 체인에 닿는다」가 통째로 사라져 규칙 목록만 남는다. sequence 는 주고받는 참가자가 둘 이상일 때의 문법이고 여기서 움직이는 것은 packet 하나뿐이다.",
"focus_node": "tail-reject"
},
"groups": [
{
"id": "our-forward-chain",
"label": "forward · priority filter - 10",
"kind": "system",
"role": "zone",
"description": "DNAT 를 정의한 파일에 둔 base 체인. 같은 훅에서 libvirt 체인보다 먼저 돈다.",
"evidence": [
{
"start_line": 7829,
"end_line": 7830
}
],
"assumption": false
},
{
"id": "libvirt-guest-input",
"label": "ip libvirt_network · guest_input",
"kind": "system",
"role": "zone",
"description": "libvirt 가 자기 테이블 안에 만드는 base 체인. 같은 훅에서 뒤에 돈다.",
"evidence": [
{
"start_line": 7818,
"end_line": 7824
}
],
"assumption": false
}
],
"nodes": [
{
"id": "inbound-packet",
"label": "inbound packet",
"kind": "packet",
"role": "source",
"shape": "box",
"details": [
"curl http://100.83.212.4"
],
"description": "밖에서 친 curl 이 만든 packet. 호스트가 직접 듣는 리스너가 없어 FORWARD 로 간다.",
"evidence": [
{
"start_line": 7814,
"end_line": 7814
},
{
"start_line": 7809,
"end_line": 7809
}
],
"assumption": false
},
{
"id": "our-accept",
"label": "ct state new accept",
"kind": "rule",
"role": "service",
"shape": "box",
"group": "our-forward-chain",
"description": "우리가 먼저 돌게 해 둔 체인의 규칙. 이 체인은 통과시키지만 평가를 끝내지는 않는다.",
"evidence": [
{
"start_line": 7829,
"end_line": 7833
}
],
"assumption": false
},
{
"id": "inserted-accept",
"label": "inserted accept",
"kind": "rule",
"role": "service",
"shape": "box",
"group": "libvirt-guest-input",
"emphasis": "primary",
"details": [
"nft insert rule",
"daddr 192.168.122.10",
"tcp dport {80,443}"
],
"description": "insert 로 guest_input 맨 앞에 넣은 구멍. add 로 넣으면 맨 뒤라 reject 뒤에 선다.",
"evidence": [
{
"start_line": 7835,
"end_line": 7840
}
],
"assumption": false
},
{
"id": "tail-reject",
"label": "reject",
"kind": "rule",
"role": "service",
"shape": "box",
"group": "libvirt-guest-input",
"emphasis": "primary",
"details": [
"chain tail",
"counter packets 4",
"bytes 240"
],
"description": "guest_input 체인을 끝내는 규칙. 앞의 ct state established,related accept 에 걸리지 못한 packet 이 여기 닿는다. 이 카운터가 밖에서 친 curl 횟수와 일치해 범인 확정에 쓰였다.",
"evidence": [
{
"start_line": 7821,
"end_line": 7823
},
{
"start_line": 7826,
"end_line": 7827
}
],
"assumption": false
},
{
"id": "edge-nginx",
"label": "edge nginx",
"kind": "service",
"role": "sink",
"shape": "box",
"details": [
"192.168.122.10"
],
"description": "게스트에서 도는 엣지. 호스트에서 친 요청에는 이미 404 로 응답하고 있었다.",
"evidence": [
{
"start_line": 7813,
"end_line": 7813
},
{
"start_line": 7839,
"end_line": 7840
}
],
"assumption": false
},
{
"id": "connection-refused",
"label": "connection refused",
"kind": "result",
"role": "sink",
"shape": "box",
"description": "drop 이 아니라 reject 라 기다리지 않고 즉시 돌아온 결과.",
"evidence": [
{
"start_line": 7814,
"end_line": 7814
},
{
"start_line": 7816,
"end_line": 7816
}
],
"assumption": false
}
],
"edges": [
{
"id": "packet-to-our-chain",
"from": "inbound-packet",
"to": "our-accept",
"label": "forward hook",
"kind": "data",
"style": "solid",
"evidence": [
{
"start_line": 7814,
"end_line": 7814
},
{
"start_line": 7829,
"end_line": 7830
}
],
"assumption": false
},
{
"id": "our-chain-after-insert",
"from": "our-accept",
"to": "inserted-accept",
"label": "after insert",
"kind": "data",
"style": "solid",
"evidence": [
{
"start_line": 7831,
"end_line": 7833
},
{
"start_line": 7835,
"end_line": 7836
}
],
"assumption": false
},
{
"id": "our-chain-before-insert",
"from": "our-accept",
"to": "tail-reject",
"label": "before insert",
"kind": "data",
"style": "dashed",
"evidence": [
{
"start_line": 7831,
"end_line": 7833
},
{
"start_line": 7821,
"end_line": 7823
}
],
"assumption": false
},
{
"id": "inserted-to-edge",
"from": "inserted-accept",
"to": "edge-nginx",
"label": "accept",
"kind": "result",
"style": "solid",
"evidence": [
{
"start_line": 7835,
"end_line": 7840
}
],
"assumption": false
},
{
"id": "reject-to-refused",
"from": "tail-reject",
"to": "connection-refused",
"label": "reject",
"kind": "result",
"style": "dashed",
"evidence": [
{
"start_line": 7823,
"end_line": 7823
},
{
"start_line": 7814,
"end_line": 7816
}
],
"assumption": false
}
],
"legend": [],
"metadata": {
"rationale": "network 으로 고른 이유는 이 절이 답하는 물음이 「packet 하나가 어느 규칙을 어떤 순서로 지나는가」이기 때문이다. 상자는 체인이 아니라 규칙 하나씩이다 — 체인을 상자로 두면 「앞 체인 accept 다음에 뒤 체인」이라는 자리 관계가 안 보이고, 그 자리가 이 사건의 전부다. 실선은 구멍을 넣은 뒤의 경로이고 점선은 넣기 전의 경로다. 색이 아니라 선 모양으로 갈랐다. guest_input 의 ct state established,related accept 는 상자로 세우지 않았다 — 세우면 그 체인 그룹이 두 칸을 차지하고 bbox 가 그룹 밖인 엣지 nginx 를 삼켜 엣지가 libvirt 체인 안에 있는 것처럼 보였다(렌더 확인). 그 규칙의 원문은 기록 본문 코드블록에 그대로 있다. ExecStartPost 로 규칙을 되살리는 수명은 시간 축이라 본문에 두었다."
}
}
@@ -0,0 +1,28 @@
# 같은 forward 훅에 붙은 base 체인 둘이 우선순위 순으로 이어서 평가되고, 앞 체인의 accept 는 뒤 체인의 reject 를 막지 못한다
## Alternative text
밖에서 온 packet 이 forward 훅에 붙은 base 체인 둘을 우선순위 순으로 지나는 경로도. 앞 체인의 accept 뒤에 libvirt guest_input 이 이어지고, 구멍을 넣기 전 경로는 그 체인 끝 reject 로, 넣은 뒤 경로는 체인 맨 앞의 구멍을 지나 엣지 nginx 로 갈라진다.
## Long description
왼쪽에서 오른쪽으로 읽는다. 왼쪽 끝이 밖에서 친 curl 이 만든 inbound packet 이고 forward 훅으로 들어간다. 첫 점선 상자가 DNAT 파일에 둔 base 체인이다. priority filter - 10 이라 먼저 돌고, 그 안의 ct state new accept 가 이 packet 을 통과시킨다. 다음 점선 상자가 libvirt 가 만든 ip libvirt_network 테이블의 guest_input 체인이다. 앞 체인의 accept 는 평가를 끝내지 않으므로 같은 packet 이 이 체인으로 이어진다. 그 안의 상자 둘은 같은 체인의 맨 앞과 맨 끝이다. 구멍을 넣기 전에는 맨 앞이 비어 있어 packet 이 ct state established,related accept 에 걸리지 못한 채 체인 끝 reject 에 닿았고 connection refused 로 끝났다. 그 reject 규칙의 카운터가 4 패킷 240 바이트다. insert 로 구멍을 넣으면 그 규칙이 맨 앞에 서서 같은 packet 을 먼저 받아 192.168.122.10 의 엣지 nginx 로 보낸다. 실선이 구멍을 넣은 뒤의 경로이고 점선이 넣기 전의 경로다. 이 그림은 그 규칙이 libvirt 네트워크를 다시 세우면 사라진다는 것은 말하지 않는다.
## Elements and evidence
- **Boundary: forward · priority filter - 10** (system): DNAT 를 정의한 파일에 둔 base 체인. 같은 훅에서 libvirt 체인보다 먼저 돈다. Evidence: L7829L7830.
- **Boundary: ip libvirt_network · guest_input** (system): libvirt 가 자기 테이블 안에 만드는 base 체인. 같은 훅에서 뒤에 돈다. Evidence: L7818L7824.
- **inbound packet** (packet): 밖에서 친 curl 이 만든 packet. 호스트가 직접 듣는 리스너가 없어 FORWARD 로 간다. Evidence: L7814L7814, L7809L7809.
- **ct state new accept** (rule): 우리가 먼저 돌게 해 둔 체인의 규칙. 이 체인은 통과시키지만 평가를 끝내지는 않는다. Evidence: L7829L7833.
- **inserted accept** (rule): insert 로 guest_input 맨 앞에 넣은 구멍. add 로 넣으면 맨 뒤라 reject 뒤에 선다. Evidence: L7835L7840.
- **reject** (rule): guest_input 체인을 끝내는 규칙. 앞의 ct state established,related accept 에 걸리지 못한 packet 이 여기 닿는다. 이 카운터가 밖에서 친 curl 횟수와 일치해 범인 확정에 쓰였다. Evidence: L7821L7823, L7826L7827.
- **edge nginx** (service): 게스트에서 도는 엣지. 호스트에서 친 요청에는 이미 404 로 응답하고 있었다. Evidence: L7813L7813, L7839L7840.
- **connection refused** (result): drop 이 아니라 reject 라 기다리지 않고 즉시 돌아온 결과. Evidence: L7814L7814, L7816L7816.
## Relationships
- **inserted accept → edge nginx:** accept. Evidence: L7835L7840.
- **ct state new accept → inserted accept:** after insert. Evidence: L7831L7833, L7835L7836.
- **ct state new accept → reject:** before insert. Evidence: L7831L7833, L7821L7823.
- **inbound packet → ct state new accept:** forward hook. Evidence: L7814L7814, L7829L7830.
- **reject → connection refused:** reject. Evidence: L7823L7823, L7814L7816.
@@ -0,0 +1,30 @@
# 같은 forward 훅에 붙은 base 체인 둘이 우선순위 순으로 이어서 평가되고, 앞 체인의 accept 는 뒤 체인의 reject 를 막지 못한다
# Question: 밖에서 온 packet 하나가 forward 훅에서 어느 규칙을 어떤 순서로 지나 어디서 끝났고, insert 로 넣은 구멍은 그 순서의 어디에 들어가는가
direction: right
g0: "forward · priority filter - 10" {
n1: "ct state new accept" {
shape: rectangle
}
}
g1: "ip libvirt_network · guest_input" {
n2: "inserted accept" {
shape: rectangle
}
n3: "reject" {
shape: rectangle
}
}
n0: "inbound packet" {
shape: rectangle
}
n4: "edge nginx" {
shape: rectangle
}
n5: "connection refused" {
shape: rectangle
}
n0 -> g0.n1: "forward hook"
g0.n1 -> g1.n2: "after insert"
g0.n1 -> g1.n3: "before insert"
g1.n2 -> n4: "accept"
g1.n3 -> n5: "reject"
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="nftables-forward-hook-chain-order" name="같은 forward 훅에 붙은 base 체인 둘이 우선순위 순으로 이어서 평가되고, 앞 체인의 accept 는 뒤 체인의 reject 를 막지 못한다">
<mxGraphModel dx="1298" dy="434" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1298" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="g_our-forward-chain" value="forward · priority filter - 10" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="402.0" y="144.0" width="227.0" height="136.0" as="geometry"/>
</mxCell>
<mxCell id="g_libvirt-guest-input" value="ip libvirt_network · guest_input" style="swimlane;html=1;rounded=1;startSize=30;horizontal=1;dashed=1;strokeWidth=1.5;fillColor=#f7f9fb;strokeColor=#66788a;fontStyle=1;fontSize=13;" vertex="1" parent="1">
<mxGeometry x="729.0" y="35.0" width="234.0" height="354.0" as="geometry"/>
</mxCell>
<mxCell id="n_inbound-packet" value="inbound packet&lt;br/&gt;curl http://100.83.212.4" tooltip="밖에서 친 curl 이 만든 packet. 호스트가 직접 듣는 리스너가 없어 FORWARD 로 간다. | Evidence: L7814-L7814, L7809-L7809" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="70.0" y="186.5" width="202.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_our-accept" value="ct state new accept" tooltip="우리가 먼저 돌게 해 둔 체인의 규칙. 이 체인은 통과시키지만 평가를 끝내지는 않는다. | Evidence: L7829-L7833" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="432.0" y="190.0" width="167.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="n_inserted-accept" value="inserted accept&lt;br/&gt;nft insert rule&lt;br/&gt;daddr 192.168.122.10&lt;br/&gt;tcp dport {80,443}" tooltip="insert 로 guest_input 맨 앞에 넣은 구멍. add 로 넣으면 맨 뒤라 reject 뒤에 선다. | Evidence: L7835-L7840" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="759.0" y="81.0" width="174.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_tail-reject" value="reject&lt;br/&gt;chain tail&lt;br/&gt;counter packets 4&lt;br/&gt;bytes 240" tooltip="guest_input 체인을 끝내는 규칙. 앞의 ct state established,related accept 에 걸리지 못한 packet 이 여기 닿는다. 이 카운터가 밖에서 친 curl 횟수와 일치해 범인 확정에 쓰였다. | Evidence: L7821-L7823, L7826-L7827" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="769.5" y="258.0" width="153.0" height="105.0" as="geometry"/>
</mxCell>
<mxCell id="n_edge-nginx" value="edge nginx&lt;br/&gt;192.168.122.10" tooltip="게스트에서 도는 엣지. 호스트에서 친 요청에는 이미 404 로 응답하고 있었다. | Evidence: L7813-L7813, L7839-L7840" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1098.0" y="118.5" width="150.0" height="71.0" as="geometry"/>
</mxCell>
<mxCell id="n_connection-refused" value="connection refused" tooltip="drop 이 아니라 reject 라 기다리지 않고 즉시 돌아온 결과. | Evidence: L7814-L7814, L7816-L7816" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="1093.0" y="261.5" width="160.0" height="64.0" as="geometry"/>
</mxCell>
<mxCell id="e_inserted-to-edge" value="accept" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_inserted-accept" target="n_edge-nginx">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1039.5" y="143.8" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_our-chain-after-insert" value="after insert" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_our-accept" target="n_inserted-accept">
<mxGeometry relative="1" as="geometry">
<mxPoint x="703.0" y="173.2" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_our-chain-before-insert" value="before insert" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_our-accept" target="n_tail-reject">
<mxGeometry relative="1" as="geometry">
<mxPoint x="708.2" y="270.8" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_packet-to-our-chain" value="forward hook" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_inbound-packet" target="n_our-accept">
<mxGeometry relative="1" as="geometry">
<mxPoint x="352.0" y="194.0" as="offset"/>
</mxGeometry>
</mxCell>
<mxCell id="e_reject-to-refused" value="reject" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;strokeWidth=2;endArrow=block;endFill=1;" edge="1" parent="1" source="n_tail-reject" target="n_connection-refused">
<mxGeometry relative="1" as="geometry">
<mxPoint x="1031.8" y="302.0" as="offset"/>
</mxGeometry>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,31 @@
{
"harness_version": "0.2.0",
"spec_id": "nftables-forward-hook-chain-order",
"spec_version": "1.1",
"spec_sha256": "7c412e3a6278b75551ce56f35693dd236d0b3bf0962c2788a87703123f009f9f",
"source_context": {
"document": "docs/virtualization/final/document.md",
"document_sha256": "60d902c7aed218ba637b48603a3bd0e6b193fea59c9de97ffdb8e05d5087aedd",
"anchor": {
"kind": "heading",
"value": "180. nftables 는 앞 체인의 `accept` 로 뒤 체인의 `reject` 를 막지 못한다",
"line": 7805
}
},
"outputs": [
"nftables-forward-hook-chain-order.svg",
"nftables-forward-hook-chain-order.drawio",
"nftables-forward-hook-chain-order.mmd",
"nftables-forward-hook-chain-order.d2",
"nftables-forward-hook-chain-order.excalidraw",
"nftables-forward-hook-chain-order.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "component-flow",
"reference_ids": [
"payment-event-flow"
],
"diagram_only": true
}
@@ -0,0 +1,18 @@
%% 같은 forward 훅에 붙은 base 체인 둘이 우선순위 순으로 이어서 평가되고, 앞 체인의 accept 는 뒤 체인의 reject 를 막지 못한다
%% question: 밖에서 온 packet 하나가 forward 훅에서 어느 규칙을 어떤 순서로 지나 어디서 끝났고, insert 로 넣은 구멍은 그 순서의 어디에 들어가는가
flowchart LR
subgraph g_our_forward_chain["forward · priority filter - 10"]
n1["ct state new accept"]
end
subgraph g_libvirt_guest_input["ip libvirt_network · guest_input"]
n2["inserted accept"]
n3["reject"]
end
n0["inbound packet"]
n4["edge nginx"]
n5["connection refused"]
n0 -->|"forward hook"| n1
n1 -->|"after insert"| n2
n1 -->|"before insert"| n3
n2 -->|"accept"| n4
n3 -->|"reject"| n5
@@ -0,0 +1,110 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1298" height="434" viewBox="0 0 1298 434" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">같은 forward 훅에 붙은 base 체인 둘이 우선순위 순으로 이어서 평가되고, 앞 체인의 accept 는 뒤 체인의 reject 를 막지 못한다</title>
<desc id="diagram-description">왼쪽에서 오른쪽으로 읽는다. 왼쪽 끝이 밖에서 친 curl 이 만든 inbound packet 이고 forward 훅으로 들어간다. 첫 점선 상자가 DNAT 파일에 둔 base 체인이다. priority filter - 10 이라 먼저 돌고, 그 안의 ct state new accept 가 이 packet 을 통과시킨다. 다음 점선 상자가 libvirt 가 만든 ip libvirt_network 테이블의 guest_input 체인이다. 앞 체인의 accept 는 평가를 끝내지 않으므로 같은 packet 이 이 체인으로 이어진다. 그 안의 상자 둘은 같은 체인의 맨 앞과 맨 끝이다. 구멍을 넣기 전에는 맨 앞이 비어 있어 packet 이 ct state established,related accept 에 걸리지 못한 채 체인 끝 reject 에 닿았고 connection refused 로 끝났다. 그 reject 규칙의 카운터가 4 패킷 240 바이트다. insert 로 구멍을 넣으면 그 규칙이 맨 앞에 서서 같은 packet 을 먼저 받아 192.168.122.10 의 엣지 nginx 로 보낸다. 실선이 구멍을 넣은 뒤의 경로이고 점선이 넣기 전의 경로다. 이 그림은 그 규칙이 libvirt 네트워크를 다시 세우면 사라진다는 것은 말하지 않는다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;nftables-forward-hook-chain-order&quot;,&quot;profile&quot;:&quot;component-flow&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;docs/virtualization/final/document.md&quot;,&quot;document_sha256&quot;:&quot;60d902c7aed218ba637b48603a3bd0e6b193fea59c9de97ffdb8e05d5087aedd&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;heading&quot;,&quot;value&quot;:&quot;180. nftables 는 앞 체인의 `accept` 로 뒤 체인의 `reject` 를 막지 못한다&quot;,&quot;line&quot;:7805}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="1298" height="434" />
<rect class="group-box" x="402.0" y="144.0" width="227.0" height="136.0" rx="8" />
<rect class="group-label-bg" x="416.0" y="134.0" width="232.0" height="22" />
<text class="group-label" x="426.0" y="149.0">forward · priority filter - 10</text>
<rect class="group-box" x="729.0" y="35.0" width="234.0" height="354.0" rx="8" />
<rect class="group-label-bg" x="743.0" y="25.0" width="246.0" height="22" />
<text class="group-label" x="753.0" y="40.0">ip libvirt_network · guest_input</text>
<polyline class="edge kind-result style-solid emphasis-normal" points="933.0,133.5 1015.5,133.5 1015.5,154.0 1098.0,154.0" data-evidence="7835-7840" />
<rect class="edge-label-bg" x="1010.4" y="129.8" width="58.2" height="22" rx="3" />
<text class="edge-label" x="1039.5" y="144.8">accept</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="599.0,213.0 679.0,213.0 679.0,133.5 759.0,133.5" data-evidence="7831-7833,7835-7836" />
<rect class="edge-label-bg" x="653.8" y="159.2" width="98.4" height="22" rx="3" />
<text class="edge-label" x="703.0" y="174.2">after insert</text>
<polyline class="edge kind-data style-dashed emphasis-normal" points="599.0,231.0 684.2,231.0 684.2,310.5 769.5,310.5" data-evidence="7831-7833,7821-7823" />
<rect class="edge-label-bg" x="655.7" y="256.8" width="105.1" height="22" rx="3" />
<text class="edge-label" x="708.2" y="271.8">before insert</text>
<polyline class="edge kind-data style-solid emphasis-normal" points="272.0,222.0 352.0,222.0 352.0,222.0 432.0,222.0" data-evidence="7814-7814,7829-7830" />
<rect class="edge-label-bg" x="302.8" y="180.0" width="98.4" height="22" rx="3" />
<text class="edge-label" x="352.0" y="195.0">forward hook</text>
<polyline class="edge kind-result style-dashed emphasis-normal" points="922.5,310.5 1007.8,310.5 1007.8,293.5 1093.0,293.5" data-evidence="7823-7823,7814-7816" />
<rect class="edge-label-bg" x="1002.6" y="288.0" width="58.2" height="22" rx="3" />
<text class="edge-label" x="1031.8" y="303.0">reject</text>
<g id="node-inbound-packet">
<rect class="node-shape kind-packet emphasis-normal role-source" data-evidence="7814-7814,7809-7809" x="70.0" y="186.5" width="202.0" height="71.0" rx="7" />
<text class="node-label" x="171.0" y="213.5">inbound packet</text>
<line class="node-detail-divider" x1="84.0" y1="234.5" x2="258.0" y2="234.5" />
<text class="node-detail" x="86.0" y="251.5">curl http://100.83.212.4</text>
</g>
<g id="node-our-accept">
<rect class="node-shape kind-rule emphasis-normal role-service" data-evidence="7829-7833" x="432.0" y="190.0" width="167.0" height="64.0" rx="7" />
<text class="node-label" x="515.5" y="220.0">ct state new accept</text>
</g>
<g id="node-inserted-accept">
<rect class="node-shape kind-rule emphasis-primary role-service" data-evidence="7835-7840" x="759.0" y="81.0" width="174.0" height="105.0" rx="7" />
<text class="node-label" x="846.0" y="108.0">inserted accept</text>
<line class="node-detail-divider" x1="773.0" y1="129.0" x2="919.0" y2="129.0" />
<text class="node-detail" x="775.0" y="146.0">nft insert rule</text>
<text class="node-detail" x="775.0" y="162.0">daddr 192.168.122.10</text>
<text class="node-detail" x="775.0" y="178.0">tcp dport {80,443}</text>
</g>
<g id="node-tail-reject">
<rect class="node-shape kind-rule emphasis-primary role-service" data-evidence="7821-7823,7826-7827" x="769.5" y="258.0" width="153.0" height="105.0" rx="7" />
<text class="node-label" x="846.0" y="285.0">reject</text>
<line class="node-detail-divider" x1="783.5" y1="306.0" x2="908.5" y2="306.0" />
<text class="node-detail" x="785.5" y="323.0">chain tail</text>
<text class="node-detail" x="785.5" y="339.0">counter packets 4</text>
<text class="node-detail" x="785.5" y="355.0">bytes 240</text>
</g>
<g id="node-edge-nginx">
<rect class="node-shape kind-service emphasis-normal role-sink" data-evidence="7813-7813,7839-7840" x="1098.0" y="118.5" width="150.0" height="71.0" rx="7" />
<text class="node-label" x="1173.0" y="145.5">edge nginx</text>
<line class="node-detail-divider" x1="1112.0" y1="166.5" x2="1234.0" y2="166.5" />
<text class="node-detail" x="1114.0" y="183.5">192.168.122.10</text>
</g>
<g id="node-connection-refused">
<rect class="node-shape kind-result emphasis-normal role-sink" data-evidence="7814-7814,7816-7816" x="1093.0" y="261.5" width="160.0" height="64.0" rx="7" />
<text class="node-label" x="1173.0" y="291.5">connection refused</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.7 KiB

File diff suppressed because it is too large Load Diff