telcoladder 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- telcoladder/__init__.py +8 -0
- telcoladder/__main__.py +6 -0
- telcoladder/adapters/__init__.py +311 -0
- telcoladder/adapters/carrier.py +103 -0
- telcoladder/adapters/diameter.py +368 -0
- telcoladder/adapters/gtp.py +103 -0
- telcoladder/adapters/gtpv2.py +341 -0
- telcoladder/adapters/nas5gs.py +320 -0
- telcoladder/adapters/naseps.py +282 -0
- telcoladder/adapters/ngap.py +275 -0
- telcoladder/adapters/pfcp.py +208 -0
- telcoladder/adapters/s1ap.py +295 -0
- telcoladder/adapters/sbi.py +422 -0
- telcoladder/adapters/sip.py +244 -0
- telcoladder/callflow.py +242 -0
- telcoladder/causes.py +199 -0
- telcoladder/chrome.py +138 -0
- telcoladder/cli.py +400 -0
- telcoladder/correlate.py +98 -0
- telcoladder/coverage.py +344 -0
- telcoladder/data/causes/diameter_3gpp.yaml +313 -0
- telcoladder/data/causes/diameter_base.yaml +216 -0
- telcoladder/data/causes/nas_5gmm.yaml +349 -0
- telcoladder/data/causes/nas_5gsm.yaml +257 -0
- telcoladder/data/causes/ngap_misc.yaml +54 -0
- telcoladder/data/causes/ngap_nas.yaml +44 -0
- telcoladder/data/causes/ngap_protocol.yaml +67 -0
- telcoladder/data/causes/ngap_radioNetwork.yaml +276 -0
- telcoladder/data/causes/ngap_transport.yaml +24 -0
- telcoladder/data/decode-as.yaml +30 -0
- telcoladder/decode.py +334 -0
- telcoladder/decodeas.py +291 -0
- telcoladder/extract.py +347 -0
- telcoladder/flowtable.py +509 -0
- telcoladder/framebytes.py +141 -0
- telcoladder/i18n.py +156 -0
- telcoladder/identities.py +342 -0
- telcoladder/identity.py +190 -0
- telcoladder/interfaces.py +92 -0
- telcoladder/lifecycle.py +155 -0
- telcoladder/mcp.py +487 -0
- telcoladder/model.py +331 -0
- telcoladder/nf.py +383 -0
- telcoladder/packets.py +406 -0
- telcoladder/pdusession.py +232 -0
- telcoladder/pipeline.py +488 -0
- telcoladder/plugins.py +86 -0
- telcoladder/prefilter.py +323 -0
- telcoladder/probe.py +214 -0
- telcoladder/procedures.py +412 -0
- telcoladder/render_mermaid.py +152 -0
- telcoladder/session.py +609 -0
- telcoladder/slicer.py +150 -0
- telcoladder/static/app.css +1 -0
- telcoladder/static/app.js +240 -0
- telcoladder/static/theme.js +85 -0
- telcoladder/summary.py +460 -0
- telcoladder/translations/__init__.py +6 -0
- telcoladder/translations/zh_tw.py +382 -0
- telcoladder/tshark.py +198 -0
- telcoladder/viewer.py +550 -0
- telcoladder/web.py +943 -0
- telcoladder/wireview.py +93 -0
- telcoladder/xdr.py +105 -0
- telcoladder-0.1.0.dist-info/METADATA +561 -0
- telcoladder-0.1.0.dist-info/RECORD +71 -0
- telcoladder-0.1.0.dist-info/WHEEL +5 -0
- telcoladder-0.1.0.dist-info/entry_points.txt +2 -0
- telcoladder-0.1.0.dist-info/licenses/LICENSE +202 -0
- telcoladder-0.1.0.dist-info/licenses/NOTICE +89 -0
- telcoladder-0.1.0.dist-info/top_level.txt +1 -0
telcoladder/__init__.py
ADDED
telcoladder/__main__.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""協定 adapter 註冊表 —— 內建的加上外掛提供的。
|
|
2
|
+
|
|
3
|
+
## 契約
|
|
4
|
+
|
|
5
|
+
一個 adapter 是一個模組(或任何有這些屬性的物件),必須提供五樣東西:
|
|
6
|
+
|
|
7
|
+
| 屬性 | 用途 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `NAME` | 出現在 `Message.protocol` 上,如 `"nas-5gs"` |
|
|
10
|
+
| `ORDER` | adapter 之間的排列順序,小的先跑。**有語意**,見下 |
|
|
11
|
+
| `DISPLAY_FILTER` | 丟給 tshark 的 filter 片段,如 `"sip"` |
|
|
12
|
+
| `DISSECTORS` | `telcoladder check` 要驗證存在的 dissector 名稱 |
|
|
13
|
+
| `parse(frame)` | `Frame` → `list[Message]` |
|
|
14
|
+
|
|
15
|
+
外加三個**選用**的:
|
|
16
|
+
|
|
17
|
+
| 屬性 | 用途 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `DECODE_AS` | tshark `-d` 規則,如 `("tcp.port==7777,http2",)` |
|
|
20
|
+
| `CARRIES` | 這個 adapter 會載送哪些協定,如 `("nas-5gs",)` |
|
|
21
|
+
| `CARRIER_LAYER` | 它的區塊在 tshark 輸出裡叫什麼層;**預設等於 `NAME`** |
|
|
22
|
+
| `carrier_keys(block, frame)` | 從**載體區塊**推出的身分鍵,回 `frozenset[IdKey]` |
|
|
23
|
+
| `blind_spots(frame)` | 這一格裡**我看得到卻讀不出來**的東西,見下 |
|
|
24
|
+
|
|
25
|
+
`CARRIER_LAYER` 存在是因為 **adapter 的名字與 tshark 的層名是兩回事**:
|
|
26
|
+
`sbi.NAME` 是 `"sbi"`(會出現在 `Message.protocol` 上),但它的區塊在 `-T ek`
|
|
27
|
+
輸出裡叫 `http2`。NGAP 剛好兩者同名,所以在只有一個載體的年代看不出來 ——
|
|
28
|
+
實作 T1 時 SBI 那條路一格都收不到,而且**不報錯**,才把這個缺口逼出來。
|
|
29
|
+
|
|
30
|
+
## CARRIES / carrier_keys:為什麼身分推導要進契約
|
|
31
|
+
|
|
32
|
+
`-T ek` 的子解剖是**巢狀在載體層內**的,所以載荷 adapter 必須知道去誰底下找 ——
|
|
33
|
+
`nas-5gs` 掛在 `ngap` 底下,也掛在 `http2.mime_multipart` 底下。
|
|
34
|
+
|
|
35
|
+
光知道去哪找還不夠。載荷自己的欄位通常**不足以歸戶**:NGAP 載送時 UE 的身分在
|
|
36
|
+
NGAP 的 UE ID 上,SBI 載送時在 HTTP/2 的 stream id 與同層的 IMSI 上。所以載荷
|
|
37
|
+
必須**問載體要鑰匙**,而不是自己猜。
|
|
38
|
+
|
|
39
|
+
在這之前 `nas5gs.py` 是直接 `from telcoladder.adapters.ngap import identity_keys` ——
|
|
40
|
+
一條硬編碼。多一個載體就多一條,Phase 2 接 SIP(載送 SDP)與 Diameter(載送 AVP)
|
|
41
|
+
之後會變成四條。改成契約屬性之後,載荷只要問 `carriers_of()`。
|
|
42
|
+
|
|
43
|
+
兩者皆選用、且比照 `DECODE_AS` 用 `getattr` 取用 —— 既有外掛不必改版。
|
|
44
|
+
沒宣告 `CARRIES` 的 adapter 就不是任何東西的載體,行為完全不變。
|
|
45
|
+
|
|
46
|
+
`DISPLAY_FILTER` 是最容易漏的一個:adapter 寫得再完美,只要它的協定不在
|
|
47
|
+
filter 裡,tshark 根本不會把那些封包吐出來 —— 而症狀是「圖比較短」,
|
|
48
|
+
不是報錯。
|
|
49
|
+
|
|
50
|
+
## DECODE_AS:光有 filter 不夠
|
|
51
|
+
|
|
52
|
+
filter 是「把這個協定的封包留下來」,前提是 tshark **已經認出**那是什麼協定。
|
|
53
|
+
擷取起點若在 TCP 連線建立之後,tshark 看不到 HTTP/2 的 preface,整條連線會
|
|
54
|
+
退回 `data` —— 這時 `DISPLAY_FILTER = "http2"` 一格都收不到,**而且不報錯**。
|
|
55
|
+
(實測:一份含 140 格 SBI 的 5GC 擷取檔,不指定 decode-as 時全部退回 `data`。)
|
|
56
|
+
|
|
57
|
+
所以宣告了 `DISPLAY_FILTER` 還不夠,跑在非標準 port 上的協定要一併宣告
|
|
58
|
+
`DECODE_AS`。IMS 會更常遇到:SIP 跑 5062 / 6060、Diameter 被改 port 都是常態。
|
|
59
|
+
|
|
60
|
+
**這些 port 是啟發式提示,不是規範值。** 與 `nf.py` 裡的 38412(TS 38.412)、
|
|
61
|
+
8805(TS 29.244)不同 —— 那兩個規範定死,而 SBI 的 port 是 NRF discovery 給的,
|
|
62
|
+
7777 只是 Open5GS 的預設。所以 `DECODE_AS` 只是「常見情況能開箱即用」,
|
|
63
|
+
其他部署一律用 CLI 的 `--decode-as` 疊加。
|
|
64
|
+
|
|
65
|
+
選用而非必填是刻意的:多數協定跑在標準 port 上,不該為了一個用不到的欄位
|
|
66
|
+
逼所有既有外掛改版。沒宣告就當空的。
|
|
67
|
+
|
|
68
|
+
## ORDER 為什麼是數字而不是清單位置
|
|
69
|
+
|
|
70
|
+
原本 `ADAPTERS` 是一個手寫的 tuple,順序藏在位置裡。加入外掛之後那個位置
|
|
71
|
+
不再看得見,所以順序必須由 adapter 自己宣告:**載體協定要排在載荷之前**
|
|
72
|
+
(NGAP 內嵌 NAS,同一格裡先畫 InitialUEMessage 再畫 Registration request
|
|
73
|
+
才讀得通)。內建的用 10 / 20 / 30,留下空隙讓外掛插得進來。
|
|
74
|
+
|
|
75
|
+
## 內建的為什麼不走 entry point
|
|
76
|
+
|
|
77
|
+
純粹是韌性:entry point 要靠套件 metadata,而 metadata 在「直接從原始碼跑」
|
|
78
|
+
或安裝損壞時可能讀不到。內建協定不是選用功能,不該有消失的可能。
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
from __future__ import annotations
|
|
82
|
+
|
|
83
|
+
from collections.abc import Iterable
|
|
84
|
+
from functools import cache
|
|
85
|
+
from typing import Protocol
|
|
86
|
+
|
|
87
|
+
from typing import Any
|
|
88
|
+
|
|
89
|
+
from telcoladder.i18n import _
|
|
90
|
+
from telcoladder.extract import Frame
|
|
91
|
+
from telcoladder.model import (
|
|
92
|
+
BLIND_CIPHERED_NAS,
|
|
93
|
+
BLIND_ECIES_PROTECTED_SUCI,
|
|
94
|
+
BLIND_UNDECODED_STREAM,
|
|
95
|
+
BlindSpot,
|
|
96
|
+
IdKey,
|
|
97
|
+
Message,
|
|
98
|
+
)
|
|
99
|
+
from telcoladder.plugins import ADAPTER_GROUP, PluginError, load_group
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class Adapter(Protocol):
|
|
103
|
+
NAME: str
|
|
104
|
+
ORDER: int
|
|
105
|
+
DISPLAY_FILTER: str
|
|
106
|
+
DISSECTORS: tuple[str, ...]
|
|
107
|
+
#: 選用。沒宣告的 adapter 一律當空的,見 `default_decode_as()`。
|
|
108
|
+
DECODE_AS: tuple[str, ...]
|
|
109
|
+
#: 選用。這個 adapter 會載送哪些協定的 `NAME`,見 `carriers_of()`。
|
|
110
|
+
CARRIES: tuple[str, ...]
|
|
111
|
+
#: 選用。這個 adapter 的區塊在 tshark 輸出裡的層名。預設等於 `NAME`
|
|
112
|
+
#: —— 只有兩者不同的 adapter 需要宣告(例如 sbi 的層是 `http2`)。
|
|
113
|
+
CARRIER_LAYER: str
|
|
114
|
+
|
|
115
|
+
def parse(self, frame: Frame) -> list[Message]: ...
|
|
116
|
+
|
|
117
|
+
#: 選用。從載體區塊推出的身分鍵。載荷 adapter 靠它歸戶,
|
|
118
|
+
#: 因為載荷自己的欄位通常不足以識別是誰。
|
|
119
|
+
def carrier_keys(self, block: dict[str, Any], frame: Frame) -> frozenset[IdKey]: ...
|
|
120
|
+
|
|
121
|
+
#: 選用。這一格裡「看得到協定層、但讀不出內容」的東西。見 `blind_spots()`。
|
|
122
|
+
def blind_spots(self, frame: Frame) -> Iterable[BlindSpot]: ...
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
from telcoladder.adapters import ( # noqa: E402
|
|
126
|
+
diameter, gtp, gtpv2, nas5gs, naseps, ngap, pfcp, s1ap, sbi, sip,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
#: 不經外掛機制、永遠都在的那些。
|
|
130
|
+
BUILTIN_ADAPTERS: tuple[Adapter, ...] = (
|
|
131
|
+
ngap, s1ap, nas5gs, naseps, sip, sbi, diameter, pfcp, gtpv2, gtp,
|
|
132
|
+
) # type: ignore[assignment]
|
|
133
|
+
|
|
134
|
+
_REQUIRED_ATTRS = ("NAME", "ORDER", "DISPLAY_FILTER", "DISSECTORS", "parse")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _validate(name: str, obj: object) -> Adapter:
|
|
138
|
+
"""外掛缺屬性要在載入時就炸,不要等到某一格封包進來才 AttributeError。"""
|
|
139
|
+
missing = [attr for attr in _REQUIRED_ATTRS if not hasattr(obj, attr)]
|
|
140
|
+
if missing:
|
|
141
|
+
raise PluginError(
|
|
142
|
+
_('Plugin adapter {name!r} is missing required attributes: {attrs}. The contract is in telcoladder/adapters/__init__.py.').format(name=name, attrs=", ".join(missing))
|
|
143
|
+
)
|
|
144
|
+
return obj # type: ignore[return-value]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@cache
|
|
148
|
+
def adapters() -> tuple[Adapter, ...]:
|
|
149
|
+
"""全部 adapter,依 `(ORDER, NAME)` 排序。
|
|
150
|
+
|
|
151
|
+
用 `NAME` 當第二鍵是為了穩定:兩個 adapter 宣告同一個 ORDER 時,
|
|
152
|
+
輸出順序不該取決於安裝順序 —— 那會讓同一份擷取檔在兩台機器上
|
|
153
|
+
產生不同的圖。
|
|
154
|
+
"""
|
|
155
|
+
found = list(BUILTIN_ADAPTERS)
|
|
156
|
+
for name, obj in load_group(ADAPTER_GROUP):
|
|
157
|
+
found.append(_validate(name, obj))
|
|
158
|
+
return tuple(sorted(found, key=lambda a: (a.ORDER, a.NAME)))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@cache
|
|
162
|
+
def carriers_of(payload: str) -> tuple[Adapter, ...]:
|
|
163
|
+
"""宣告會載送 `payload` 的 adapter,依 `adapters()` 的順序。
|
|
164
|
+
|
|
165
|
+
`@cache` 與 `adapters()` 同一個理由:這在每一格封包上都會被問到,
|
|
166
|
+
而答案在行程生命週期內不變。
|
|
167
|
+
|
|
168
|
+
宣告了 `CARRIES` 卻沒有 `carrier_keys` 是允許的 —— 那代表「我載送它,
|
|
169
|
+
但我身上沒有可以歸戶的東西」。載荷 adapter 會拿到空的鍵集合,訊息仍然
|
|
170
|
+
看得到,只是歸不了戶。這比整個消失好,也比編一個假的身分好。
|
|
171
|
+
"""
|
|
172
|
+
return tuple(
|
|
173
|
+
adapter
|
|
174
|
+
for adapter in adapters()
|
|
175
|
+
if payload in getattr(adapter, "CARRIES", ())
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def carrier_blocks(adapter: Adapter, frame: Frame) -> list[dict[str, Any]]:
|
|
180
|
+
"""這一格裡屬於該載體的區塊。
|
|
181
|
+
|
|
182
|
+
走 `CARRIER_LAYER` 而不是 `NAME` —— 兩者不一定相同(`sbi` 的層叫 `http2`)。
|
|
183
|
+
用錯的症狀是**一格都收不到而且不報錯**,所以這個查詢只有這一份。
|
|
184
|
+
"""
|
|
185
|
+
return frame.layer(getattr(adapter, "CARRIER_LAYER", adapter.NAME))
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def carrier_keys_from(adapter: Adapter, block: dict[str, Any], frame: Frame) -> frozenset[IdKey]:
|
|
189
|
+
"""問載體要身分鍵。沒實作 `carrier_keys` 的載體回空集合。"""
|
|
190
|
+
fn = getattr(adapter, "carrier_keys", None)
|
|
191
|
+
if fn is None:
|
|
192
|
+
return frozenset()
|
|
193
|
+
return fn(block, frame)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def display_filter() -> str:
|
|
197
|
+
"""全部 adapter 的 filter 片段聯集。
|
|
198
|
+
|
|
199
|
+
每個片段各自括起來再用 `||` 串 —— 外掛的片段可能本身就含 `||`
|
|
200
|
+
(例如 `"sip || sdp"`),不括起來會讓運算優先序悄悄改變。
|
|
201
|
+
"""
|
|
202
|
+
return " || ".join(f"({a.DISPLAY_FILTER})" for a in adapters())
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def protocol_filters(present: "set[str] | frozenset[str]") -> list[dict[str, str]]:
|
|
206
|
+
"""給 UI 的協定快篩清單:`[{"name", "label", "filter"}, …]`。
|
|
207
|
+
|
|
208
|
+
**只列這份擷取檔裡真的有的協定**,而且 `filter` 直接取 adapter 自己宣告的
|
|
209
|
+
`DISPLAY_FILTER` —— 前端不再自己維護一份「SBI 其實要打 http2」的對照表。
|
|
210
|
+
|
|
211
|
+
那份對照表本來寫死在 `web/src/components/DataMiningView.tsx` 裡(四個 5G
|
|
212
|
+
協定),Diameter adapter 落地之後它就過期了 —— 而症狀是「Diameter 的封包
|
|
213
|
+
在清單上看得到,但沒有一個快篩鈕點得出來」。這與 `identities.py` 開頭那條
|
|
214
|
+
「不要硬寫 kind 清單」是同一個教訓:**硬寫的清單不會自己知道有人加了協定。**
|
|
215
|
+
|
|
216
|
+
`label` 用 adapter 名的大寫形式,只有慣用寫法與它不同的才特別列出來 ——
|
|
217
|
+
外掛加進來的協定不必為了好看而改核心程式碼。
|
|
218
|
+
"""
|
|
219
|
+
labels = {"ngap": "NGAP / NAS", "nas-5gs": "NGAP / NAS", "sbi": "SBI",
|
|
220
|
+
"pfcp": "PFCP", "gtp": "GTP-U", "diameter": "Diameter"}
|
|
221
|
+
out: list[dict[str, str]] = []
|
|
222
|
+
seen: set[str] = set()
|
|
223
|
+
for adapter in adapters():
|
|
224
|
+
if adapter.NAME not in present:
|
|
225
|
+
continue
|
|
226
|
+
label = labels.get(adapter.NAME, adapter.NAME.upper())
|
|
227
|
+
if label in seen:
|
|
228
|
+
# NGAP 與 NAS-5GS 共用一個鈕(NAS 一定包在 NGAP 裡)。
|
|
229
|
+
continue
|
|
230
|
+
seen.add(label)
|
|
231
|
+
out.append({"name": adapter.NAME, "label": label,
|
|
232
|
+
"filter": adapter.DISPLAY_FILTER})
|
|
233
|
+
return out
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _decode_as_selector(rule: str) -> str:
|
|
237
|
+
"""`"tcp.port==7777,http2"` → `"tcp.port==7777"`。
|
|
238
|
+
|
|
239
|
+
切最後一個逗號:tshark 的規則是「選擇器,協定」,而選擇器本身
|
|
240
|
+
可以含逗號(`tcp.port==80,443`)。
|
|
241
|
+
"""
|
|
242
|
+
return rule.rsplit(",", 1)[0].strip()
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def default_decode_as() -> tuple[str, ...]:
|
|
246
|
+
"""全部 adapter 宣告的 decode-as 規則,去重後保持穩定順序。
|
|
247
|
+
|
|
248
|
+
`DECODE_AS` 是選用屬性 —— 用 `getattr` 而不是直接存取,是為了讓
|
|
249
|
+
契約落地(`2a9a641`)之前寫好的外掛不必改版就能繼續用。
|
|
250
|
+
|
|
251
|
+
**同一個選擇器被指向兩個協定時大聲報錯,不靜默取其一。** tshark 只會
|
|
252
|
+
採用最後一條,而落選的那個 adapter 的症狀是「一格都收不到」——
|
|
253
|
+
又是一個不報錯的失敗。裝了兩個都宣告 5060 的外掛(SIP 與某個自訂協定)
|
|
254
|
+
正是這種情況,比照 cause 表撞號的處理方式。
|
|
255
|
+
"""
|
|
256
|
+
seen: dict[str, None] = {}
|
|
257
|
+
owner: dict[str, str] = {} # 選擇器 → 先宣告它的 adapter 名稱
|
|
258
|
+
for adapter in adapters():
|
|
259
|
+
for rule in getattr(adapter, "DECODE_AS", ()):
|
|
260
|
+
selector = _decode_as_selector(rule)
|
|
261
|
+
previous = owner.get(selector)
|
|
262
|
+
if previous is not None and rule not in seen:
|
|
263
|
+
raise PluginError(
|
|
264
|
+
_("decode-as clash: {selector!r} is claimed by both {first!r} and {second!r} for different protocols. tshark will apply only one; the other adapter receives nothing and nothing reports it. Specify explicitly with the CLI's --decode-as.").format(selector=selector, first=previous, second=adapter.NAME)
|
|
265
|
+
)
|
|
266
|
+
owner.setdefault(selector, adapter.NAME)
|
|
267
|
+
seen.setdefault(rule, None)
|
|
268
|
+
return tuple(seen)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def required_dissectors() -> tuple[str, ...]:
|
|
272
|
+
"""`telcoladder check` 要驗證的 dissector,去重後保持穩定順序。"""
|
|
273
|
+
seen: dict[str, None] = {}
|
|
274
|
+
for adapter in adapters():
|
|
275
|
+
for dissector in adapter.DISSECTORS:
|
|
276
|
+
seen.setdefault(dissector, None)
|
|
277
|
+
return tuple(seen)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def blind_spots(frame: Frame) -> list[BlindSpot]:
|
|
281
|
+
"""問過每一個 adapter:這一格裡有什麼是你看得到卻讀不出來的?
|
|
282
|
+
|
|
283
|
+
**這個函式存在的理由是 `pipeline` 不該指名任何一個 adapter。**
|
|
284
|
+
在它之前,`pipeline` 直接 `from telcoladder.adapters.nas5gs import
|
|
285
|
+
count_ciphered` 與 `from telcoladder.adapters.sbi import
|
|
286
|
+
undecoded_header_streams` —— 兩處都是核心相依特定 adapter,而外掛契約
|
|
287
|
+
寫著「只加模組,不改核心」。
|
|
288
|
+
|
|
289
|
+
具體後果:T5 的 NAS-EPS 一樣會加密(4G 的 NAS 過了 Security Mode
|
|
290
|
+
Command 之後同樣讀不到內層),照原本的寫法就得在 `pipeline` 再加一條
|
|
291
|
+
指名分支;T4 的 S1AP、T7 的 SIP 各有自己的不可見面,再兩條。現在它們
|
|
292
|
+
只要宣告 `blind_spots()`,**核心一行都不必動**。
|
|
293
|
+
|
|
294
|
+
沒宣告這個鉤子的 adapter 完全正常 —— 多數協定沒有「看得到讀不出來」
|
|
295
|
+
這種狀態。
|
|
296
|
+
"""
|
|
297
|
+
out: list[BlindSpot] = []
|
|
298
|
+
for adapter in adapters():
|
|
299
|
+
hook = getattr(adapter, "blind_spots", None)
|
|
300
|
+
if hook is None:
|
|
301
|
+
continue
|
|
302
|
+
out.extend(hook(frame))
|
|
303
|
+
return out
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def parse_frame(frame: Frame) -> list[Message]:
|
|
307
|
+
"""跑過所有 adapter,回傳這一格產生的全部訊息。"""
|
|
308
|
+
messages: list[Message] = []
|
|
309
|
+
for adapter in adapters():
|
|
310
|
+
messages.extend(adapter.parse(frame))
|
|
311
|
+
return messages
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""載荷 adapter 共用的「找出我的區塊」機制。
|
|
2
|
+
|
|
3
|
+
## 為什麼抽出來
|
|
4
|
+
|
|
5
|
+
**這是 §3.1 那個教訓的唯一一份實作。** 「子解剖巢狀在載體層內」在
|
|
6
|
+
NAS-5GS(掛 NGAP 與 SBI)、NAS-EPS(掛 S1AP)與日後的 SIP/SDP 上是同一件事,
|
|
7
|
+
而它的失敗模式是**一則訊息都收不到,且完全不報錯**。
|
|
8
|
+
|
|
9
|
+
複製第二份的代價不是多幾行,是**兩份會漂**:一份修好了另一份沒有,
|
|
10
|
+
而沒修好的那份不會說話。2026-08-24 寫 NAS-EPS 時抽出來,
|
|
11
|
+
在那之前它私有在 `nas5gs.py` 裡。
|
|
12
|
+
|
|
13
|
+
放在 `adapters/` 底下而不是 `extract.py`,是因為它要問註冊表「誰載送我」——
|
|
14
|
+
那是 adapter 契約的一部分,不是 tshark 輸出的一般性處理。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from telcoladder.extract import Frame
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
#: `dig` 的遞迴層數上限。
|
|
25
|
+
#:
|
|
26
|
+
#: **數的是中間層的數量,不是路徑段數。** 實測:SBI 那條路徑是
|
|
27
|
+
#: `http2 → mime_multipart → nas-5gs`(兩段),但中間層只有 `mime_multipart`
|
|
28
|
+
#: 一個,所以只需要 **1**;NGAP 那條是 **0**(`nas-5gs` 是直接子鍵)。
|
|
29
|
+
#:
|
|
30
|
+
#: 這裡設 3 是留餘裕給 tshark 未來多包一兩層,但**餘裕不是守衛** —— 它只會
|
|
31
|
+
#: 讓結構改變時默默吐出不同的結果。真正的守衛是
|
|
32
|
+
#: `test_dig_needs_exactly_one_intermediate_layer`:結構一變它就紅。
|
|
33
|
+
#: 不夠再放寬,並同時更新那條測試。
|
|
34
|
+
MAX_DIG_DEPTH = 3
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dig(node: Any, target: str, depth: int = 0) -> list[dict[str, Any]]:
|
|
38
|
+
"""在載體區塊底下有界地找出 `target` 層。
|
|
39
|
+
|
|
40
|
+
**不寫死路徑**:NGAP 是 `ngap.nas-5gs` 直接一層,SBI 是隔著
|
|
41
|
+
`mime_multipart`。寫死的話 tshark 換版本改了中間層名字就靜默失效 ——
|
|
42
|
+
而「靜默失效」正是 T1 要修的這個 bug 本身。
|
|
43
|
+
"""
|
|
44
|
+
if depth > MAX_DIG_DEPTH:
|
|
45
|
+
return []
|
|
46
|
+
if isinstance(node, list):
|
|
47
|
+
found: list[dict[str, Any]] = []
|
|
48
|
+
for item in node:
|
|
49
|
+
found.extend(dig(item, target, depth))
|
|
50
|
+
return found
|
|
51
|
+
if not isinstance(node, dict):
|
|
52
|
+
return []
|
|
53
|
+
hit = node.get(target)
|
|
54
|
+
if isinstance(hit, dict):
|
|
55
|
+
return [hit]
|
|
56
|
+
if isinstance(hit, list):
|
|
57
|
+
return [item for item in hit if isinstance(item, dict)]
|
|
58
|
+
found = []
|
|
59
|
+
for value in node.values():
|
|
60
|
+
if isinstance(value, (dict, list)):
|
|
61
|
+
found.extend(dig(value, target, depth + 1))
|
|
62
|
+
return found
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def carried_blocks(name: str, frame: Frame) -> list[tuple[dict[str, Any], dict[str, Any] | None, Any]]:
|
|
66
|
+
"""挖出這一格裡的每一則 NAS,**連同它的載體與載體 adapter 一起回傳**。
|
|
67
|
+
|
|
68
|
+
載體不能丟:NAS PDU 自己的欄位通常不足以歸戶。NGAP 載送時 UE 的身分在
|
|
69
|
+
NGAP 的 UE ID 上,SBI 載送時在 HTTP/2 stream id 與同層的 IMSI 上。少了
|
|
70
|
+
這層連結,只帶 SUPI 的 Registration request 會跟其後只有 NGAP ID 的訊息
|
|
71
|
+
分成兩條流程 —— 而且分完各自看起來都很合理。
|
|
72
|
+
|
|
73
|
+
載體是**查表**來的(`carriers_of`)而不是寫死的 —— 見
|
|
74
|
+
`adapters/__init__.py` 的契約說明。
|
|
75
|
+
|
|
76
|
+
**去重**:同一個區塊有可能既被某個載體挖到、又出現在頂層。多算一則訊息
|
|
77
|
+
不會報錯,圖上只是多一條看起來合理的箭頭,所以這裡用物件識別擋掉。
|
|
78
|
+
`id()` 只在同一格的解析期間有意義,而這正是它的作用域。
|
|
79
|
+
"""
|
|
80
|
+
# 延後 import:避免與註冊表循環
|
|
81
|
+
from telcoladder.adapters import carrier_blocks, carriers_of
|
|
82
|
+
|
|
83
|
+
blocks: list[tuple[dict[str, Any], dict[str, Any] | None, Any]] = []
|
|
84
|
+
seen: set[int] = set()
|
|
85
|
+
|
|
86
|
+
for carrier_adapter in carriers_of(name):
|
|
87
|
+
for parent in carrier_blocks(carrier_adapter, frame):
|
|
88
|
+
for nested in dig(parent, name):
|
|
89
|
+
if id(nested) in seen:
|
|
90
|
+
continue
|
|
91
|
+
seen.add(id(nested))
|
|
92
|
+
blocks.append((nested, parent, carrier_adapter))
|
|
93
|
+
|
|
94
|
+
# NAS 直接出現在頂層(未知載體,或 tshark 就這樣給)。目前六份 fixture
|
|
95
|
+
# 都是 0,但保留它 —— 刪掉是拿「現在沒有」當「永遠不會有」,而那正是
|
|
96
|
+
# 這個 bug 的成因。沒有載體就沒有載體的鑰匙,訊息仍然看得到。
|
|
97
|
+
for block in frame.layer(name):
|
|
98
|
+
if id(block) in seen:
|
|
99
|
+
continue
|
|
100
|
+
seen.add(id(block))
|
|
101
|
+
blocks.append((block, None, None))
|
|
102
|
+
|
|
103
|
+
return blocks
|