mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 18:05:13 +08:00
🐶 Sync 2026-09-26 16:35:25
This commit is contained in:
1 parent
1eb453ca2e
commit
3f39d2b62b
44 files changed
+2741
-2902
No files matched your search
@@ -11,8 +11,8 @@ LUCI_DESCRIPTION:=Live firewall log table (firewall4/nft; iptables-tagged lines
|
||||
LUCI_DEPENDS:=+luci-base +logd +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.45
|
||||
PKG_RELEASE:=38
|
||||
PKG_VERSION:=0.1.46
|
||||
PKG_RELEASE:=39
|
||||
|
||||
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
return baseclass.extend({
|
||||
/* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */
|
||||
APP_VERSION: '0.1.45',
|
||||
APP_VERSION: '0.1.46',
|
||||
ROW_LIMIT_OPTIONS: [25, 50, 100, 250, 500, 1000, 2000],
|
||||
DEFAULT_ROW_LIMIT: 100,
|
||||
FETCH_MODE_OPTIONS: ['auto', 'manual'],
|
||||
|
||||
@@ -262,6 +262,8 @@ function buildEmptyStateNodes(state, callbacks) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!st) return nodes;
|
||||
|
||||
if (blocker === 'no_wan_zone') {
|
||||
nodes.push(
|
||||
E(
|
||||
|
||||
@@ -746,11 +746,11 @@ return view.extend({
|
||||
this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true);
|
||||
} catch (_e) {
|
||||
if (this.viewDisposed) return;
|
||||
/* Keep last toolbar state. Do not clobber a toggle success/failure notice. */
|
||||
/* Keep last-known toolbar; unknown until the first successful fetch. */
|
||||
if (!this.loggingNotice)
|
||||
this.loggingNotice = _(
|
||||
'Could not refresh logging status; showing the last known state.'
|
||||
);
|
||||
this.loggingNotice = this.loggingStatus
|
||||
? _('Could not refresh logging status; showing the last known state.')
|
||||
: _('Could not load logging status.');
|
||||
}
|
||||
this.updateBackendUi();
|
||||
this.updateLoggingToolbarUi();
|
||||
|
||||
@@ -160,7 +160,11 @@ msgstr "Protokollierung konnte nicht deaktiviert werden."
|
||||
msgid "Could not enable logging."
|
||||
msgstr "Protokollierung konnte nicht aktiviert werden."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:753
|
||||
msgid "Could not load logging status."
|
||||
msgstr "Protokollstatus konnte nicht geladen werden."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:752
|
||||
msgid "Could not refresh logging status; showing the last known state."
|
||||
msgstr "Protokollstatus konnte nicht aktualisiert werden; zeige den zuletzt bekannten Zustand."
|
||||
|
||||
@@ -367,7 +371,7 @@ msgstr ""
|
||||
"möglicherweise keine Erfolgs/Fehler-CSS-Variablen; fwlive fällt auf lokale "
|
||||
"Farben zurück (air-gapped, keine Daten verlassen das Gerät)."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323
|
||||
msgid ""
|
||||
"If the WAN is quiet, wait for probes or use the optional ping check in "
|
||||
"Help / the enabling-logs guide."
|
||||
@@ -393,15 +397,15 @@ msgid "Interface (prefix ! to exclude)"
|
||||
msgstr "Schnittstelle (! voranstellen zum Ausschließen)"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361
|
||||
msgid "I’ll configure this under Network → Firewall"
|
||||
msgstr "Ich konfiguriere das unter Netzwerk → Firewall"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:284
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286
|
||||
msgid "Kernel log modules missing"
|
||||
msgstr "Kernel-Logmodule fehlen"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:287
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289
|
||||
msgid ""
|
||||
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
|
||||
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
|
||||
@@ -426,7 +430,7 @@ msgstr "Eingeschränkte Diagnose — timeout-Befehl fehlt"
|
||||
msgid "Live view may be incomplete — a legacy iptables table is registered"
|
||||
msgstr "Die Live-Ansicht kann unvollständig sein — eine veraltete iptables-Tabelle ist registriert"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:330
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332
|
||||
msgid "Logging is off on this router"
|
||||
msgstr "Protokollierung ist auf diesem Router deaktiviert"
|
||||
|
||||
@@ -447,7 +451,7 @@ msgstr ""
|
||||
"Manuell nutzt weiterhin Serverschutz und Abfrageintervall; es ändert das "
|
||||
"Abrufbudget, nicht das Abfrageintervall."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:378
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380
|
||||
msgid "Manual test (System → Terminal):"
|
||||
msgstr "Manueller Test (System → Terminal):"
|
||||
|
||||
@@ -470,14 +474,14 @@ msgstr "Weitere Filter"
|
||||
msgid "Network → Firewall"
|
||||
msgstr "Netzwerk → Firewall"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:275
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277
|
||||
msgid ""
|
||||
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
|
||||
msgstr ""
|
||||
"Keine WAN-Firewallzone in /etc/config/firewall gefunden. Zonen konfigurieren "
|
||||
"unter:"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:270
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272
|
||||
msgid "No WAN zone found"
|
||||
msgstr "Keine WAN-Zone gefunden"
|
||||
|
||||
@@ -492,7 +496,7 @@ msgstr ""
|
||||
msgid "Not now"
|
||||
msgstr "Nicht jetzt"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:353
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355
|
||||
msgid "Nothing changes until you click Enable."
|
||||
msgstr "Nichts ändert sich, bis Sie auf Aktivieren klicken."
|
||||
|
||||
@@ -504,11 +508,11 @@ msgstr "AUSGANG"
|
||||
msgid "One line"
|
||||
msgstr "Einzeilig"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:326
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328
|
||||
msgid "Open firewall zone settings"
|
||||
msgstr "Firewall-Zonen-Einstellungen öffnen"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:333
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
|
||||
msgid ""
|
||||
"OpenWrt does not write firewall events to the log until you turn logging on. "
|
||||
"Live View only shows what the firewall already logs — it does not add allow/"
|
||||
@@ -719,11 +723,11 @@ msgstr ""
|
||||
msgid "Theme tint fallback"
|
||||
msgstr "Theme-Farb-Fallback"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:386
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388
|
||||
msgid "Then ping the router."
|
||||
msgstr "Dann den Router anpingen."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306
|
||||
msgid ""
|
||||
"This router reported a logging blocker that Live View does not recognize yet."
|
||||
msgstr ""
|
||||
@@ -752,7 +756,7 @@ msgstr ""
|
||||
"Über das WAN-Protokollierungs-Steuerelement in der Beobachtungsleiste wieder "
|
||||
"deaktivieren."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:347
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349
|
||||
msgid ""
|
||||
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
|
||||
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
|
||||
@@ -786,7 +790,7 @@ msgstr "Ansicht"
|
||||
msgid "WAN drop/reject logging is off."
|
||||
msgstr "WAN-Drop/Reject-Protokollierung ist aus."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:314
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316
|
||||
msgid ""
|
||||
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
|
||||
"here. Normal LAN browsing will not."
|
||||
@@ -815,7 +819,7 @@ msgid "WAN logging on (%s). Click to disable."
|
||||
msgstr "WAN-Protokollierung: EIN (%s). Klicken zum Deaktivieren."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:301
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
|
||||
msgid "WAN logging unavailable"
|
||||
msgstr "WAN-Protokollierung nicht verfügbar"
|
||||
|
||||
@@ -827,7 +831,7 @@ msgstr "WAN-Protokollierung nicht verfügbar: fehlende Kernel-Log-Module"
|
||||
msgid "WAN logging unavailable: no WAN zone"
|
||||
msgstr "WAN-Protokollierung nicht verfügbar: keine WAN-Zone"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:311
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
|
||||
msgid "Waiting for firewall events"
|
||||
msgstr "Warte auf Firewall-Ereignisse"
|
||||
|
||||
|
||||
@@ -157,7 +157,11 @@ msgstr "Не удалось отключить журналирование."
|
||||
msgid "Could not enable logging."
|
||||
msgstr "Не удалось включить журналирование."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:753
|
||||
msgid "Could not load logging status."
|
||||
msgstr "Не удалось загрузить состояние журналирования."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:752
|
||||
msgid "Could not refresh logging status; showing the last known state."
|
||||
msgstr "Не удалось обновить состояние журналирования; показан последний известный статус."
|
||||
|
||||
@@ -360,7 +364,7 @@ msgstr ""
|
||||
"содержать CSS-переменные success/error или info/warn; fwlive использует "
|
||||
"локальные цвета (работает автономно, данные не покидают устройство)."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323
|
||||
msgid ""
|
||||
"If the WAN is quiet, wait for probes or use the optional ping check in "
|
||||
"Help / the enabling-logs guide."
|
||||
@@ -386,15 +390,15 @@ msgid "Interface (prefix ! to exclude)"
|
||||
msgstr "Интерфейс (! в начале для исключения)"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361
|
||||
msgid "I’ll configure this under Network → Firewall"
|
||||
msgstr "Я настрою это в разделе Сеть → Межсетевой экран"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:284
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286
|
||||
msgid "Kernel log modules missing"
|
||||
msgstr "Отсутствуют модули журналирования ядра"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:287
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289
|
||||
msgid ""
|
||||
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
|
||||
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
|
||||
@@ -419,7 +423,7 @@ msgstr "Ограниченная диагностика — отсутствуе
|
||||
msgid "Live view may be incomplete — a legacy iptables table is registered"
|
||||
msgstr "Живой просмотр может быть неполным — зарегистрирована устаревшая таблица iptables"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:330
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332
|
||||
msgid "Logging is off on this router"
|
||||
msgstr "Журналирование на этом роутере отключено"
|
||||
|
||||
@@ -440,7 +444,7 @@ msgstr ""
|
||||
"«Вручную» по-прежнему использует защиту сервера и частоту опроса; меняется "
|
||||
"бюджет выборки, а не интервал опроса."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:378
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380
|
||||
msgid "Manual test (System → Terminal):"
|
||||
msgstr "Ручной тест (Система → Терминал):"
|
||||
|
||||
@@ -463,14 +467,14 @@ msgstr "Больше фильтров"
|
||||
msgid "Network → Firewall"
|
||||
msgstr "Сеть → Межсетевой экран"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:275
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277
|
||||
msgid ""
|
||||
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
|
||||
msgstr ""
|
||||
"WAN-зона межсетевого экрана не найдена в /etc/config/firewall. Настройте "
|
||||
"зоны в разделе:"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:270
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272
|
||||
msgid "No WAN zone found"
|
||||
msgstr "WAN-зона не найдена"
|
||||
|
||||
@@ -483,7 +487,7 @@ msgstr "WAN-зона не найдена; без неё нельзя перек
|
||||
msgid "Not now"
|
||||
msgstr "Не сейчас"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:353
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355
|
||||
msgid "Nothing changes until you click Enable."
|
||||
msgstr "Ничего не изменится, пока вы не нажмёте «Включить»."
|
||||
|
||||
@@ -495,11 +499,11 @@ msgstr "ВЫХОД"
|
||||
msgid "One line"
|
||||
msgstr "Одна строка"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:326
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328
|
||||
msgid "Open firewall zone settings"
|
||||
msgstr "Открыть настройки зон файрвола"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:333
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
|
||||
msgid ""
|
||||
"OpenWrt does not write firewall events to the log until you turn logging on. "
|
||||
"Live View only shows what the firewall already logs — it does not add allow/"
|
||||
@@ -705,11 +709,11 @@ msgstr ""
|
||||
msgid "Theme tint fallback"
|
||||
msgstr "Запасной цвет темы"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:386
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388
|
||||
msgid "Then ping the router."
|
||||
msgstr "Затем отправьте ping на роутер."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306
|
||||
msgid ""
|
||||
"This router reported a logging blocker that Live View does not recognize yet."
|
||||
msgstr ""
|
||||
@@ -737,7 +741,7 @@ msgid "Turn it back off with the WAN logging control on the watch strip."
|
||||
msgstr ""
|
||||
"Отключите её снова элементом управления журналом WAN на панели наблюдения."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:347
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349
|
||||
msgid ""
|
||||
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
|
||||
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
|
||||
@@ -771,7 +775,7 @@ msgstr "Вид"
|
||||
msgid "WAN drop/reject logging is off."
|
||||
msgstr "Журналирование drop/reject для WAN отключено."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:314
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316
|
||||
msgid ""
|
||||
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
|
||||
"here. Normal LAN browsing will not."
|
||||
@@ -801,7 +805,7 @@ msgid "WAN logging on (%s). Click to disable."
|
||||
msgstr "WAN-журнал: ВКЛ (%s). Нажмите, чтобы отключить."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:301
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
|
||||
msgid "WAN logging unavailable"
|
||||
msgstr "WAN-журналирование недоступно"
|
||||
|
||||
@@ -813,7 +817,7 @@ msgstr "WAN-журналирование недоступно: отсутств
|
||||
msgid "WAN logging unavailable: no WAN zone"
|
||||
msgstr "WAN-журналирование недоступно: нет зоны WAN"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:311
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
|
||||
msgid "Waiting for firewall events"
|
||||
msgstr "Ожидание событий межсетевого экрана"
|
||||
|
||||
|
||||
@@ -133,7 +133,11 @@ msgstr ""
|
||||
msgid "Could not enable logging."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:753
|
||||
msgid "Could not load logging status."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:752
|
||||
msgid "Could not refresh logging status; showing the last known state."
|
||||
msgstr ""
|
||||
|
||||
@@ -317,7 +321,7 @@ msgid ""
|
||||
"data leaves the device)."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323
|
||||
msgid ""
|
||||
"If the WAN is quiet, wait for probes or use the optional ping check in "
|
||||
"Help / the enabling-logs guide."
|
||||
@@ -341,15 +345,15 @@ msgid "Interface (prefix ! to exclude)"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361
|
||||
msgid "I’ll configure this under Network → Firewall"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:284
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286
|
||||
msgid "Kernel log modules missing"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:287
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289
|
||||
msgid ""
|
||||
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
|
||||
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
|
||||
@@ -371,7 +375,7 @@ msgstr ""
|
||||
msgid "Live view may be incomplete — a legacy iptables table is registered"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:330
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332
|
||||
msgid "Logging is off on this router"
|
||||
msgstr ""
|
||||
|
||||
@@ -390,7 +394,7 @@ msgid ""
|
||||
"budget, not the polling interval."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:378
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380
|
||||
msgid "Manual test (System → Terminal):"
|
||||
msgstr ""
|
||||
|
||||
@@ -413,12 +417,12 @@ msgstr ""
|
||||
msgid "Network → Firewall"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:275
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277
|
||||
msgid ""
|
||||
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:270
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272
|
||||
msgid "No WAN zone found"
|
||||
msgstr ""
|
||||
|
||||
@@ -431,7 +435,7 @@ msgstr ""
|
||||
msgid "Not now"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:353
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355
|
||||
msgid "Nothing changes until you click Enable."
|
||||
msgstr ""
|
||||
|
||||
@@ -443,11 +447,11 @@ msgstr ""
|
||||
msgid "One line"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:326
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328
|
||||
msgid "Open firewall zone settings"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:333
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
|
||||
msgid ""
|
||||
"OpenWrt does not write firewall events to the log until you turn logging on. "
|
||||
"Live View only shows what the firewall already logs — it does not add allow/"
|
||||
@@ -633,11 +637,11 @@ msgstr ""
|
||||
msgid "Theme tint fallback"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:386
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388
|
||||
msgid "Then ping the router."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306
|
||||
msgid ""
|
||||
"This router reported a logging blocker that Live View does not recognize yet."
|
||||
msgstr ""
|
||||
@@ -662,7 +666,7 @@ msgstr ""
|
||||
msgid "Turn it back off with the WAN logging control on the watch strip."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:347
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349
|
||||
msgid ""
|
||||
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
|
||||
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
|
||||
@@ -690,7 +694,7 @@ msgstr ""
|
||||
msgid "WAN drop/reject logging is off."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:314
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316
|
||||
msgid ""
|
||||
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
|
||||
"here. Normal LAN browsing will not."
|
||||
@@ -715,7 +719,7 @@ msgid "WAN logging on (%s). Click to disable."
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:301
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
|
||||
msgid "WAN logging unavailable"
|
||||
msgstr ""
|
||||
|
||||
@@ -727,7 +731,7 @@ msgstr ""
|
||||
msgid "WAN logging unavailable: no WAN zone"
|
||||
msgstr ""
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:311
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
|
||||
msgid "Waiting for firewall events"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -148,7 +148,11 @@ msgstr "无法禁用日志记录。"
|
||||
msgid "Could not enable logging."
|
||||
msgstr "无法启用日志记录。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:753
|
||||
msgid "Could not load logging status."
|
||||
msgstr "无法加载日志状态。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:752
|
||||
msgid "Could not refresh logging status; showing the last known state."
|
||||
msgstr "无法刷新日志状态;显示上次已知状态。"
|
||||
|
||||
@@ -341,7 +345,7 @@ msgstr ""
|
||||
"如果行着色看起来缺失,当前 LuCI 主题可能未提供成功/错误或信息/警告 CSS 变量;"
|
||||
"fwlive 会回退到本地颜色(隔离运行,数据不会离开本设备)。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323
|
||||
msgid ""
|
||||
"If the WAN is quiet, wait for probes or use the optional ping check in "
|
||||
"Help / the enabling-logs guide."
|
||||
@@ -367,15 +371,15 @@ msgid "Interface (prefix ! to exclude)"
|
||||
msgstr "接口(前缀 ! 表示排除)"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361
|
||||
msgid "I’ll configure this under Network → Firewall"
|
||||
msgstr "稍后我会在 网络 → 防火墙 中配置"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:284
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286
|
||||
msgid "Kernel log modules missing"
|
||||
msgstr "缺少内核日志模块"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:287
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289
|
||||
msgid ""
|
||||
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
|
||||
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
|
||||
@@ -399,7 +403,7 @@ msgstr "诊断功能受限 — 缺少 timeout 命令"
|
||||
msgid "Live view may be incomplete — a legacy iptables table is registered"
|
||||
msgstr "实时视图可能不完整 — 已注册旧版 iptables 表"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:330
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332
|
||||
msgid "Logging is off on this router"
|
||||
msgstr "此路由器的日志记录已关闭"
|
||||
|
||||
@@ -419,7 +423,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"手动模式仍受服务器保护和轮询节奏约束;它改变的是抓取配额,而不是轮询间隔。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:378
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380
|
||||
msgid "Manual test (System → Terminal):"
|
||||
msgstr "手动测试(系统 → 终端):"
|
||||
|
||||
@@ -442,13 +446,13 @@ msgstr "更多筛选"
|
||||
msgid "Network → Firewall"
|
||||
msgstr "网络 → 防火墙"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:275
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277
|
||||
msgid ""
|
||||
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
|
||||
msgstr ""
|
||||
"在 /etc/config/firewall 中未找到 WAN 防火墙区域。请在以下位置配置区域:"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:270
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272
|
||||
msgid "No WAN zone found"
|
||||
msgstr "未找到 WAN 区域"
|
||||
|
||||
@@ -461,7 +465,7 @@ msgstr "未找到 WAN 区域;没有它无法切换日志记录。"
|
||||
msgid "Not now"
|
||||
msgstr "暂不"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:353
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355
|
||||
msgid "Nothing changes until you click Enable."
|
||||
msgstr "点击「启用」之前不会有任何更改。"
|
||||
|
||||
@@ -473,11 +477,11 @@ msgstr "出接口"
|
||||
msgid "One line"
|
||||
msgstr "单行"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:326
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328
|
||||
msgid "Open firewall zone settings"
|
||||
msgstr "打开防火墙区域设置"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:333
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
|
||||
msgid ""
|
||||
"OpenWrt does not write firewall events to the log until you turn logging on. "
|
||||
"Live View only shows what the firewall already logs — it does not add allow/"
|
||||
@@ -673,11 +677,11 @@ msgstr "防火墙记录流量时表格会自动更新。如果滚动太快,请
|
||||
msgid "Theme tint fallback"
|
||||
msgstr "主题着色回退"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:386
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388
|
||||
msgid "Then ping the router."
|
||||
msgstr "然后 ping 路由器。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306
|
||||
msgid ""
|
||||
"This router reported a logging blocker that Live View does not recognize yet."
|
||||
msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。"
|
||||
@@ -702,7 +706,7 @@ msgstr "主要通信方"
|
||||
msgid "Turn it back off with the WAN logging control on the watch strip."
|
||||
msgstr "通过监视栏上的 WAN 日志控件将其重新关闭。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:347
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349
|
||||
msgid ""
|
||||
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
|
||||
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
|
||||
@@ -732,7 +736,7 @@ msgstr "视图"
|
||||
msgid "WAN drop/reject logging is off."
|
||||
msgstr "WAN 丢弃/拒绝日志记录已关闭。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:314
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316
|
||||
msgid ""
|
||||
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
|
||||
"here. Normal LAN browsing will not."
|
||||
@@ -761,7 +765,7 @@ msgid "WAN logging on (%s). Click to disable."
|
||||
msgstr "WAN 日志:开(%s)。点击可禁用。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:301
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
|
||||
msgid "WAN logging unavailable"
|
||||
msgstr "WAN 日志不可用"
|
||||
|
||||
@@ -773,7 +777,7 @@ msgstr "WAN 日志不可用:缺少内核日志模块"
|
||||
msgid "WAN logging unavailable: no WAN zone"
|
||||
msgstr "WAN 日志不可用:无 WAN 区域"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:311
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
|
||||
msgid "Waiting for firewall events"
|
||||
msgstr "正在等待防火墙事件"
|
||||
|
||||
|
||||
@@ -200,7 +200,11 @@ find_wan_zone_section_state() {
|
||||
_firewall_show=$(uci -q show firewall 2>/dev/null || true)
|
||||
_zones=$(printf '%s\n' "$_firewall_show" \
|
||||
| sed -n 's/^firewall\.\([^.=]*\)=zone$/\1/p')
|
||||
for zone in $_zones; do
|
||||
# Line-wise here-doc (not an unquoted for-loop): @zone[N] is a glob
|
||||
# character class and would expand when cwd contains @zoneN. The
|
||||
# while-read stays in the current shell (here-doc, not a pipeline)
|
||||
# so WAN_ZONE_* mutations and return reach the caller.
|
||||
while IFS= read -r zone || [ -n "$zone" ]; do
|
||||
[ -n "$zone" ] || continue
|
||||
_name=$(uci -q get "firewall.${zone}.name" 2>/dev/null || true)
|
||||
if [ -z "$_name" ]; then
|
||||
@@ -239,7 +243,9 @@ find_wan_zone_section_state() {
|
||||
WAN_ZONE_FOUND="$zone"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done <<EOF
|
||||
$_zones
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -105,9 +105,16 @@ uci_rule_names() {
|
||||
}
|
||||
|
||||
is_uci_style_name() {
|
||||
# Domain: non-empty UCI-style identifier, every character in
|
||||
# [A-Za-z0-9_-]. That is the map-key set this helper admits
|
||||
# (names parseRuleHint can look up). nft comments can print
|
||||
# anything; skip names outside this set so they do not become
|
||||
# dead map keys. parseRuleHint also allows '.' — not accepted
|
||||
# here. The charset glob is *[!class]* (whole string);
|
||||
# [!class]* only tested the first character.
|
||||
case "$1" in
|
||||
*' '*|*'"'*|*"'"*) return 1 ;;
|
||||
[!A-Za-z0-9_-]*) return 1 ;;
|
||||
''|*' '*|*'"'*|*"'"*) return 1 ;;
|
||||
*[!A-Za-z0-9_-]*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
@@ -177,6 +184,8 @@ EOF
|
||||
|
||||
# One awk pass: prefix<TAB>comment. Last quoted log prefix / comment on
|
||||
# the line, including \" units (same capture as the former sed).
|
||||
# Raw tabs in prefix/comment become the two-character sequence \t so the
|
||||
# separator tab is unique. Backslashes are not re-encoded.
|
||||
nft_dump_fields() {
|
||||
LC_ALL=C awk '
|
||||
function last_at(s, needle, p, i, found) {
|
||||
@@ -219,17 +228,60 @@ nft_dump_fields() {
|
||||
comment = quoted_at($0, "comment \"")
|
||||
if (prefix == "" && comment == "")
|
||||
next
|
||||
gsub(/\\/, "\\\\", prefix)
|
||||
gsub(/\t/, "\\t", prefix)
|
||||
gsub(/\\/, "\\\\", comment)
|
||||
gsub(/\t/, "\\t", comment)
|
||||
print prefix "\t" comment
|
||||
}
|
||||
' || return 1
|
||||
}
|
||||
|
||||
# Restore nft_dump_fields TSV encoding. Walk left to right: \\ is a
|
||||
# literal backslash, \t is a tab. Other backslash pairs stay as written.
|
||||
# Do not put backslash sequences in case patterns (they are escapes).
|
||||
# Fast-path strings with no backslash: dash ${var#?} walks are O(n^2) and
|
||||
# the map byte-cap tests feed ~60k prefixes with none.
|
||||
# Result is _tsv_out (no fork).
|
||||
_nft_tsv_unescape() {
|
||||
case "$1" in
|
||||
*\\*) ;;
|
||||
*)
|
||||
_tsv_out="$1"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
_tab=$(printf '\t')
|
||||
_in="$1"
|
||||
_out=
|
||||
while [ -n "$_in" ]; do
|
||||
_head=${_in%"${_in#?}"}
|
||||
_in=${_in#?}
|
||||
if [ "$_head" = "\\" ]; then
|
||||
_n=${_in%"${_in#?}"}
|
||||
_in=${_in#?}
|
||||
if [ "$_n" = t ]; then
|
||||
_out="${_out}${_tab}"
|
||||
elif [ "$_n" = "\\" ]; then
|
||||
_out="${_out}\\"
|
||||
else
|
||||
_out="${_out}\\${_n}"
|
||||
fi
|
||||
else
|
||||
_out="${_out}${_head}"
|
||||
fi
|
||||
done
|
||||
_tsv_out="$_out"
|
||||
}
|
||||
|
||||
# pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
|
||||
# an earlier cosmetic for the same prefix under first-wins.
|
||||
# Reads TSV from nft_dump_fields; stops once the map cap trips.
|
||||
map_from_nft_stream() {
|
||||
pass="${1:-all}"
|
||||
_tab=$(printf '\t')
|
||||
_esc_t=$(printf '\\t')
|
||||
_esc_b=$(printf '\\\\')
|
||||
# IFS= keeps a leading tab (empty prefix + comment-only !fw4: line).
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
[ -n "$_map_overflow" ] && break
|
||||
@@ -239,6 +291,10 @@ map_from_nft_stream() {
|
||||
*"$_tab"*) comment="${line#*"$_tab"}" ;;
|
||||
*) comment="" ;;
|
||||
esac
|
||||
_nft_tsv_unescape "$prefix"
|
||||
prefix="$_tsv_out"
|
||||
_nft_tsv_unescape "$comment"
|
||||
comment="$_tsv_out"
|
||||
label=''
|
||||
|
||||
case "$comment" in
|
||||
@@ -666,14 +722,14 @@ resolve_addresses() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Wall-clock budget clock starts at the TRUE function entry: the budget
|
||||
# must bound the whole call — including read_rpc_input (stalled stdin
|
||||
# must not escape the budget) and json parsing. `date +%s` can jump
|
||||
# under NTP sync — accepted (worst case the budget over- or under-runs
|
||||
# by the jump; worker starvation is still prevented).
|
||||
start=$(date +%s)
|
||||
# Optional JSON on $1 (rpcd may also pass argv $3 at the call site); else stdin.
|
||||
input=$(read_rpc_input "$1")
|
||||
# Wall-clock budget starts after the input read and bounds the lookup
|
||||
# loop only. Stalled stdin is not interrupted — production rpcd closes
|
||||
# a finite payload. `date +%s` can jump under NTP sync — accepted
|
||||
# (worst case the budget over- or under-runs by the jump; worker
|
||||
# starvation is still prevented).
|
||||
start=$(date +%s)
|
||||
lookups=0
|
||||
truncated=0
|
||||
OUT=''
|
||||
@@ -719,7 +775,7 @@ resolve_addresses() {
|
||||
truncated=1
|
||||
break
|
||||
fi
|
||||
# Abort cleanly once the whole-call wall-clock budget is spent;
|
||||
# Abort cleanly once the lookup-loop wall-clock budget is spent;
|
||||
# never START a lookup past the budget.
|
||||
now=$(date +%s)
|
||||
if [ $((now - start)) -ge "$RESOLVE_BUDGET" ]; then
|
||||
@@ -793,6 +849,42 @@ run_selftest() {
|
||||
expected=$(printf 'a\\n\\nb')
|
||||
check_eq 'json_escape blank line' "$escaped" "$expected" "expected $expected got $escaped" || return 1
|
||||
|
||||
# Charset applies to every character, not only the first.
|
||||
if ! is_uci_style_name 'Rule'; then
|
||||
echo 'is_uci_style_name: expected accept Rule' >&2
|
||||
return 1
|
||||
fi
|
||||
if ! is_uci_style_name 'Allow-SSH'; then
|
||||
echo 'is_uci_style_name: expected accept Allow-SSH' >&2
|
||||
return 1
|
||||
fi
|
||||
if ! is_uci_style_name '-lead'; then
|
||||
echo 'is_uci_style_name: expected accept -lead' >&2
|
||||
return 1
|
||||
fi
|
||||
if is_uci_style_name 'My Rule'; then
|
||||
echo 'is_uci_style_name: expected reject for space' >&2
|
||||
return 1
|
||||
fi
|
||||
# First character is legal; later $ / ; / . must still fail.
|
||||
# shellcheck disable=SC2016
|
||||
if is_uci_style_name 'rule$x'; then
|
||||
echo 'is_uci_style_name: expected reject rule$x (later-char illegal)' >&2
|
||||
return 1
|
||||
fi
|
||||
if is_uci_style_name 'rule;id'; then
|
||||
echo 'is_uci_style_name: expected reject rule;id (later-char illegal)' >&2
|
||||
return 1
|
||||
fi
|
||||
if is_uci_style_name 'rule.x'; then
|
||||
echo 'is_uci_style_name: expected reject rule.x (later-char illegal)' >&2
|
||||
return 1
|
||||
fi
|
||||
if is_uci_style_name ''; then
|
||||
echo 'is_uci_style_name: expected reject empty' >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if is_resolvable_address 'not an ip'; then
|
||||
echo 'is_resolvable_address: expected reject for spaces' >&2
|
||||
return 1
|
||||
@@ -864,10 +956,17 @@ run_selftest() {
|
||||
# Escaped-quote capture: a log prefix containing \" must not be truncated
|
||||
# at the escaped quote (nft dumps strings with backslash escapes).
|
||||
_tab=$(printf '\t')
|
||||
_esc_t=$(printf '\\t')
|
||||
_esc_b=$(printf '\\\\')
|
||||
got=$(printf '%s\n' 'log prefix "My \" Rule"' | nft_dump_fields)
|
||||
got="${got%%${_tab}*}"
|
||||
if [ "$got" != 'My \" Rule' ]; then
|
||||
echo "escaped-quote prefix capture: expected 'My \\\" Rule' got '$got'" >&2
|
||||
if [ "$got" != 'My \\" Rule' ]; then
|
||||
echo "escaped-quote prefix capture: expected encoded 'My \\\\\" Rule' got '$got'" >&2
|
||||
return 1
|
||||
fi
|
||||
_nft_tsv_unescape "$got"
|
||||
if [ "$_tsv_out" != 'My \" Rule' ]; then
|
||||
echo "escaped-quote prefix unescape: expected 'My \\\" Rule' got '$_tsv_out'" >&2
|
||||
return 1
|
||||
fi
|
||||
# Plain prefix still captures unchanged.
|
||||
@@ -884,6 +983,70 @@ run_selftest() {
|
||||
echo "empty prefix capture: expected '' got '$got'" >&2
|
||||
return 1
|
||||
fi
|
||||
# Guest nft list emits a raw tab inside the quoted prefix
|
||||
# (log prefix "fwlive<TAB>tabbed"). Encode it in the TSV field.
|
||||
got=$(printf '%s\n' "log prefix \"fwlive${_tab}tabbed\"" | nft_dump_fields)
|
||||
case "$got" in
|
||||
*"${_tab}"*"${_tab}"*)
|
||||
echo "tabbed prefix TSV: expected one separator tab, got extra" >&2
|
||||
return 1
|
||||
;;
|
||||
*"${_tab}"*) ;;
|
||||
*)
|
||||
echo "tabbed prefix TSV: missing separator tab, got '$got'" >&2
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
got_prefix="${got%%${_tab}*}"
|
||||
if [ "$got_prefix" != "fwlive${_esc_t}tabbed" ]; then
|
||||
echo "tabbed prefix TSV: expected fwlive\\ttabbed got '$got_prefix'" >&2
|
||||
return 1
|
||||
fi
|
||||
_nft_tsv_unescape "$got_prefix"
|
||||
if [ "$_tsv_out" != "fwlive${_tab}tabbed" ]; then
|
||||
echo "tabbed prefix unescape: expected fwlive<TAB>tabbed" >&2
|
||||
return 1
|
||||
fi
|
||||
# Literal backslash-t must round-trip and not become a tab.
|
||||
got=$(printf '%s\n' "log prefix \"fwlive${_esc_t}lit\"" | nft_dump_fields)
|
||||
got_prefix="${got%%${_tab}*}"
|
||||
if [ "$got_prefix" != "fwlive${_esc_b}tlit" ]; then
|
||||
echo "literal \\\\t TSV: expected fwlive\\\\\\\\tlit got '$got_prefix'" >&2
|
||||
return 1
|
||||
fi
|
||||
_nft_tsv_unescape "$got_prefix"
|
||||
if [ "$_tsv_out" != "fwlive${_esc_t}lit" ]; then
|
||||
echo "literal \\\\t unescape: expected fwlive\\\\tlit got '$_tsv_out'" >&2
|
||||
return 1
|
||||
fi
|
||||
# Tabbed prefix plus !fw4: label: map key is the full prefix.
|
||||
got=$(printf '%s\n' "log prefix \"fwlive${_tab}tabbed\" comment \"!fw4: TabRule\"" | nft_dump_fields)
|
||||
OUT=''
|
||||
_map_keys=0
|
||||
_map_overflow=''
|
||||
map_from_nft_stream all <<EOF
|
||||
$got
|
||||
EOF
|
||||
if [ "$OUT" != '"fwlive'"${_esc_t}"'tabbed":"TabRule"' ]; then
|
||||
echo "tabbed prefix map: expected full prefix -> TabRule, got $OUT" >&2
|
||||
return 1
|
||||
fi
|
||||
# Comment-only / !fw4: label path is unchanged when there is no tab.
|
||||
got=$(printf '%s\n' 'comment "!fw4: GoodName"' | nft_dump_fields)
|
||||
if [ "$got" != "${_tab}!fw4: GoodName" ]; then
|
||||
echo "comment-only TSV: expected leading tab + !fw4: GoodName got '$got'" >&2
|
||||
return 1
|
||||
fi
|
||||
OUT=''
|
||||
_map_keys=0
|
||||
_map_overflow=''
|
||||
map_from_nft_stream all <<EOF
|
||||
$got
|
||||
EOF
|
||||
if [ "$OUT" != '"GoodName":"GoodName","goodname":"GoodName"' ]; then
|
||||
echo "comment-only map: expected GoodName mapping, got $OUT" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
got=$(poll_clamp_lines "99999999999999999999")
|
||||
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "over-long expected $POLL_LINES_MAX got $got" || return 1
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.16
|
||||
PKG_RELEASE:=292
|
||||
PKG_VERSION:=26.9.26
|
||||
PKG_RELEASE:=293
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -101,7 +101,7 @@ o = s:option(Value, "tls_serverName", translate("Domain"))
|
||||
o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
|
||||
o.default = "0"
|
||||
|
||||
o = s:option(Value, "tls_pinSHA256", translate("PinSHA256"),translate("Certificate fingerprint"))
|
||||
o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)"))
|
||||
|
||||
o = s:option(Value, "up_mbps", translate("Max upload Mbps"))
|
||||
o.rewrite_option = o.option
|
||||
|
||||
@@ -140,7 +140,7 @@ function gen_config(var)
|
||||
tls = {
|
||||
sni = node.tls_serverName,
|
||||
insecure = (node.tls_allowInsecure == "1") and true or false,
|
||||
pinSHA256 = (node.tls_pinSHA256) and node.tls_pinSHA256 or nil,
|
||||
pinSHA256 = (node.tls_pinSHA256 and node.tls_pinSHA256 ~= "") and api.sha256_sb_xray(node.tls_pinSHA256) or nil,
|
||||
ech = (node.ech == "1") and node.ech_config or nil
|
||||
},
|
||||
quic = {
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
2002::/16
|
||||
3fff::/20
|
||||
5f00::/16
|
||||
fc00::/7
|
||||
fd00::/8
|
||||
fe80::/64
|
||||
ff00::/8
|
||||
@@ -11,3 +11,17 @@ google.com.tw
|
||||
google.com.hk
|
||||
gstatic.com
|
||||
xn--ngstr-lra8j.com
|
||||
|
||||
#DNS-Leak
|
||||
dnsleaktest.com
|
||||
dnsleak.com
|
||||
expressvpn.com
|
||||
nordvpn.com
|
||||
surfshark.com
|
||||
ipleak.net
|
||||
perfect-privacy.com
|
||||
browserleaks.com
|
||||
browserleaks.org
|
||||
vpnunlimited.com
|
||||
whoer.net
|
||||
whrq.net
|
||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=196
|
||||
PKG_RELEASE:=76
|
||||
PKG_RELEASE:=77
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
@@ -32,7 +32,7 @@ LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:= \
|
||||
+libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
|
||||
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
|
||||
+resolveip +bind-dig +curl +nping +unzip +xz-utils +xz \
|
||||
+resolveip +curl +nping +unzip +xz-utils +xz \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \
|
||||
+PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \
|
||||
|
||||
@@ -5,27 +5,24 @@
|
||||
local m, s, sec, o
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local URL = require "url"
|
||||
local sys = require "luci.sys"
|
||||
local util = require "luci.util"
|
||||
local datatypes = require "luci.cbi.datatypes"
|
||||
|
||||
-- 获取 LAN IP 地址
|
||||
function lanip()
|
||||
local lan_ip
|
||||
|
||||
-- 尝试从 UCI 直接读取
|
||||
lan_ip = sys.exec("uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}' | tr -d '\\n'")
|
||||
lan_ip = luci.sys.exec("uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}' | tr -d '\\n'")
|
||||
|
||||
-- 尝试从 LAN 接口信息中读取(优先 ifname,再 fallback 到 device)
|
||||
if not lan_ip or lan_ip == "" then
|
||||
lan_ip = sys.exec([[
|
||||
lan_ip = luci.sys.exec([[
|
||||
ip -4 addr show $(uci -q -p /tmp/state get network.lan.device || uci -q -p /tmp/state get network.lan.device) 2>/dev/null \
|
||||
| grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
|
||||
end
|
||||
|
||||
-- 取任意一个 global IPv4 地址
|
||||
if not lan_ip or lan_ip == "" then
|
||||
lan_ip = sys.exec([[
|
||||
lan_ip = luci.sys.exec([[
|
||||
ip -4 addr show scope global 2>/dev/null \
|
||||
| grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
|
||||
end
|
||||
@@ -34,9 +31,10 @@ ip -4 addr show scope global 2>/dev/null \
|
||||
end
|
||||
|
||||
local lan_ip = lanip()
|
||||
local validation = require "luci.cbi.datatypes"
|
||||
local clash_nodes = {}
|
||||
local function is_finded(e)
|
||||
return sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
|
||||
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
|
||||
end
|
||||
|
||||
local function clash_display_name(s)
|
||||
@@ -50,28 +48,6 @@ local function clash_display_name(s)
|
||||
return "[CLASH]"
|
||||
end
|
||||
|
||||
local function is_valid_dns(str)
|
||||
if datatypes.ip4addrport(str) then
|
||||
return true
|
||||
end
|
||||
|
||||
local scheme, target = str:match("^([a-zA-Z0-9%+%-%.]+)://(.+)$")
|
||||
if scheme and target and #target > 0 then
|
||||
return true
|
||||
end
|
||||
|
||||
local host = str:match("^([^:]+):%d+$") or str
|
||||
if host:match("[a-zA-Z]")
|
||||
and not host:match("[^%w%.%-]")
|
||||
and not host:match("^[%.%-]")
|
||||
and not host:match("[%.%-]$")
|
||||
and not host:match("%.%.") then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH etc.</h3>"))
|
||||
m:section(SimpleSection).template = "shadowsocksr/status"
|
||||
|
||||
@@ -155,23 +131,6 @@ o:value("7", translate("Prefer module built-in DNS"))
|
||||
o:value("0", translate("Use Local DNS Service listen port 5335"))
|
||||
o.default = 1
|
||||
|
||||
o = s:option(Value, "dns2tcp_tunnel_forward", translate("Anti-pollution DNS Server"))
|
||||
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
|
||||
o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)"))
|
||||
o:value("208.67.222.222:53", translate("OpenDNS (208.67.222.222)"))
|
||||
o:value("208.67.220.220:53", translate("OpenDNS (208.67.220.220)"))
|
||||
o:value("209.244.0.3:53", translate("Level 3 Public DNS (209.244.0.3)"))
|
||||
o:value("209.244.0.4:53", translate("Level 3 Public DNS (209.244.0.4)"))
|
||||
o:value("4.2.2.1:53", translate("Level 3 Public DNS (4.2.2.1)"))
|
||||
o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
|
||||
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
|
||||
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
|
||||
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
|
||||
o:depends("pdnsd_enable", "1")
|
||||
o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
|
||||
o.datatype = "ip4addrport"
|
||||
o.default = "8.8.4.4:53"
|
||||
|
||||
o = s:option(Value, "tunnel_forward", translate("Anti-pollution DNS Server"))
|
||||
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
|
||||
o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)"))
|
||||
@@ -184,38 +143,11 @@ o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
|
||||
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
|
||||
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
|
||||
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
|
||||
o:depends("pdnsd_enable", "1")
|
||||
o:depends("pdnsd_enable", "7")
|
||||
o.datatype = nil
|
||||
o.description =
|
||||
"<ul>" ..
|
||||
"<li>" .. translate("Custom DNS Server (supports format IP:PORT, domain name, or scheme://...).") .. "</li>" ..
|
||||
"<li>" .. translate("Muitiple DNS server can saperate with ','") .. "</li>" ..
|
||||
"<li>" .. translate("Note: IP addresses MUST specify a port.") .. "</li>" ..
|
||||
"</ul>"
|
||||
o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
|
||||
o.datatype = "ip4addrport"
|
||||
o.default = "8.8.4.4:53"
|
||||
o.validate = function(self, value, section)
|
||||
if section and value and value ~= "" then
|
||||
local parts = {}
|
||||
for part in (value .. ","):gmatch("(.-),") do
|
||||
part = part:gsub("^%s*", ""):gsub("%s*$", "")
|
||||
if part ~= "" then
|
||||
if not is_valid_dns(part) then
|
||||
return nil, translate("Expecting: %s"):format(translate("IP:PORT, Domain, or Protocol URL (e.g. 8.8.8.8:53, dns.google, https://...).") ..
|
||||
"<br>".. translate("Note: Pure IP without port is not allowed."))
|
||||
end
|
||||
table.insert(parts, part)
|
||||
end
|
||||
end
|
||||
|
||||
if #parts > 0 then
|
||||
return table.concat(parts, ",")
|
||||
end
|
||||
|
||||
return nil, translate("Expecting: %s"):format(translate("valid address:port")) -- 有效的地址:端口
|
||||
end
|
||||
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, "tunnel_forward_mosdns", translate("Anti-pollution DNS Server"))
|
||||
o:value("tcp://8.8.4.4:53,tcp://8.8.8.8:53", translate("Google Public DNS"))
|
||||
@@ -293,7 +225,7 @@ if is_finded("chinadns-ng") then
|
||||
local parts = {}
|
||||
for part in string.gmatch(value, "[^,]+") do
|
||||
part = part:gsub("^%s*", ""):gsub("%s*$", "")
|
||||
if not datatypes.ip4addrport(part) then
|
||||
if not validation.ip4addrport(part) then
|
||||
return nil, translate("Expecting: %s"):format(translate("valid address:port (comma separated)"))
|
||||
end
|
||||
table.insert(parts, part)
|
||||
@@ -304,7 +236,7 @@ if is_finded("chinadns-ng") then
|
||||
return table.concat(parts, ",")
|
||||
end
|
||||
|
||||
if datatypes.ip4addrport(value) then
|
||||
if validation.ip4addrport(value) then
|
||||
return value
|
||||
end
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
require "luci.ip"
|
||||
require "nixio.fs"
|
||||
require "luci.sys"
|
||||
local ucic = luci.model.uci.cursor()
|
||||
local m, s, o
|
||||
|
||||
local enable_mihomo = ucic:get_first('shadowsocksr', 'server_subscribe', 'enable_mihomo') or ''
|
||||
|
||||
m = Map("shadowsocksr")
|
||||
|
||||
s = m:section(TypedSection, "access_control")
|
||||
@@ -119,7 +116,7 @@ o.rows = 13
|
||||
o.wrap = "off"
|
||||
o.rmempty = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return nixio.fs.readfile(denydomainconf) or ""
|
||||
return nixio.fs.readfile(denydomainconf) or " "
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
nixio.fs.writefile(denydomainconf, value:gsub("\r\n", "\n"))
|
||||
@@ -128,54 +125,6 @@ o.remove = function(self, section, value)
|
||||
nixio.fs.writefile(denydomainconf, "")
|
||||
end
|
||||
|
||||
if enable_mihomo == "1" then
|
||||
s:tab("fake_ip_filter", translate("Fake-IP Filter and Fallback Lists"))
|
||||
local fake_ip_filter_conf = "/etc/ssrplus/fake_ip_filter.list"
|
||||
o = s:taboption("fake_ip_filter", TextValue, "fake_ip_filter_conf", "", "<font style=color:red>" .. translate("Specifically for editing the Fake-IP filter list.") .. "</font>") -- 专用于编辑 Fake-IP 过滤规则文件。
|
||||
o.rows = 13
|
||||
o.wrap = "off"
|
||||
o.rmempty = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return nixio.fs.readfile(fake_ip_filter_conf) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
nixio.fs.writefile(fake_ip_filter_conf, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
nixio.fs.writefile(fake_ip_filter_conf, "")
|
||||
end
|
||||
|
||||
local fallback_ipcidr_conf = "/etc/ssrplus/fallback_ipcidr.list"
|
||||
o = s:taboption("fake_ip_filter", TextValue, "fallback_ipcidr_conf", "", "<font style=color:red>" .. translate("Specifically for editing the Fallback IP-CIDR list.") .. "</font>") -- 专用于编辑 Fallback IP-CIDR 规则文件。
|
||||
o.rows = 13
|
||||
o.wrap = "off"
|
||||
o.rmempty = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return nixio.fs.readfile(fallback_ipcidr_conf) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
nixio.fs.writefile(fallback_ipcidr_conf, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
nixio.fs.writefile(fallback_ipcidr_conf, "")
|
||||
end
|
||||
|
||||
local fallback_domain_conf = "/etc/ssrplus/fallback_domain.list"
|
||||
o = s:taboption("fake_ip_filter", TextValue, "fallback_domain_conf", "", "<font style=color:red>" .. translate("Specifically for editing the Fallback domain filter list.") .. "</font>") -- 专用于编辑 Fallback 域名过滤规则文件。
|
||||
o.rows = 13
|
||||
o.wrap = "off"
|
||||
o.rmempty = true
|
||||
o.cfgvalue = function(self, section)
|
||||
return nixio.fs.readfile(fallback_domain_conf) or ""
|
||||
end
|
||||
o.write = function(self, section, value)
|
||||
nixio.fs.writefile(fallback_domain_conf, value:gsub("\r\n", "\n"))
|
||||
end
|
||||
o.remove = function(self, section, value)
|
||||
nixio.fs.writefile(fallback_domain_conf, "")
|
||||
end
|
||||
end
|
||||
|
||||
if luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 then
|
||||
m.apply_on_parse = true
|
||||
function m.on_apply(self)
|
||||
|
||||
@@ -188,10 +188,9 @@ local function save_uploaded_clash_node(upload_name, final_path)
|
||||
local alias
|
||||
|
||||
if not sid then
|
||||
local sid_output = luci.sys.exec("uci add shadowsocksr servers 2>/dev/null")
|
||||
sid = trim(sid_output)
|
||||
sid = uci:add("shadowsocksr", "servers")
|
||||
end
|
||||
if not sid or sid == "" then
|
||||
if not sid then
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -201,50 +200,20 @@ local function save_uploaded_clash_node(upload_name, final_path)
|
||||
alias = upload_alias(upload_name)
|
||||
end
|
||||
|
||||
local switch_enable = uci:get("shadowsocksr", sid, "switch_enable")
|
||||
if not switch_enable or switch_enable == "" then
|
||||
switch_enable = uci:get_first("shadowsocksr", "server_subscribe", "switch", "1") or "1"
|
||||
end
|
||||
|
||||
local clash_user_agent = uci:get("shadowsocksr", sid, "clash_user_agent")
|
||||
if not clash_user_agent or clash_user_agent == "" then
|
||||
clash_user_agent = "clash"
|
||||
end
|
||||
|
||||
local safe_sid = luci.util.shellquote(sid)
|
||||
local safe_alias = luci.util.shellquote(alias)
|
||||
local safe_path = luci.util.shellquote(final_path)
|
||||
local safe_upload_name = luci.util.shellquote(upload_name)
|
||||
local safe_switch_enable = luci.util.shellquote(switch_enable)
|
||||
local safe_user_agent = luci.util.shellquote(clash_user_agent)
|
||||
|
||||
local cmd = string.format(
|
||||
"uci set shadowsocksr.%s.type=clash && " ..
|
||||
"uci set shadowsocksr.%s.alias=%s && " ..
|
||||
"uci set shadowsocksr.%s.server=127.0.0.1 && " ..
|
||||
"uci set shadowsocksr.%s.server_port=0 && " ..
|
||||
"uci delete shadowsocksr.%s.clash_url 2>/dev/null; " ..
|
||||
"uci set shadowsocksr.%s.clash_path=%s && " ..
|
||||
"uci set shadowsocksr.%s.clash_user_agent=%s && " ..
|
||||
"uci set shadowsocksr.%s.switch_enable=%s && " ..
|
||||
"uci set shadowsocksr.%s.yaml_upload=1 && " ..
|
||||
"uci set shadowsocksr.%s.yaml_upload_name=%s && " ..
|
||||
"uci commit shadowsocksr",
|
||||
safe_sid,
|
||||
safe_sid, safe_alias,
|
||||
safe_sid,
|
||||
safe_sid,
|
||||
safe_sid,
|
||||
safe_sid, safe_path,
|
||||
safe_sid, safe_user_agent,
|
||||
safe_sid, safe_switch_enable,
|
||||
safe_sid,
|
||||
safe_sid, safe_upload_name
|
||||
)
|
||||
|
||||
if luci.sys.call(cmd) ~= 0 then
|
||||
return nil
|
||||
uci:set("shadowsocksr", sid, "type", "clash")
|
||||
uci:set("shadowsocksr", sid, "alias", alias)
|
||||
uci:set("shadowsocksr", sid, "server", "127.0.0.1")
|
||||
uci:set("shadowsocksr", sid, "server_port", "0")
|
||||
uci:delete("shadowsocksr", sid, "clash_url")
|
||||
uci:set("shadowsocksr", sid, "clash_path", final_path)
|
||||
uci:set("shadowsocksr", sid, "clash_user_agent", uci:get("shadowsocksr", sid, "clash_user_agent") or "clash")
|
||||
if not uci:get("shadowsocksr", sid, "switch_enable") then
|
||||
uci:set("shadowsocksr", sid, "switch_enable", uci:get_first("shadowsocksr", "server_subscribe", "switch", "1") or "1")
|
||||
end
|
||||
uci:set("shadowsocksr", sid, "yaml_upload", "1")
|
||||
uci:set("shadowsocksr", sid, "yaml_upload_name", upload_name)
|
||||
uci:save("shadowsocksr")
|
||||
uci:commit("shadowsocksr")
|
||||
|
||||
cleanup_old_clash_path(old_path, final_path, sid)
|
||||
luci.sys.call(string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1 &", luci.util.shellquote(sid)))
|
||||
@@ -298,48 +267,32 @@ if has_mihomo then
|
||||
local tmp_output = string.format("%s/.upload-%d-%d.yaml", CLASH_YAML_DIR, nixio.getpid(), os.time())
|
||||
local sid
|
||||
local alias
|
||||
local redirect_message_url
|
||||
|
||||
nixio.fs.mkdirr(CLASH_YAML_DIR)
|
||||
nixio.fs.remove(tmp_output)
|
||||
if preprocess_clash_yaml(upload_tmp_path, tmp_output) then
|
||||
hash = hash_file(tmp_output)
|
||||
if not hash or hash == "" then
|
||||
if hash == "" then
|
||||
nixio.fs.remove(tmp_output)
|
||||
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
|
||||
else
|
||||
final_path = string.format("%s/%s.yaml", CLASH_YAML_DIR, hash)
|
||||
local mv_ret = luci.sys.call(string.format("mv -f %s %s", luci.util.shellquote(tmp_output), luci.util.shellquote(final_path)))
|
||||
if mv_ret == 0 and nixio.fs.access(final_path) then
|
||||
sid, alias = save_uploaded_clash_node(upload_filename, final_path)
|
||||
if sid then
|
||||
local message = string.format(translate("Custom YAML imported successfully: %s"), alias or sid)
|
||||
local redirect_url = luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers")
|
||||
redirect_message_url = redirect_url .. "?upload_message=" .. luci.http.urlencode(message)
|
||||
else
|
||||
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
|
||||
if final_path and nixio.fs.access(final_path) then
|
||||
nixio.fs.remove(final_path)
|
||||
end
|
||||
end
|
||||
luci.sys.call(string.format("mv -f %s %s", luci.util.shellquote(tmp_output), luci.util.shellquote(final_path)))
|
||||
sid, alias = save_uploaded_clash_node(upload_filename, final_path)
|
||||
if sid then
|
||||
upload_message = string.format(translate("Custom YAML imported successfully: %s"), alias or sid)
|
||||
else
|
||||
upload_errmessage = translate("Failed to move uploaded YAML to target directory.")
|
||||
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
|
||||
end
|
||||
end
|
||||
else
|
||||
upload_errmessage = upload_errmessage or translate("Uploaded YAML validation or preprocessing failed.")
|
||||
end
|
||||
|
||||
if upload_tmp_path and nixio.fs.access(upload_tmp_path) then
|
||||
nixio.fs.remove(upload_tmp_path)
|
||||
end
|
||||
if tmp_output and nixio.fs.access(tmp_output) then
|
||||
nixio.fs.remove(tmp_output)
|
||||
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
|
||||
end
|
||||
end
|
||||
|
||||
if redirect_message_url then
|
||||
luci.http.redirect(redirect_message_url)
|
||||
return
|
||||
end
|
||||
if upload_tmp_path then
|
||||
nixio.fs.remove(upload_tmp_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -454,11 +407,8 @@ end
|
||||
m = Map("shadowsocksr", translate("Servers subscription and manage"))
|
||||
if upload_errmessage then
|
||||
m.errmessage = upload_errmessage
|
||||
else
|
||||
local message = luci.http.formvalue("upload_message")
|
||||
if message and message ~= "" then
|
||||
m.message = message
|
||||
end
|
||||
elseif upload_message then
|
||||
m.message = upload_message
|
||||
end
|
||||
|
||||
local style_section = m:section(SimpleSection)
|
||||
@@ -616,29 +566,6 @@ if has_mihomo then
|
||||
o.rmempty = true
|
||||
o.description = translate("Disabled means use Xray or ShadowSocks-Rust core.")
|
||||
|
||||
o = s:option(Flag, "enable_fake_ip", string.format("<b><span style='color:red;'>%s</span></b>", translate("Enable Fake-IP Mode")))
|
||||
o.default = "0"
|
||||
o.rmempty = true
|
||||
o:depends("enable_mihomo", "1")
|
||||
o.description = translate(
|
||||
"<ul>" ..
|
||||
"<li>" .. translate("Enable Fake-IP mode; disable to use redir-host mode.") .. "</li>" ..
|
||||
"<li>" .. translate("Enable Fake-IP mode for better performance.") .. "</li>" ..
|
||||
"<li>" .. translate("If disabled, Redir-Host mode will be used which returns real IP addresses.") .. "</li>" ..
|
||||
"<li>" .. translate("Tips: Fake-IP Filter and Fallback Lists:") .. " " ..
|
||||
"<span id='fake_ip_filter_control_link'></span>" ..
|
||||
"<script type='text/javascript'>" ..
|
||||
"(function() {" ..
|
||||
"var url = '" .. luci.dispatcher.build_url("admin", "services", "shadowsocksr", "control") .. "?tab=fake_ip_filter#fake_ip_filter';" ..
|
||||
"var linkText = '<font style=\"color:green\"><b>" .. translate("Click here to manage") .. "</b></font>';" ..
|
||||
"var elem = document.getElementById('fake_ip_filter_control_link');" ..
|
||||
"if (elem) elem.innerHTML = '<a href=\"' + url + '\">' + linkText + '</a>';" ..
|
||||
"})();" ..
|
||||
"</script>" ..
|
||||
"</li>" ..
|
||||
"</ul>"
|
||||
)
|
||||
|
||||
o = s:option(Flag, "sub_convert", translate("Subscribe Convert Online"))
|
||||
o.description = translate("Convert subscriptions to Clash/Mihomo YAML with a subscription template URL.")
|
||||
o.default = "0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%+cbi/valueheader%>
|
||||
<input class="cbi-input-file" style="width: 400px" type="file" id="clash_yaml_file" name="clash_yaml_file" accept=".yaml,.yml" />
|
||||
<input type="submit" class="bt cbi-button cbi-input-apply" name="upload_clash_yaml" value="<%:Upload%>" />
|
||||
<input type="submit" class="cbi-button cbi-input-apply" name="upload_clash_yaml" value="<%:Upload%>" />
|
||||
<%+cbi/valuefooter%>
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -608,10 +608,6 @@ prepare_clash_runtime_config() {
|
||||
local dns_mode
|
||||
local client_policy_stats
|
||||
local client_rules
|
||||
local enable_fake_ip
|
||||
local socks5_auth
|
||||
local socks5_user
|
||||
local socks5_pass
|
||||
|
||||
cache_file="$(get_clash_cache_file "$sid")"
|
||||
workdir="$(get_clash_workdir "$sid")"
|
||||
@@ -627,7 +623,6 @@ prepare_clash_runtime_config() {
|
||||
dns_mode="$(uci_get_by_type global pdnsd_enable 0)"
|
||||
client_policy_stats=""
|
||||
client_rules=0
|
||||
enable_fake_ip="$(uci_get_by_type server_subscribe enable_fake_ip "")"
|
||||
|
||||
[ -s "$cache_file" ] || return 1
|
||||
|
||||
@@ -645,35 +640,15 @@ prepare_clash_runtime_config() {
|
||||
routing-mark: $CLASH_OUTBOUND_MARK
|
||||
profile:
|
||||
store-selected: true
|
||||
store-fake-ip: true
|
||||
tun:
|
||||
enable: false
|
||||
dns:
|
||||
enable: $( [ "$dns_mode" = "7" ] && echo "true" || echo "false" )
|
||||
enhanced-mode: redir-host
|
||||
listen: 127.0.0.1:$dns_port
|
||||
ipv6: $( [ "$dns_ipv4_only" = "1" ] && echo "false" || echo "true" )
|
||||
EOF
|
||||
|
||||
# 根据 enable_fake_ip 决定 enhanced-mode
|
||||
if [ "$enable_fake_ip" = "1" ]; then
|
||||
ENHANCED_MODE="fake-ip"
|
||||
else
|
||||
ENHANCED_MODE="redir-host"
|
||||
fi
|
||||
# 根据 dns_mode 添加不同的 dns 配置
|
||||
if [ "$dns_mode" = "7" ]; then
|
||||
cat >> "$overlay_file" <<-EOF
|
||||
dns:
|
||||
enable: true
|
||||
enhanced-mode: $ENHANCED_MODE
|
||||
listen: 127.0.0.1:$dns_port
|
||||
ipv6: $( [ "$dns_ipv4_only" = "1" ] && echo "false" || echo "true" )
|
||||
EOF
|
||||
else
|
||||
cat >> "$overlay_file" <<-EOF
|
||||
dns:
|
||||
enable: true
|
||||
enhanced-mode: $ENHANCED_MODE
|
||||
ipv6: $( [ "$dns_ipv4_only" = "1" ] && echo "false" || echo "true" )
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ -n "$socks_port" ] && [ "$socks_port" != "0" ]; then
|
||||
echo "socks-port: $socks_port" >>"$overlay_file"
|
||||
|
||||
@@ -1273,24 +1248,14 @@ start_dns() {
|
||||
ssrplus_dns="$(uci_get_by_type global pdnsd_enable 0)"
|
||||
global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)"
|
||||
builtin_dns_enabled=0
|
||||
dnsserver="$(uci_get_by_type global tunnel_forward 8.8.4.4:53)"
|
||||
|
||||
if [ "$ssrplus_dns" = "5" ]; then
|
||||
ssrplus_dns="$(force_dns2tcp_fallback "旧版 DNSPROXY 模式已移除")" || return 1
|
||||
uci_set_by_type global pdnsd_enable "$ssrplus_dns"
|
||||
elif [ "$ssrplus_dns" = "6" ]; then
|
||||
dnsserver="$(uci_get_by_type global chinadns_ng_tunnel_forward 8.8.4.4:53)"
|
||||
fi
|
||||
case "$ssrplus_dns" in
|
||||
1)
|
||||
dnsserver="$(uci_get_by_type global dns2tcp_tunnel_forward 8.8.4.4:53)"
|
||||
;;
|
||||
6)
|
||||
dnsserver="$(uci_get_by_type global chinadns_ng_tunnel_forward 8.8.4.4:53)"
|
||||
;;
|
||||
7)
|
||||
dnsserver="$(uci_get_by_type global tunnel_forward 8.8.4.4:53)"
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
run_mode="$(normalize_run_mode)"
|
||||
|
||||
if [ "$ssrplus_dns" = "4" ] && ! is_finded "mosdns"; then
|
||||
@@ -1556,15 +1521,15 @@ generate_xray_config() {
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning"
|
||||
},
|
||||
"inbounds": [$inbound_json],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"inbounds": [$inbound_json],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
@@ -2940,18 +2905,12 @@ start_rules() {
|
||||
local ac_ips
|
||||
local custom_ports
|
||||
local proxyport
|
||||
local enable_fake_ip
|
||||
|
||||
server=$(get_host_ip "$GLOBAL_SERVER")
|
||||
local_port=$(get_default_node_local_port)
|
||||
lan_ac_ips=$(uci_get_by_type access_control lan_ac_ips)
|
||||
lan_ac_mode=$(uci_get_by_type access_control lan_ac_mode)
|
||||
|
||||
enable_fake_ip=$(uci_get_by_type server_subscribe enable_fake_ip "")
|
||||
if [ -z "$enable_fake_ip" ]; then
|
||||
enable_fake_ip="0"
|
||||
fi
|
||||
|
||||
if [ "$kcp_enable_flag" = "0" ] && [ "$redir_udp" = "1" ]; then
|
||||
udp_server=$(get_host_ip "$UDP_RELAY_SERVER")
|
||||
udp_local_port=$tmp_udp_port
|
||||
@@ -2996,27 +2955,26 @@ start_rules() {
|
||||
fi
|
||||
elif [ "$USE_TABLES" = "iptables" ]; then
|
||||
ARG_A=""
|
||||
fi
|
||||
/usr/share/shadowsocksr/gfw2ipset.sh
|
||||
/usr/bin/ssr-rules \
|
||||
-s "$server" \
|
||||
-l "$local_port" \
|
||||
-S "$udp_server" \
|
||||
-L "$udp_local_port" \
|
||||
-a "$ac_ips" \
|
||||
-i "/etc/ssrplus/china_ssr.txt" \
|
||||
-b "$(uci_get_by_type access_control wan_bp_ips)" \
|
||||
-w "$(uci_get_by_type access_control wan_fw_ips)" \
|
||||
-B "$(uci_get_by_type access_control lan_bp_ips)" \
|
||||
-p "$(uci_get_by_type access_control lan_fp_ips)" \
|
||||
-G "$(uci_get_by_type access_control lan_gm_ips)" \
|
||||
-m "$(uci_get_by_type access_control Interface)" \
|
||||
-D "$proxyport" \
|
||||
-E "$enable_fake_ip" \
|
||||
"$(get_arg_out)" "$(gfwmode)" "$ARG_UDP" "$ARG_UDP_RULES" "$ARG_A"
|
||||
fi
|
||||
/usr/share/shadowsocksr/gfw2ipset.sh
|
||||
/usr/bin/ssr-rules \
|
||||
-s "$server" \
|
||||
-l "$local_port" \
|
||||
-S "$udp_server" \
|
||||
-L "$udp_local_port" \
|
||||
-a "$ac_ips" \
|
||||
-i "/etc/ssrplus/china_ssr.txt" \
|
||||
-b "$(uci_get_by_type access_control wan_bp_ips)" \
|
||||
-w "$(uci_get_by_type access_control wan_fw_ips)" \
|
||||
-B "$(uci_get_by_type access_control lan_bp_ips)" \
|
||||
-p "$(uci_get_by_type access_control lan_fp_ips)" \
|
||||
-G "$(uci_get_by_type access_control lan_gm_ips)" \
|
||||
-m "$(uci_get_by_type access_control Interface)" \
|
||||
-D "$proxyport" \
|
||||
"$(get_arg_out)" "$(gfwmode)" "$ARG_UDP" "$ARG_UDP_RULES" "$ARG_A"
|
||||
|
||||
return $?
|
||||
}
|
||||
return $?
|
||||
}
|
||||
|
||||
start() {
|
||||
local run_mode
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
*.lan
|
||||
*.localdomain
|
||||
*.example
|
||||
*.invalid
|
||||
*.localhost
|
||||
*.test
|
||||
*.local
|
||||
*.home.arpa
|
||||
time.*.com
|
||||
time.*.gov
|
||||
time.*.edu.cn
|
||||
time.*.apple.com
|
||||
time1.*.com
|
||||
time2.*.com
|
||||
time3.*.com
|
||||
time4.*.com
|
||||
time5.*.com
|
||||
time6.*.com
|
||||
time7.*.com
|
||||
ntp.*.com
|
||||
ntp1.*.com
|
||||
ntp2.*.com
|
||||
ntp3.*.com
|
||||
ntp4.*.com
|
||||
ntp5.*.com
|
||||
ntp6.*.com
|
||||
ntp7.*.com
|
||||
*.time.edu.cn
|
||||
*.ntp.org.cn
|
||||
+.pool.ntp.org
|
||||
time1.cloud.tencent.com
|
||||
stun.*.*
|
||||
stun.*.*.*
|
||||
swscan.apple.com
|
||||
mesu.apple.com
|
||||
music.163.com
|
||||
*.music.163.com
|
||||
*.126.net
|
||||
musicapi.taihe.com
|
||||
music.taihe.com
|
||||
songsearch.kugou.com
|
||||
trackercdn.kugou.com
|
||||
*.kuwo.cn
|
||||
api-jooxtt.sanook.com
|
||||
api.joox.com
|
||||
y.qq.com
|
||||
*.y.qq.com
|
||||
streamoc.music.tc.qq.com
|
||||
mobileoc.music.tc.qq.com
|
||||
isure.stream.qqmusic.qq.com
|
||||
dl.stream.qqmusic.qq.com
|
||||
aqqmusic.tc.qq.com
|
||||
amobile.music.tc.qq.com
|
||||
localhost.ptlogin2.qq.com
|
||||
*.msftconnecttest.com
|
||||
*.msftncsi.com
|
||||
*.xiami.com
|
||||
*.music.migu.cn
|
||||
music.migu.cn
|
||||
+.wotgame.cn
|
||||
+.wggames.cn
|
||||
+.wowsgame.cn
|
||||
+.wargaming.net
|
||||
*.*.*.srv.nintendo.net
|
||||
*.*.stun.playstation.net
|
||||
xbox.*.*.microsoft.com
|
||||
*.*.xboxlive.com
|
||||
*.ipv6.microsoft.com
|
||||
teredo.*.*.*
|
||||
teredo.*.*
|
||||
speedtest.cros.wr.pvp.net
|
||||
+.jjvip8.com
|
||||
www.douyu.com
|
||||
activityapi.huya.com
|
||||
activityapi.huya.com.w.cdngslb.com
|
||||
www.bilibili.com
|
||||
api.bilibili.com
|
||||
a.w.bilicdn1.com
|
||||
@@ -1,9 +0,0 @@
|
||||
+.google.com
|
||||
+.facebook.com
|
||||
+.youtube.com
|
||||
+.twitter.com
|
||||
+.telegram.org
|
||||
+.githubusercontent.com
|
||||
+.googlevideo.com
|
||||
+.msftconnecttest.com
|
||||
+.msftncsi.com
|
||||
@@ -1,15 +0,0 @@
|
||||
0.0.0.0/8
|
||||
10.0.0.0/8
|
||||
100.64.0.0/10
|
||||
127.0.0.0/8
|
||||
169.254.0.0/16
|
||||
172.16.0.0/12
|
||||
192.0.0.0/24
|
||||
192.0.2.0/24
|
||||
192.88.99.0/24
|
||||
192.168.0.0/16
|
||||
198.51.100.0/24
|
||||
203.0.113.0/24
|
||||
224.0.0.0/4
|
||||
240.0.0.0/4
|
||||
255.255.255.255/32
|
||||
@@ -47,18 +47,6 @@ if [ -s "/etc/config/shadowsocksr" ]; then
|
||||
uci -q set shadowsocksr.@server_subscribe[0].auto_update_min_time='0'
|
||||
fi
|
||||
|
||||
if ! uci -q get shadowsocksr.@server_subscribe[0].enable_mihomo > /dev/null; then
|
||||
uci -q set shadowsocksr.@server_subscribe[0].enable_mihomo='1'
|
||||
fi
|
||||
|
||||
if ! uci -q get shadowsocksr.@server_subscribe[0].enable_fake_ip > /dev/null; then
|
||||
uci -q set shadowsocksr.@server_subscribe[0].enable_fake_ip='1'
|
||||
fi
|
||||
|
||||
if ! uci -q get shadowsocksr.@server_subscribe[0].sub_convert > /dev/null; then
|
||||
uci -q set shadowsocksr.@server_subscribe[0].sub_convert='1'
|
||||
fi
|
||||
|
||||
if ! uci -q get shadowsocksr.@server_subscribe[0].config_auto_update_mode > /dev/null; then
|
||||
uci -q set shadowsocksr.@server_subscribe[0].config_auto_update_mode='0'
|
||||
fi
|
||||
|
||||
@@ -46,7 +46,6 @@ CHECK_STATUS=0
|
||||
RESTORE_RULES=0
|
||||
FLUSH_RULES=0
|
||||
CLEANUP_PERSISTENCE=0
|
||||
ENABLE_FAKE_IP=0
|
||||
# ASCII code for SSRP.Use whatever,just not the same.
|
||||
FWMARK="0x53535250"
|
||||
|
||||
@@ -84,15 +83,14 @@ usage() {
|
||||
-m <Interface> Interface name
|
||||
-I <ip_list_file> a file content is bypassed shunt ip list
|
||||
-e <extra_options> extra options for iptables
|
||||
-E <0|1> enable_fake_ip status
|
||||
-o apply the rules to the OUTPUT chain
|
||||
-O apply the global rules to the OUTPUT chain
|
||||
-u enable udprelay mode, TPROXY is required
|
||||
-U enable udprelay mode, using different IP
|
||||
and ports for TCP and UDP
|
||||
-y disable all auxiliary UDP rules when UDP
|
||||
transparent proxy is unavailable
|
||||
-f flush the rules
|
||||
-u enable udprelay mode, TPROXY is required
|
||||
-U enable udprelay mode, using different IP
|
||||
and ports for TCP and UDP
|
||||
-y disable all auxiliary UDP rules when UDP
|
||||
transparent proxy is unavailable
|
||||
-f flush the rules
|
||||
-g gfwlist mode
|
||||
-r router mode
|
||||
-z all mode
|
||||
@@ -234,12 +232,6 @@ cleanup_persistence_files() {
|
||||
loger 5 "Removed xhttp hash file: /tmp/.last_xhttp_hash"
|
||||
fi
|
||||
|
||||
# Remove Fake_IP Mode state file
|
||||
if [ -f "$ENABLE_FAKE_IP_STATE_FILE" ]; then
|
||||
rm -f "$ENABLE_FAKE_IP_STATE_FILE" 2>/dev/null
|
||||
loger 5 "Removed Fake_IP Mode state file: $ENABLE_FAKE_IP_STATE_FILE"
|
||||
fi
|
||||
|
||||
loger 5 "Persistence cleanup completed"
|
||||
return 0
|
||||
}
|
||||
@@ -323,64 +315,25 @@ flush_nftables() {
|
||||
|
||||
flush_iptables_legacy() {
|
||||
flush_iptables() {
|
||||
local table=$1
|
||||
local ipt="iptables -t $table"
|
||||
local DAT
|
||||
|
||||
DAT=$(iptables-save -t $table 2>/dev/null)
|
||||
|
||||
[ -z "$DAT" ] && return 0
|
||||
|
||||
# 1. 解除所有引用 SS_SPEC* 链的规则
|
||||
echo "$DAT" | grep -E "^-A .*SS_SPEC" | while read -r line; do
|
||||
local chain rule
|
||||
chain=$(echo "$line" | awk '{print $2}')
|
||||
rule=$(echo "$line" | sed 's/^-A [^ ]* //')
|
||||
[ -n "$chain" ] && [ -n "$rule" ] && \
|
||||
eval "$ipt -D \"$chain\" $rule 2>/dev/null"
|
||||
local ipt="iptables -t $1"
|
||||
local DAT=$(iptables-save -t $1)
|
||||
eval $(echo "$DAT" | grep "$TAG" | sed -e 's/^-A/$ipt -D/' -e 's/$/;/')
|
||||
for chain in $(echo "$DAT" | awk '/^:SS_SPEC/{print $1}'); do
|
||||
$ipt -F ${chain:1} 2>/dev/null && $ipt -X ${chain:1}
|
||||
done
|
||||
|
||||
# 2. 删除带 TAG 或 mark 的残留规则(同样加上 eval)
|
||||
echo "$DAT" | grep -E "^-A " | grep -E "$TAG|-m mark|--mark" | while read -r line; do
|
||||
local chain rule
|
||||
chain=$(echo "$line" | awk '{print $2}')
|
||||
rule=$(echo "$line" | sed 's/^-A [^ ]* //')
|
||||
[ -n "$chain" ] && [ -n "$rule" ] && \
|
||||
eval "$ipt -D \"$chain\" $rule 2>/dev/null"
|
||||
done
|
||||
|
||||
# 3. 提取所有以 SS_SPEC 开头的自定义链
|
||||
local chains
|
||||
chains=$(echo "$DAT" | awk '/^:SS_SPEC/{sub(/^:/,"",$1); print $1}')
|
||||
|
||||
# 4. 清空并彻底删除 SS_SPEC* 自定义链
|
||||
for chain in $chains; do
|
||||
$ipt -F "$chain" 2>/dev/null
|
||||
$ipt -X "$chain" 2>/dev/null
|
||||
done
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# 依次对 nat 和 mangle 表执行清理
|
||||
flush_iptables nat
|
||||
flush_iptables mangle
|
||||
|
||||
# 清理策略路由与路由表
|
||||
if ip rule show | grep -Eiq "fwmark ${FWMARK}.*lookup 999"; then
|
||||
if ip rule show | grep -Eq "fwmark ${FWMARK}.*lookup 999"; then
|
||||
ip rule del fwmark ${FWMARK} table 999 2>/dev/null
|
||||
fi
|
||||
if ip route show table 999 | grep -Eq "^local.*dev lo"; then
|
||||
ip route del local 0.0.0.0/0 dev lo table 999 2>/dev/null
|
||||
fi
|
||||
|
||||
# 清理 IPSET 集合(先 -F 清空,后 -X 销毁)
|
||||
for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \
|
||||
china fplan bplan gmlan whitelist blacklist gfwlist music; do
|
||||
ipset -F $setname 2>/dev/null
|
||||
ipset -X $setname 2>/dev/null
|
||||
done
|
||||
|
||||
[ -n "$FWI" ] && echo '#!/bin/sh' >$FWI
|
||||
return 0
|
||||
}
|
||||
@@ -571,11 +524,6 @@ fw_rule() {
|
||||
}
|
||||
|
||||
fw_rule_nft() {
|
||||
# 优先将 Fake-IP (198.18.0.0/16) 的 TCP 流量重定向至代理端口
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
$NFT add rule inet ss_spec ss_spec_wan_fw ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
fi
|
||||
|
||||
# redirect/translation: when PROXY_PORTS present, redirect those tcp ports to local_port
|
||||
if [ -n "$PROXY_PORTS" ]; then
|
||||
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
|
||||
@@ -611,10 +559,6 @@ fw_rule_iptables() {
|
||||
$IPT -A SS_SPEC_WAN_FW -d "$net" -j RETURN
|
||||
done
|
||||
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
$IPT -A SS_SPEC_WAN_FW -d 198.18.0.0/16 -p tcp -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
fi
|
||||
|
||||
$IPT -A SS_SPEC_WAN_FW -p tcp $PROXY_PORTS -j REDIRECT --to-ports "$local_port" 2>/dev/null || {
|
||||
loger 3 "Can't redirect TCP, please check the iptables."
|
||||
exit 1
|
||||
@@ -667,33 +611,6 @@ ac_rule_nft() {
|
||||
fi
|
||||
$NFT flush chain inet ss_spec ss_spec_prerouting 2>/dev/null
|
||||
|
||||
# Fake-IP 重定向处理 (引入 Interface 匹配,防止 WAN 口越权)
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
if [ -z "$Interface" ]; then
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
$NFT add rule inet ss_spec ss_spec_prerouting ip daddr 198.18.0.0/16 $MATCH_SET meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_prerouting ip daddr 198.18.0.0/16 $MATCH_SET meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
else
|
||||
$NFT add rule inet ss_spec ss_spec_prerouting ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_prerouting ip daddr 198.18.0.0/16 meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
fi
|
||||
else
|
||||
for name in $Interface; do
|
||||
local IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
|
||||
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
|
||||
if [ -n "$IFNAME" ]; then
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
$NFT add rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" ip daddr 198.18.0.0/16 $MATCH_SET meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" ip daddr 198.18.0.0/16 $MATCH_SET meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
else
|
||||
$NFT add rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" ip daddr 198.18.0.0/16 meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Exclude special local addresses
|
||||
if $NFT list chain inet ss_spec ss_spec_prerouting >/dev/null 2>&1; then
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4; do
|
||||
@@ -708,6 +625,16 @@ ac_rule_nft() {
|
||||
# done
|
||||
#fi
|
||||
|
||||
# Build a rule in the prerouting hook chain that jumps to business chain with conditions
|
||||
if [ -n "$PROXY_PORTS" ]; then
|
||||
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
|
||||
if [ -n "$PORTS_ARGS" ]; then
|
||||
TCP_EXT_ARGS="meta l4proto tcp tcp dport { $PORTS_ARGS }"
|
||||
fi
|
||||
else
|
||||
TCP_EXT_ARGS="meta l4proto tcp"
|
||||
fi
|
||||
|
||||
# Block UDP port 443 when TPROXY not Enable
|
||||
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then
|
||||
# Add UDP 443 block rule
|
||||
@@ -731,17 +658,6 @@ ac_rule_nft() {
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build a rule in the prerouting hook chain that jumps to business chain with conditions
|
||||
if [ -n "$PROXY_PORTS" ]; then
|
||||
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
|
||||
if [ -n "$PORTS_ARGS" ]; then
|
||||
TCP_EXT_ARGS="meta l4proto tcp tcp dport { $PORTS_ARGS }"
|
||||
fi
|
||||
else
|
||||
TCP_EXT_ARGS="meta l4proto tcp"
|
||||
fi
|
||||
|
||||
if [ -z "$Interface" ]; then
|
||||
# generic prerouting jump already exists (see ipset_nft), but if we have MATCH_SET_CONDITION we add a more specific rule
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
@@ -767,16 +683,11 @@ ac_rule_nft() {
|
||||
case "$OUTPUT" in
|
||||
1)
|
||||
# Create ss_spec_output tcp chain
|
||||
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
|
||||
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
|
||||
fi
|
||||
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
|
||||
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
$NFT add rule inet ss_spec ss_spec_output ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_output ip daddr 198.18.0.0/16 meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
fi
|
||||
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
|
||||
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
|
||||
fi
|
||||
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
|
||||
|
||||
# Exclude special local addresses
|
||||
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
|
||||
@@ -797,16 +708,11 @@ ac_rule_nft() {
|
||||
;;
|
||||
2)
|
||||
# Create ss_spec_output tcp chain
|
||||
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
|
||||
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
|
||||
fi
|
||||
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
|
||||
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
$NFT add rule inet ss_spec ss_spec_output ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_output ip daddr 198.18.0.0/16 meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
|
||||
fi
|
||||
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
|
||||
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
|
||||
fi
|
||||
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
|
||||
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
|
||||
|
||||
# Exclude special local addresses
|
||||
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
|
||||
@@ -848,38 +754,11 @@ ac_rule_iptables() {
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
|
||||
local ip_list="${LAN_AC_IP#?}"
|
||||
ipset -! create ss_spec_lan_ac hash:net 2>/dev/null
|
||||
ipset flush ss_spec_lan_ac 2>/dev/null
|
||||
if [ -n "$ip_list" ]; then
|
||||
for ip in $ip_list; do
|
||||
ipset add ss_spec_lan_ac "$ip" 2>/dev/null
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# 2. PREROUTING 链处理
|
||||
if [ -z "$Interface" ]; then
|
||||
# Global rules
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
else
|
||||
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
fi
|
||||
else
|
||||
for name in $Interface; do
|
||||
local IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
|
||||
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
|
||||
if [ -n "$IFNAME" ]; then
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
else
|
||||
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
ipset -! -R <<-EOF || return 1
|
||||
create ss_spec_lan_ac hash:net
|
||||
$(for ip in ${LAN_AC_IP#?}; do echo "add ss_spec_lan_ac $ip"; done)
|
||||
EOF
|
||||
|
||||
# Block UDP port 443 when TPROXY not Enable
|
||||
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then
|
||||
@@ -905,67 +784,42 @@ ac_rule_iptables() {
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# 拦截 Fake-IP 流量 (198.18.0.0/16),受 Interface 和 LAN_AC 控制
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
if [ -z "$Interface" ]; then
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
$IPT -I PREROUTING 1 -p tcp -d 198.18.0.0/16 $MATCH_SET -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
$IPT -I PREROUTING 2 -p udp -d 198.18.0.0/16 $MATCH_SET -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
else
|
||||
$IPT -I PREROUTING 1 -p tcp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
$IPT -I PREROUTING 2 -p udp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
fi
|
||||
if [ -z "$Interface" ]; then
|
||||
# Global rules
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
else
|
||||
for name in $Interface; do
|
||||
local IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
|
||||
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
|
||||
if [ -n "$IFNAME" ]; then
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp -d 198.18.0.0/16 $MATCH_SET -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
$IPT -I PREROUTING 2 ${IFNAME:+-i $IFNAME} -p udp -d 198.18.0.0/16 $MATCH_SET -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
else
|
||||
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
$IPT -I PREROUTING 2 ${IFNAME:+-i $IFNAME} -p udp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
fi
|
||||
else
|
||||
for name in $Interface; do
|
||||
local IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
|
||||
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
|
||||
if [ -n "$IFNAME" ]; then
|
||||
if [ -n "$MATCH_SET" ]; then
|
||||
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
else
|
||||
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
fi
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
case "$OUTPUT" in
|
||||
1)
|
||||
# fake-ip first
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
$IPT -I OUTPUT 1 -m mark --mark 255 -j RETURN 2>/dev/null
|
||||
$IPT -I OUTPUT 2 -p tcp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
$IPT -I OUTPUT 3 -p udp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
$IPT -I OUTPUT 4 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
else
|
||||
$IPT -I OUTPUT 1 -m mark --mark 255 -j RETURN 2>/dev/null
|
||||
$IPT -I OUTPUT 2 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
fi
|
||||
$IPT -I OUTPUT 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
|
||||
;;
|
||||
2)
|
||||
ipset -! -R <<-EOF || return 1
|
||||
create ssr_gen_router hash:net
|
||||
$(gen_spec_iplist | sed -e "s/^/add ssr_gen_router /")
|
||||
EOF
|
||||
$IPT -N SS_SPEC_ROUTER 2>/dev/null
|
||||
$IPT -F SS_SPEC_ROUTER 2>/dev/null
|
||||
# $IPT -A SS_SPEC_ROUTER -m mark --mark 255 -j RETURN
|
||||
$IPT -A SS_SPEC_ROUTER -m set --match-set ssr_gen_router dst -j RETURN
|
||||
$IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
|
||||
# fake-ip first
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
$IPT -I OUTPUT 1 -m mark --mark 255 -j RETURN 2>/dev/null
|
||||
$IPT -I OUTPUT 2 -p tcp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
$IPT -I OUTPUT 3 -p udp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
|
||||
$IPT -I OUTPUT 4 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
|
||||
else
|
||||
$IPT -I OUTPUT 1 -m mark --mark 255 -j RETURN 2>/dev/null
|
||||
$IPT -I OUTPUT 2 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
|
||||
fi
|
||||
$IPT -N SS_SPEC_ROUTER 2>/dev/null
|
||||
$IPT -F SS_SPEC_ROUTER 2>/dev/null
|
||||
$IPT -A SS_SPEC_ROUTER -m mark --mark 255 -j RETURN && \
|
||||
$IPT -A SS_SPEC_ROUTER -m set --match-set ssr_gen_router dst -j RETURN && \
|
||||
$IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
|
||||
$IPT -I OUTPUT 1 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
|
||||
;;
|
||||
esac
|
||||
return $?
|
||||
@@ -1001,29 +855,29 @@ tp_rule_nft() {
|
||||
fi
|
||||
|
||||
local MATCH_SET=""
|
||||
local EXT_ARGS=""
|
||||
|
||||
if [ -n "$PROXY_PORTS" ]; then
|
||||
local PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
|
||||
[ -n "$PORTS_ARGS" ] && EXT_ARGS="udp dport { $PORTS_ARGS }"
|
||||
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
|
||||
if [ -n "$PORTS_ARGS" ]; then
|
||||
EXT_ARGS="udp dport { $PORTS_ARGS }"
|
||||
else
|
||||
EXT_ARGS=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$LAN_AC_IP" ]; then
|
||||
# Create LAN access control set if needed
|
||||
local mode="${LAN_AC_IP:0:1}"
|
||||
local ips="${LAN_AC_IP:1}"
|
||||
|
||||
if ! $NFT list set ip ss_spec_mangle ss_spec_lan_ac >/dev/null 2>&1; then
|
||||
$NFT add set ip ss_spec_mangle ss_spec_lan_ac '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
|
||||
else
|
||||
$NFT flush set ip ss_spec_mangle ss_spec_lan_ac 2>/dev/null
|
||||
fi
|
||||
|
||||
for ip in $ips; do
|
||||
for ip in ${LAN_AC_IP#?}; do
|
||||
[ -n "$ip" ] && $NFT add element ip ss_spec_mangle ss_spec_lan_ac "{ $ip }" 2>/dev/null
|
||||
done
|
||||
|
||||
case "$mode" in
|
||||
case "${LAN_AC_IP%${LAN_AC_IP#?}}" in
|
||||
w | W)
|
||||
MATCH_SET="ip saddr @ss_spec_lan_ac"
|
||||
;;
|
||||
@@ -1052,10 +906,8 @@ tp_rule_nft() {
|
||||
fi
|
||||
|
||||
# Bulk import xhttp ip list into nft whitelist (server + shunt)
|
||||
local xhttp_file="${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}"
|
||||
if [ -s "$xhttp_file" ]; then
|
||||
local xhttp_content=$(tr '\n' ',' < "$xhttp_file" | sed 's/,$//')
|
||||
[ -n "$xhttp_content" ] && $NFT add element ip ss_spec_mangle whitelist "{ $xhttp_content }" 2>/dev/null
|
||||
if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then
|
||||
$NFT add element ip ss_spec_mangle whitelist "{ $(tr '\n' ',' < "${xhttp_ip}" | sed 's/,$//') }" 2>/dev/null
|
||||
fi
|
||||
|
||||
# use priority mangle for compatibility with other rules
|
||||
@@ -1065,12 +917,6 @@ tp_rule_nft() {
|
||||
$NFT flush chain ip ss_spec_mangle ss_spec_tproxy 2>/dev/null
|
||||
fi
|
||||
|
||||
# 优先对 Fake-IP 进行 TPROXY 重定向 (无需受限于 PROXY_PORTS 限制)
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy ip daddr 198.18.0.0/16 counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
|
||||
fi
|
||||
|
||||
# Exclude special local addresses
|
||||
if $NFT list chain ip ss_spec_mangle ss_spec_tproxy >/dev/null 2>&1; then
|
||||
for net in 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4; do
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy ip daddr $net return 2>/dev/null
|
||||
@@ -1088,9 +934,9 @@ tp_rule_nft() {
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport 53 return 2>/dev/null
|
||||
|
||||
# avoid redirecting to udp server address
|
||||
# if [ -n "$server" ]; then
|
||||
# $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport != 53 ip daddr "$server" return 2>/dev/null
|
||||
# fi
|
||||
if [ -n "$server" ]; then
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport != 53 ip daddr "$server" return 2>/dev/null
|
||||
fi
|
||||
|
||||
# if server != SERVER add SERVER to whitelist set (so tproxy won't touch it)
|
||||
if [ -n "$server" ]; then
|
||||
@@ -1100,9 +946,6 @@ tp_rule_nft() {
|
||||
$NFT add element ip ss_spec_mangle whitelist "{ $SERVER }" 2>/dev/null
|
||||
fi
|
||||
|
||||
# 显式豁免白名单集合中的 IP
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy ip daddr @whitelist return 2>/dev/null
|
||||
|
||||
# access control and tproxy rules
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @bplan return 2>/dev/null
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip saddr @fplan counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
|
||||
@@ -1122,9 +965,7 @@ tp_rule_nft() {
|
||||
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @ss_spec_wan_ac return 2>/dev/null
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @china return 2>/dev/null
|
||||
# 80 端口规则:非国内 IP 的 80 端口,走 TPROXY
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp udp dport 80 counter drop comment "\"$TAG\"" 2>/dev/null
|
||||
# 443 端口规则:非国内 IP 的 443 端口,走 TPROXY
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp udp dport 443 counter drop comment "\"$TAG\"" 2>/dev/null
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr != @ss_spec_wan_ac counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
|
||||
@@ -1134,9 +975,7 @@ tp_rule_nft() {
|
||||
$NFT add set ip ss_spec_mangle gfwlist '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
|
||||
fi
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @china return 2>/dev/null
|
||||
# 80 端口规则:非国内 IP 的 80 端口,走 TPROXY
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp udp dport 80 counter drop comment "\"$TAG\"" 2>/dev/null
|
||||
# 443 端口规则:非国内 IP 的 443 端口,走 TPROXY
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp udp dport 443 counter drop comment "\"$TAG\"" 2>/dev/null
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr @gfwlist counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
|
||||
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
|
||||
@@ -1149,8 +988,6 @@ tp_rule_nft() {
|
||||
# finally, ensure prerouting hook entry to jump to tproxy chain
|
||||
if ! $NFT list chain ip ss_spec_mangle prerouting >/dev/null 2>&1; then
|
||||
$NFT add chain ip ss_spec_mangle prerouting '{ type filter hook prerouting priority mangle; policy accept; }'
|
||||
else
|
||||
$NFT flush chain ip ss_spec_mangle prerouting 2>/dev/null
|
||||
fi
|
||||
|
||||
# add prerouting jump (idempotent)
|
||||
@@ -1219,14 +1056,8 @@ tp_rule_iptables() {
|
||||
do
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -d "$net" -j RETURN
|
||||
done
|
||||
# fake-ip 规则
|
||||
if [ "$ENABLE_FAKE_IP" = "1" ]; then
|
||||
$ipt -A SS_SPEC_TPROXY -p udp $MATCH_SET -d 198.18.0.0/16 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
fi
|
||||
|
||||
[ -n "$SERVER" ] && $ipt -A SS_SPEC_TPROXY -p udp ! --dport 53 -d "$SERVER" -j RETURN
|
||||
[ -n "$SERVER" ] && [ "$server" != "$SERVER" ] && ipset -! add whitelist "$SERVER"
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set whitelist dst -j RETURN
|
||||
if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then
|
||||
while IFS= read -r ip; do
|
||||
[ -n "$ip" ] && ipset add whitelist "$ip" -exist
|
||||
@@ -1234,7 +1065,6 @@ tp_rule_iptables() {
|
||||
fi
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set bplan src -j RETURN
|
||||
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set --match-set fplan src -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set blacklist dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
case "$RUNMODE" in
|
||||
router)
|
||||
ipset -! -R <<-EOF || return 1
|
||||
@@ -1243,27 +1073,15 @@ tp_rule_iptables() {
|
||||
EOF
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set ss_spec_wan_ac dst -j RETURN
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set china dst -j RETURN
|
||||
# 80 端口规则:非国内 IP 的 80 端口,走 TPROXY
|
||||
$ipt -A SS_SPEC_TPROXY -p udp --dport 80 -j DROP
|
||||
# 443 端口规则:非国内 IP 的 443 端口,走 TPROXY
|
||||
$ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j DROP
|
||||
# 保留海外 QUIC 代理
|
||||
# $ipt -A SS_SPEC_TPROXY -p udp --dport 80 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
# $ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
# $ipt -A SS_SPEC_TPROXY -p udp -m set ! --match-set china dst --dport 80 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
# $ipt -A SS_SPEC_TPROXY -p udp -m set ! --match-set china dst --dport 443 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
$ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j DROP
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set gmlan src -m set ! --match-set china dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set ! --match-set ss_spec_wan_ac dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
;;
|
||||
gfw)
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set china dst -j RETURN
|
||||
# 80 端口规则:非国内 IP 的 80 端口,走 TPROXY
|
||||
$ipt -A SS_SPEC_TPROXY -p udp --dport 80 -j DROP
|
||||
# 443 端口规则:非国内 IP 的 443 端口,走 TPROXY
|
||||
$ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j DROP
|
||||
# 保留海外 QUIC 代理
|
||||
# $ipt -A SS_SPEC_TPROXY -p udp --dport 80 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
# $ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
$ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j DROP
|
||||
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set --match-set gfwlist dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set gmlan src -m set ! --match-set china dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
|
||||
;;
|
||||
@@ -1645,7 +1463,7 @@ restore_from_persistence() {
|
||||
fi
|
||||
}
|
||||
|
||||
while getopts ":m:s:l:S:L:i:e:E:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do
|
||||
while getopts ":m:s:l:S:L:i:e:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do
|
||||
case "$arg" in
|
||||
m)
|
||||
Interface=$OPTARG
|
||||
@@ -1668,9 +1486,6 @@ while getopts ":m:s:l:S:L:i:e:E:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do
|
||||
e)
|
||||
EXT_ARGS=$OPTARG
|
||||
;;
|
||||
E)
|
||||
ENABLE_FAKE_IP=$OPTARG
|
||||
;;
|
||||
a)
|
||||
LAN_AC_IP=$OPTARG
|
||||
;;
|
||||
@@ -1874,14 +1689,6 @@ if [ -n "$local_port" ]; then
|
||||
|
||||
if [ "$USE_NFT" = "1" ]; then
|
||||
# NFTables
|
||||
# Detect Fake_IP Mode state change (execute first)
|
||||
ENABLE_FAKE_IP_STATE_FILE="/tmp/.last_enable_fake_ip"
|
||||
if [ -f "$ENABLE_FAKE_IP_STATE_FILE" ]; then
|
||||
LAST_ENABLE_FAKE_IP=$(cat "$ENABLE_FAKE_IP_STATE_FILE")
|
||||
else
|
||||
LAST_ENABLE_FAKE_IP=""
|
||||
fi
|
||||
|
||||
# Save previous TPROXY state file
|
||||
TPROXY_STATE_FILE="/tmp/.last_tproxy"
|
||||
if [ -f "$TPROXY_STATE_FILE" ]; then
|
||||
@@ -1971,13 +1778,6 @@ if [ -n "$local_port" ]; then
|
||||
LAST_XHTTP_FILE_HASH=""
|
||||
fi
|
||||
|
||||
# Check if Fake_IP Mode state has changed
|
||||
ENABLE_FAKE_IP_CHANGED=0
|
||||
if [ "$LAST_ENABLE_FAKE_IP" != "$ENABLE_FAKE_IP" ]; then
|
||||
loger 6 "Fake_IP Mode state changed: $LAST_ENABLE_FAKE_IP -> $ENABLE_FAKE_IP"
|
||||
ENABLE_FAKE_IP_CHANGED=1
|
||||
fi
|
||||
|
||||
# STEP 1: Check if TPROXY has value (1 or 2)
|
||||
if [ "$TPROXY" = "1" ] || [ "$TPROXY" = "2" ]; then
|
||||
TPROXY_HAS_VALUE=1
|
||||
@@ -2073,9 +1873,7 @@ if [ -n "$local_port" ]; then
|
||||
# 1. TPROXY changes from empty ↔ has value
|
||||
# 2. PROXY_PORTS changes from empty ↔ has value
|
||||
# 3. Any IP list has changed
|
||||
# 4. ENABLE_FAKE_IP state changed
|
||||
if [ "$ENABLE_FAKE_IP_CHANGED" = "1" ] || \
|
||||
[ "$TPROXY_HAS_VALUE" != "$LAST_HAS_VALUE" ] || \
|
||||
if [ "$TPROXY_HAS_VALUE" != "$LAST_HAS_VALUE" ] || \
|
||||
[ "$PROXY_HAS_VALUE" != "$LAST_PROXY_HAS_VALUE" ] || \
|
||||
[ "$ANY_IP_LIST_CHANGED" = "1" ]; then
|
||||
FORCE_RECREATE=1
|
||||
@@ -2093,7 +1891,6 @@ if [ -n "$local_port" ]; then
|
||||
fi
|
||||
|
||||
# STEP 4: Save current state
|
||||
echo "$ENABLE_FAKE_IP" > "$ENABLE_FAKE_IP_STATE_FILE"
|
||||
echo "$TPROXY" > "$TPROXY_STATE_FILE"
|
||||
echo "$PROXY_PORTS" > "$PROXY_PORTS_STATE_FILE"
|
||||
echo "$(normalize_ip_list "$WAN_BP_IP")" > "$WAN_BP_IP_STATE_FILE"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -3,7 +3,6 @@
|
||||
require "luci.sys"
|
||||
local ucursor = require "luci.model.uci".cursor()
|
||||
local json = require "luci.jsonc"
|
||||
local datatypes = require "luci.cbi.datatypes"
|
||||
|
||||
-- An omitted value reaches us either as a missing argument or as an empty
|
||||
-- string, depending on whether the caller quoted the expansion. Empty strings
|
||||
@@ -182,62 +181,6 @@ local function format_host_port(host, port)
|
||||
return host .. ":" .. tostring(port)
|
||||
end
|
||||
|
||||
local function format_dns_server(proxy)
|
||||
if not proxy or proxy == "" then
|
||||
return proxy
|
||||
end
|
||||
|
||||
local scheme, rest = proxy:match("^([a-zA-Z0-9%+%-%.]+)://(.+)$")
|
||||
if scheme then
|
||||
scheme = scheme:lower()
|
||||
if scheme == "tls" or scheme == "dot" then
|
||||
if not rest:match(":%d+$") then
|
||||
rest = rest .. ":853"
|
||||
end
|
||||
return "tls://" .. rest
|
||||
elseif scheme == "https" or scheme == "doh" or scheme == "dohl" or scheme == "doq" or scheme == "doql" then
|
||||
local host, path = rest:match("^([^/]+)(.*)$")
|
||||
host = host or rest
|
||||
path = path or ""
|
||||
if path == "" or path == "/" then
|
||||
path = "/dns-query"
|
||||
end
|
||||
return "https://" .. host .. path
|
||||
elseif scheme == "tcp" or scheme == "udp" then
|
||||
if not rest:match(":%d+$") then
|
||||
rest = rest .. ":53"
|
||||
end
|
||||
return scheme .. "://" .. rest
|
||||
else
|
||||
return proxy
|
||||
end
|
||||
end
|
||||
|
||||
local host, port = proxy:match("^([^:]+):(%d+)$")
|
||||
if host and port then
|
||||
if port == "853" then
|
||||
return "tls://" .. host .. ":853"
|
||||
elseif port == "443" then
|
||||
return "https://" .. host .. "/dns-query"
|
||||
elseif port == "53" then
|
||||
return host -- 53 端口输出纯 IP / Host (如 8.8.4.4)
|
||||
else
|
||||
return "udp://" .. host .. ":" .. port
|
||||
end
|
||||
end
|
||||
|
||||
-- 确保包含字母,防止把 IP 当成域名
|
||||
if proxy:match("[a-zA-Z]") and not proxy:match("[^%w%.%-]") then
|
||||
local path = ""
|
||||
if not proxy:match("/") then
|
||||
path = "/dns-query"
|
||||
end
|
||||
return "https://" .. proxy .. path
|
||||
end
|
||||
|
||||
return proxy
|
||||
end
|
||||
|
||||
-- 确保正确判断程序是否存在
|
||||
local function is_finded(e)
|
||||
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
|
||||
@@ -448,141 +391,31 @@ local Xray = {
|
||||
}
|
||||
|
||||
if server.type == "v2ray" and dns_mode == "7" and os.getenv("SSR_SWITCH_PROBE") ~= "1" then
|
||||
local raw_servers_str = builtin_dns_server or "8.8.4.4:53"
|
||||
local dns_host = builtin_dns_server:match("^([^:]+)") or "8.8.4.4"
|
||||
local dns_port = tonumber(builtin_dns_server:match(":(%d+)$")) or 53
|
||||
|
||||
local dns_servers = {}
|
||||
local host_list = {}
|
||||
local port_list = {}
|
||||
local first_host = nil
|
||||
local first_port = nil
|
||||
|
||||
-- 按逗号或空格分割多个 DNS 地址
|
||||
for raw_server in string.gmatch(raw_servers_str, "[^,%s\r\n]+") do
|
||||
if raw_server and raw_server ~= "" then
|
||||
-- 1. 先进行格式化处理
|
||||
local formatted = format_dns_server(raw_server)
|
||||
|
||||
local host = nil
|
||||
local port = nil
|
||||
|
||||
-- 2. 解析 host 和对应的实际端口
|
||||
if formatted:match("^https://") then
|
||||
local target = formatted:match("^https://([^/]+)")
|
||||
if target:match(":%d+$") then
|
||||
host, port = target:match("^([^:]+):(%d+)$")
|
||||
port = tonumber(port)
|
||||
else
|
||||
host = target
|
||||
port = 443
|
||||
end
|
||||
elseif formatted:match("^tls://") then
|
||||
local target = formatted:match("^tls://([^/]+)")
|
||||
if target:match(":%d+$") then
|
||||
host, port = target:match("^([^:]+):(%d+)$")
|
||||
port = tonumber(port)
|
||||
else
|
||||
host = target
|
||||
port = 853
|
||||
end
|
||||
elseif formatted:match("^tcp://") or formatted:match("^udp://") then
|
||||
local target = formatted:match("^[a-zA-Z0-9%+%-%.]+://([^/]+)")
|
||||
if target:match(":%d+$") then
|
||||
host, port = target:match("^([^:]+):(%d+)$")
|
||||
port = tonumber(port)
|
||||
else
|
||||
host = target
|
||||
port = 53
|
||||
end
|
||||
else
|
||||
if formatted:match(":%d+$") then
|
||||
host, port = formatted:match("^([^:]+):(%d+)$")
|
||||
port = tonumber(port)
|
||||
else
|
||||
host = formatted
|
||||
port = 53
|
||||
end
|
||||
end
|
||||
|
||||
-- 保存格式化后的 Xray.dns.servers 条目
|
||||
table.insert(dns_servers, formatted)
|
||||
|
||||
-- 分别收集 Host 与 Port
|
||||
if host and port then
|
||||
table.insert(host_list, host)
|
||||
table.insert(port_list, port)
|
||||
-- 保存第一个 DNS 的 host 和 port
|
||||
-- if not first_host then --dokodemo-door 入站的 address 单个地址使用。
|
||||
-- first_host = host --dokodemo-door 入站的 address 单个地址使用。
|
||||
-- first_port = port --dokodemo-door 入站的 address 单个地址使用。
|
||||
-- end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- 如果没有有效的 DNS 服务器,使用默认值
|
||||
if #dns_servers == 0 then
|
||||
table.insert(dns_servers, "8.8.4.4")
|
||||
table.insert(host_list, "8.8.4.4")
|
||||
table.insert(port_list, 53)
|
||||
-- first_host = "8.8.4.4" --dokodemo-door 入站的 address 单个地址使用。
|
||||
-- first_port = 53 --dokodemo-door 入站的 address 单个地址使用。
|
||||
end
|
||||
|
||||
-- 构建 dns 出站地址(支持逗号分隔的多个地址)
|
||||
-- 检查所有 DNS 端口是否一致
|
||||
--local outbound_address = "" --dokodemo-door 入站的 address 单个地址使用。
|
||||
local all_same_port = true
|
||||
local common_port = port_list[1]
|
||||
for i = 2, #port_list do
|
||||
if port_list[i] ~= common_port then
|
||||
all_same_port = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- 根据端口一致性计算 address 与 target_port
|
||||
local address_str = ""
|
||||
local target_port = 0
|
||||
|
||||
if all_same_port then
|
||||
-- outbound_address = table.concat(host_list, ",") --dokodemo-door 入站的 address 单个地址时使用。
|
||||
address_str = table.concat(host_list, ",")
|
||||
target_port = common_port
|
||||
else
|
||||
local combined = {}
|
||||
for i = 1, #host_list do
|
||||
table.insert(combined, string.format("%s:%d", host_list[i], port_list[i]))
|
||||
end
|
||||
-- outbound_address = table.concat(combined, ",") --dokodemo-door 入站的 address 单个地址时使用。
|
||||
address_str = table.concat(combined, ",")
|
||||
target_port = 0
|
||||
end
|
||||
|
||||
-- 构建 Xray.dns 配置
|
||||
Xray.dns = {
|
||||
queryStrategy = (dns_ipv4_only == "1") and "UseIPv4" or "UseIP",
|
||||
servers = dns_servers
|
||||
servers = {
|
||||
string.format("tcp://%s:%d", dns_host, dns_port)
|
||||
}
|
||||
}
|
||||
|
||||
-- 构建 dokodemo-door 入站
|
||||
table.insert(Xray.inbounds, {
|
||||
listen = "127.0.0.1",
|
||||
port = 5335,
|
||||
protocol = "dokodemo-door",
|
||||
settings = {
|
||||
address = address_str,
|
||||
-- address = first_host, -- 单个地址
|
||||
port = target_port,
|
||||
-- port = first_port, -- 对应端口
|
||||
address = dns_host,
|
||||
port = dns_port,
|
||||
network = "tcp,udp"
|
||||
},
|
||||
tag = "builtin-dns-in"
|
||||
})
|
||||
|
||||
|
||||
xray_builtin_dns = {
|
||||
address = address_str,
|
||||
-- address = outbound_address, --dokodemo-door 入站的 address 单个地址时使用。
|
||||
port = target_port
|
||||
address = dns_host,
|
||||
port = dns_port
|
||||
}
|
||||
end
|
||||
-- 传入连接
|
||||
|
||||
@@ -27,9 +27,6 @@ config server_subscribe
|
||||
option auto_update_week_time '*'
|
||||
option auto_update_day_time '2'
|
||||
option auto_update_min_time '0'
|
||||
option enable_mihomo '1'
|
||||
option enable_fake_ip '1'
|
||||
option sub_convert '1'
|
||||
option url_test_url 'https://www.google.com/generate_204'
|
||||
option user_agent 'v2rayN/9.99'
|
||||
option filter_words '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国'
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wwand
|
||||
PKG_VERSION:=1.6.8_p1
|
||||
PKG_RELEASE:=29
|
||||
PKG_VERSION:=1.6.8_p4
|
||||
PKG_RELEASE:=30
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
|
||||
PKG_SOURCE_VERSION:=f4e1c02e71936eb67981734f3a97a710f0aab264
|
||||
PKG_SOURCE_VERSION:=3d7585f3afe7eca05d55a179f4acc56653d8eac3
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
@@ -360,6 +360,7 @@ return baseclass.extend({
|
||||
visible: true,
|
||||
value: band
|
||||
},
|
||||
|
||||
hostname : {
|
||||
title: _('Hostname'),
|
||||
visible: true,
|
||||
|
||||
@@ -56,7 +56,10 @@ function probeColor(expr) {
|
||||
* shared with `luci-app-*`, and an app's unlayered `span { color: … !important }` outranks
|
||||
* a layer and a plain inline style alike. A probe that loses its own colour reports the
|
||||
* app's, which then becomes the admin's saved axis on the next confirm. */
|
||||
_probe = E('span', { 'aria-hidden': 'true' });
|
||||
/* the id is this probe's escape from fs-router.js's strayBodyNode() (issue #56): it is
|
||||
* appended once and never removed, so without an fs-* id it would read as body litter and
|
||||
* force every navigation away from this page into a full load. */
|
||||
_probe = E('span', { 'id': 'fs-appearance-probe', 'aria-hidden': 'true' });
|
||||
_probe.style.cssText = 'position:fixed!important;left:-9999px!important;top:0!important;'
|
||||
+ 'width:0!important;height:0!important;overflow:hidden!important;'
|
||||
+ 'pointer-events:none!important;';
|
||||
|
||||
@@ -110,6 +110,12 @@ let _probe = null;
|
||||
function resolveLen(token, dflt) {
|
||||
if (!_probe) {
|
||||
_probe = document.createElement('div');
|
||||
/* the id is fs-router.js's strayBodyNode() (issue #56) reading an fs-* id as "ours", which is
|
||||
* a different question from the chrome mark above: this stays unmarked (no data-fs-chrome,
|
||||
* so the fence still does not spare it from a hostile !important) but still needs SOME fs-*
|
||||
* name, or an element appended once and never removed would read as body litter and force
|
||||
* every navigation off this page into a full load. */
|
||||
_probe.id = 'fs-chrome-geom-probe';
|
||||
_probe.setAttribute('aria-hidden', 'true');
|
||||
/* out of flow, no box, no ink: it must never affect layout, scroll extent or hit-testing */
|
||||
_probe.style.cssText = 'position:absolute!important;visibility:hidden!important;' +
|
||||
|
||||
@@ -338,6 +338,66 @@ function discard(el) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- a page that leaves nodes as direct children of <body> hands the next page over by full load,
|
||||
* the same way an invasive foreign sheet does (docs/third-party-apps.md, Rule 2 extended from CSS to
|
||||
* the DOM — issue #56) ----
|
||||
*
|
||||
* luci-app-bandix's render() appends 7 tooltip/modal nodes to document.body on every visit,
|
||||
* unconditionally (reproduced on owrt2512 and owrt2410) — nothing here sweeps them; the reasoning is
|
||||
* docs/spa-router.md, "Foreign view DOM". The recorder is header.ut's first <body> <script>, filling
|
||||
* window.__fsBodyAdds; this file only reads it. */
|
||||
|
||||
/* -> true if `el` is a node the theme must treat as stray body litter: not the theme's own chrome,
|
||||
* not something stock LuCI parks there itself, not a node type that can never paint. Exported and
|
||||
* pure (no DOM writes, no window.__fsBodyAdds read) so tests/body-litter.test.mjs can drive it
|
||||
* without a router or a recorder. */
|
||||
function strayBodyNode(el) {
|
||||
if (!el || el.nodeType !== 1) return false;
|
||||
/* never rendered on their own, wherever a script parks them */
|
||||
switch (el.nodeName) {
|
||||
case 'SCRIPT': case 'STYLE': case 'LINK': case 'TEMPLATE': case 'NOSCRIPT': case 'META':
|
||||
return false;
|
||||
}
|
||||
/* zone 1, ours (docs/third-party-apps.md): the chrome mark, or an fs-* id/class. All of these
|
||||
* parent straight to <body> and must never be read as litter: this file's own #fs-nav-progress,
|
||||
* fs-search.js's #fs-search-ov (which also carries the mark), and the geometry/colour probes
|
||||
* fs-chrome.js and fs-appearance.js each park on <body> once and never remove — an fs-* id is
|
||||
* what tells them from bandix's own unmarked nodes below. */
|
||||
/* read through `dataset`, not a literal `hasAttribute()` call: `tools/chrome-fence.mjs` counts
|
||||
* every single-quoted occurrence of the chrome mark's own name (as a JS string) across this
|
||||
* whole tree as a JS-BUILT chrome root (EXPECT_JS_ROOTS) — right for fs-search.js's palette,
|
||||
* which mounts one, and a false positive here, which only READS the mark on somebody else's
|
||||
* node and builds nothing. camelCase avoids the exact spelling the gate scans for. */
|
||||
if (el.dataset && el.dataset.fsChrome !== undefined) return false;
|
||||
if (el.id && el.id.indexOf('fs-') === 0) return false;
|
||||
for (const c of el.classList) if (c.indexOf('fs-') === 0) return false;
|
||||
/* stock LuCI's own ui.js parents these to <body> at its own __init__ and keeps them for the life
|
||||
* of the document: #modal_overlay (showModal's host) and the shared .cbi-tooltip */
|
||||
if (el.id === 'modal_overlay') return false;
|
||||
if (el.classList.contains('cbi-tooltip')) return false;
|
||||
/* ui.js's handleDownload() appends a hidden `<a download>`, clicks it once and revokes its object
|
||||
* URL, but never removes the element itself. Every app that downloads the same way (wireguard,
|
||||
* filemanager, snmpd, banip/adblock's feed export) calls a.remove() of its own accord and is
|
||||
* pruned by bodyLittered() below before ever reaching this function. */
|
||||
if (el.nodeName === 'A' && el.hasAttribute('download') && el.style.display === 'none') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -> true iff the document holds a stray body node right now. A recording only ever grows — the
|
||||
* recorder cannot know a node was later removed or moved inside #view — so this is also the one
|
||||
* place that prunes it: an entry whose element is no longer a direct child of <body> is dropped for
|
||||
* good, or the list would grow for the life of the document and re-judge nodes nobody can reach. */
|
||||
function bodyLittered() {
|
||||
const adds = window.__fsBodyAdds;
|
||||
if (!adds || !adds.length) return false;
|
||||
let littered = false;
|
||||
for (let i = adds.length - 1; i >= 0; i--) {
|
||||
if (!document.body || adds[i].parentNode !== document.body) { adds.splice(i, 1); continue; }
|
||||
if (strayBodyNode(adds[i])) littered = true;
|
||||
}
|
||||
return littered;
|
||||
}
|
||||
|
||||
let _wired = false;
|
||||
/* the pathname whose view is currently rendered; popstate compares against it to tell a real
|
||||
* navigation from a fragment change */
|
||||
@@ -887,6 +947,10 @@ function navigate(pathname, push, kbd) {
|
||||
* only exit leaving both pages correct is a real navigation (fs-sheets.js) */
|
||||
if (sheets.documentPoisoned()) return false;
|
||||
|
||||
/* …and a page that left its own nodes as direct children of <body> is spent the same way — Rule 2
|
||||
* extended to the DOM, issue #56. See strayBodyNode()/bodyLittered() above. */
|
||||
if (bodyLittered()) return false;
|
||||
|
||||
/* …and a document whose session has died is spent the same way: the only page it can render
|
||||
* correctly is the login form, and only a real navigation gets there (watchSession) */
|
||||
if (_expired) return false;
|
||||
@@ -1495,5 +1559,13 @@ return baseclass.extend({
|
||||
/* fs-search warms its recents and the arrow-key-highlighted result, neither of which the
|
||||
* pointer/focus triggers above can see. The edge points search -> router, because the router
|
||||
* must keep no dependency on the palette. */
|
||||
prefetchSegs
|
||||
prefetchSegs,
|
||||
/* out-of-package, like the three probe exports above (contractBreaks, clearViewIntervals,
|
||||
* sessionExpired) — prefetchSegs just above is not one of them, it is a real in-package export
|
||||
* fs-search.js calls. tests/body-litter.test.mjs drives the classifier and the counter directly,
|
||||
* and navigate() itself to prove the early return actually reaches them rather than just sitting
|
||||
* beside it unused */
|
||||
strayBodyNode, /* fs:probe */
|
||||
bodyLittered, /* fs:probe */
|
||||
navigate, /* fs:probe */
|
||||
});
|
||||
@@ -85,6 +85,46 @@
|
||||
#}
|
||||
{% const fs_page = entityencode(join('-', length(ctx.path) ? ctx.path : ctx.request_path), true) %}
|
||||
<body class="lang_{{ dispatcher.lang }}" data-page="{{ fs_page }}">
|
||||
<script>
|
||||
/* Records every node a script parks directly on <body> (issue #56): luci-app-bandix's
|
||||
* render() appends 7 tooltip/modal nodes unconditionally on every visit, never
|
||||
* checking whether they already exist. fs-router.js's strayBodyNode()/bodyLittered()
|
||||
* read window.__fsBodyAdds to decide whether the document is still fit to navigate
|
||||
* away from — Rule 2 (docs/third-party-apps.md), extended from CSS to the DOM. First
|
||||
* byte of <body>, before any chrome element or the dispatched page's own <div id="view">.
|
||||
* Own property of this <body> instance, never Node.prototype, and its gap:
|
||||
* docs/spa-router.md, "Foreign view DOM". */
|
||||
(() => {
|
||||
const body = document.body;
|
||||
if (!body || body.__fsBodyHooked) return;
|
||||
body.__fsBodyHooked = true;
|
||||
const adds = window.__fsBodyAdds || (window.__fsBodyAdds = []);
|
||||
const names = [ 'appendChild', 'insertBefore', 'replaceChild', 'append', 'prepend',
|
||||
'insertAdjacentElement', 'insertAdjacentHTML' ];
|
||||
names.forEach((name) => {
|
||||
const orig = body[name];
|
||||
if (typeof orig !== 'function') return;
|
||||
try {
|
||||
Object.defineProperty(body, name, {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value() {
|
||||
const before = Array.prototype.slice.call(this.children);
|
||||
const ret = orig.apply(this, arguments);
|
||||
const after = this.children;
|
||||
for (let i = 0; i < after.length; i++) {
|
||||
if (before.indexOf(after[i]) === -1) adds.push(after[i]);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
});
|
||||
}
|
||||
/* a host that refuses to redefine this one name leaves it unwrapped; the rest
|
||||
* still are */
|
||||
catch (e) {}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{#
|
||||
The wallpaper pattern's paint surface: 15-wallpaper.css masks this element so an
|
||||
uploaded SVG takes the theme's ink. A mask applies to an element's whole rendering,
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||
|
||||
PKG_NAME:=mise
|
||||
PKG_VERSION:=2026.9.13
|
||||
PKG_RELEASE:=20
|
||||
PKG_VERSION:=2026.9.14
|
||||
PKG_RELEASE:=21
|
||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -2,8 +2,8 @@ include $(TOPDIR)/rules.mk
|
||||
include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=tcp-brutal
|
||||
PKG_VERSION:=2.0.0
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=2.0.1
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/apernet/tcp-brutal/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#
|
||||
# eSIM fleet management for wwand: the router as the SGP.32 IoT Profile
|
||||
# Assistant of an eIM. A wwand PLUGIN (wwand plugins.uc), kept out of the
|
||||
# wwand sources on purpose: it is not meant for upstream.
|
||||
#
|
||||
# Sources live in https://github.com/ddimension/wwand-ipa, tagged vX.Y.Z. Pin
|
||||
# a new commit with `scripts/bump-source.sh wwand-ipa <tag|commit>`.
|
||||
#
|
||||
# Built from a git PKG_SOURCE, so, like luci-app-wwand, installed explicitly
|
||||
# from $(PKG_BUILD_DIR) and not through luci.mk.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand-ipa
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand-ipa.git
|
||||
PKG_SOURCE_VERSION:=fc0fad051213f23d10864f26ebc7205ae5c0a451
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
UCDIR:=/usr/share/ucode/wwand
|
||||
|
||||
define Package/wwand-ipa
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=WWAN
|
||||
TITLE:=eSIM fleet management (SGP.32 eIM) for wwand
|
||||
# wwand-esim: its bridge carries the assistant's APDUs; wwand-ipad: the
|
||||
# assistant itself (onomondo-ipa with the stdio card backend)
|
||||
DEPENDS:=+wwand-esim +wwand-ipad
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/wwand-ipa/description
|
||||
Lets an eIM manage the modem's eSIM: wwand polls it (after the connection
|
||||
comes up, then every ipa_interval seconds) with the onomondo-ipa assistant,
|
||||
relays the assistant's card traffic over the modem's APDU channel, and after
|
||||
a profile change resets the SIM (or the modem) and waits for the session on
|
||||
the new profile before the result is reported. Runs against an SGP.22
|
||||
consumer eUICC (the assistant's IoT eUICC emulation); the eIM must accept
|
||||
that mode. Enable per modem with `wwandctl ipa [modem] eim <file>`. While
|
||||
enabled, manual profile changes on that card are refused unless forced.
|
||||
endef
|
||||
|
||||
define Package/luci-app-wwand-ipa
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=eSIM fleet management page for wwand-ipa
|
||||
DEPENDS:=+luci-base +wwand-ipa
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/luci-app-wwand-ipa/description
|
||||
Network -> eSIM Fleet: per modem, where the eIM polling stands, what the
|
||||
last run did to the card, and a poll on request.
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
# /etc/wwand/ipa holds each card's nvstate (in emulation: its eIM
|
||||
# configuration and replay counter) and usually the eIM configuration file.
|
||||
# Losing it in a sysupgrade cuts the card off its eIM, so it is kept through
|
||||
# keep.d, NOT conffiles: sysupgrade treats conffiles as file+checksum pairs of
|
||||
# what the package shipped, and these files are created at runtime; keep.d
|
||||
# entries go through find, which takes the whole directory (base-files
|
||||
# sbin/sysupgrade list_conffiles / list_static_conffiles, OpenWrt bacda03b76,
|
||||
# 2026-07-19).
|
||||
define Package/wwand-ipa/install
|
||||
$(INSTALL_DIR) $(1)$(UCDIR)/plugins $(1)$(UCDIR)/ctl
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/plugins/ipa.uc $(1)$(UCDIR)/plugins/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/ctl/ipa.uc $(1)$(UCDIR)/ctl/
|
||||
$(INSTALL_DIR) $(1)/lib/upgrade/keep.d
|
||||
echo /etc/wwand/ipa/ > $(1)/lib/upgrade/keep.d/wwand-ipa
|
||||
endef
|
||||
|
||||
# named, not globbed (the rule luci-app-wwand follows)
|
||||
define Package/luci-app-wwand-ipa/install
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/wwand $(1)/www/luci-static/resources/wwand
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/luci/htdocs/luci-static/resources/view/wwand/ipa.js \
|
||||
$(1)/www/luci-static/resources/view/wwand/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/luci/htdocs/luci-static/resources/wwand/ipafmt.js \
|
||||
$(1)/www/luci-static/resources/wwand/
|
||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/luci/root/usr/share/luci/menu.d/luci-app-wwand-ipa.json \
|
||||
$(1)/usr/share/luci/menu.d/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/luci/root/usr/share/rpcd/acl.d/luci-app-wwand-ipa.json \
|
||||
$(1)/usr/share/rpcd/acl.d/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,wwand-ipa))
|
||||
$(eval $(call BuildPackage,luci-app-wwand-ipa))
|
||||
@@ -0,0 +1,154 @@
|
||||
# onomondo-ipa, the GSMA SGP.32 IoT Profile Assistant, built for wwand-ipa.
|
||||
#
|
||||
# The assistant polls an eIM (the fleet server) for eUICC packages and runs
|
||||
# them on the card. Upstream reaches the card through PC/SC; patch 100 adds a
|
||||
# card backend that speaks lpac's stdio APDU protocol instead, so wwand's
|
||||
# esim_bridge relays its APDUs over the modem's own channel (QMI UIM / MBIM
|
||||
# UICC / AT) exactly as it does for lpac, and adds -H: a profile change is
|
||||
# handed to the host, which resets the SIM and waits for the new session. The
|
||||
# patch carries a test for the card backend (tests/scard_stdio). The package
|
||||
# build does not run it (BUILD_TESTING=OFF, target ipa only); on a host:
|
||||
# cmake -S . -B build -DIPA_SCARD_STDIO=ON && cmake --build build && ctest --test-dir build
|
||||
# The -H wiring in main.c has no test of its own: exercising it takes an eIM
|
||||
# that sends a profile change.
|
||||
#
|
||||
# TLS: the same minimal static wolfSSL + libcurl bundle as wwand-lpac, for the
|
||||
# same reason (OpenWrt's mbedtls lacks curves the GSMA PKI uses, and
|
||||
# libcurl+OpenSSL is ~6.4 MB). The eIM is reached over HTTPS with the system
|
||||
# CA bundle unless the eIM configuration says otherwise.
|
||||
#
|
||||
# Upstream implements SGP.32 v1.0; wwand always runs it in IoT eUICC emulation
|
||||
# (-E), i.e. against an SGP.22 consumer eUICC. See wwand's src-ucode/ipa.uc.
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand-ipad
|
||||
PKG_VERSION:=0.0.1_git20260901
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/onomondo/onomondo-ipa.git
|
||||
PKG_SOURCE_VERSION:=6aaeb38a569e648c8305aa5bd4d300cc1ea6822a
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only curl GPL-3.0-or-later
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=
|
||||
|
||||
# bundled, statically-linked dependencies (keep in step with wwand-lpac)
|
||||
WOLFSSL_VERSION:=5.9.2-stable
|
||||
CURL_VERSION:=8.21.0
|
||||
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
|
||||
# wolfSSL's SP math carries inline assembly that MIPS16 cannot encode
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Download/wolfssl
|
||||
FILE:=wolfssl-$(WOLFSSL_VERSION).tar.gz
|
||||
URL:=https://github.com/wolfSSL/wolfssl/archive/v$(WOLFSSL_VERSION)
|
||||
HASH:=skip
|
||||
endef
|
||||
$(eval $(call Download,wolfssl))
|
||||
|
||||
define Download/curl
|
||||
FILE:=curl-$(CURL_VERSION).tar.xz
|
||||
URL:=https://github.com/curl/curl/releases/download/curl-8_21_0
|
||||
HASH:=skip
|
||||
endef
|
||||
$(eval $(call Download,curl))
|
||||
|
||||
define Package/wwand-ipad
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=WWAN
|
||||
TITLE:=SGP.32 IoT Profile Assistant (onomondo-ipa) for wwand-ipa
|
||||
URL:=https://github.com/onomondo/onomondo-ipa
|
||||
# the eIM is an HTTPS server with an ordinary certificate chain
|
||||
DEPENDS:=+ca-bundle
|
||||
endef
|
||||
|
||||
define Package/wwand-ipad/description
|
||||
onomondo-ipa, the GSMA SGP.32 IoT Profile Assistant, built with a card
|
||||
backend that reaches the eUICC through wwand (lpac's stdio APDU protocol)
|
||||
instead of PC/SC, and with a minimal static wolfSSL + libcurl linked in.
|
||||
Installed as /usr/lib/wwand/ipad; wwand-ipa runs it. Implements SGP.32 v1.0
|
||||
and is run in IoT eUICC emulation against an SGP.22 consumer eUICC.
|
||||
endef
|
||||
|
||||
PREFIX:=$(PKG_BUILD_DIR)/.bundle
|
||||
WOLFSSL_DIR:=$(PKG_BUILD_DIR)/wolfssl-$(WOLFSSL_VERSION)
|
||||
CURL_DIR:=$(PKG_BUILD_DIR)/curl-$(CURL_VERSION)
|
||||
IPA_BUILD:=$(PKG_BUILD_DIR)/.ipa-build
|
||||
|
||||
LTO_CFLAGS:=-flto=auto -ffat-lto-objects
|
||||
BUNDLE_AR:=AR="$(TARGET_CROSS)gcc-ar" RANLIB="$(TARGET_CROSS)gcc-ranlib"
|
||||
|
||||
# unpack + patch the assistant, then the bundled wolfSSL + curl beside it
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
$(TAR) -C $(PKG_BUILD_DIR) -xzf $(DL_DIR)/wolfssl-$(WOLFSSL_VERSION).tar.gz
|
||||
$(TAR) -C $(PKG_BUILD_DIR) -xJf $(DL_DIR)/curl-$(CURL_VERSION).tar.xz
|
||||
endef
|
||||
|
||||
Build/Configure:=
|
||||
|
||||
define Build/Compile
|
||||
# 1) minimal static wolfSSL (the wwand-lpac recipe)
|
||||
( cd $(WOLFSSL_DIR) && ./autogen.sh )
|
||||
( cd $(WOLFSSL_DIR) && ./configure --host=$(GNU_TARGET_NAME) --prefix=$(PREFIX) \
|
||||
--enable-static --disable-shared \
|
||||
--enable-curl --enable-tls13 --enable-brainpool --enable-ecccustcurves \
|
||||
--enable-curve25519 --disable-dtls --disable-oldtls \
|
||||
--disable-examples --disable-crypttests \
|
||||
CC="$(TARGET_CC)" $(BUNDLE_AR) \
|
||||
CFLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -DWOLFSSL_NO_ASN_STRICT" )
|
||||
+$(MAKE) -C $(WOLFSSL_DIR)
|
||||
$(MAKE) -C $(WOLFSSL_DIR) install
|
||||
|
||||
# 2) minimal static libcurl (HTTP/HTTPS only) against it. HTTP stays: an
|
||||
# eIM configuration may name a plain-HTTP lab eIM (the assistant's -S).
|
||||
( cd $(CURL_DIR) && \
|
||||
PKG_CONFIG_PATH="$(PREFIX)/lib/pkgconfig" PKG_CONFIG_LIBDIR="$(PREFIX)/lib/pkgconfig" \
|
||||
./configure --host=$(GNU_TARGET_NAME) --prefix=$(PREFIX) --with-wolfssl=$(PREFIX) \
|
||||
--enable-static --disable-shared \
|
||||
--disable-ftp --disable-file --disable-ldap --disable-ldaps --disable-rtsp \
|
||||
--disable-dict --disable-telnet --disable-tftp --disable-pop3 --disable-imap \
|
||||
--disable-smtp --disable-gopher --disable-mqtt --disable-smb --disable-ntlm \
|
||||
--disable-manual --disable-unix-sockets --disable-progress-meter \
|
||||
--without-libpsl --without-zlib --without-nghttp2 --without-brotli \
|
||||
--without-zstd --without-libidn2 --without-librtmp --without-ca-path \
|
||||
--with-ca-bundle=/etc/ssl/certs/ca-certificates.crt \
|
||||
CC="$(TARGET_CC)" $(BUNDLE_AR) \
|
||||
CFLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -DSIZEOF_LONG_LONG=8" \
|
||||
CPPFLAGS="-I$(PREFIX)/include" LDFLAGS="-L$(PREFIX)/lib" LIBS="-lm" )
|
||||
+$(MAKE) -C $(CURL_DIR)
|
||||
$(MAKE) -C $(CURL_DIR) install
|
||||
|
||||
# 3) the assistant with the stdio card backend, statically against both.
|
||||
# AR/RANLIB go in as full paths: CMake took the bare name as a path relative
|
||||
# to its build directory and wrote `.ipa-build/aarch64-openwrt-linux-musl-gcc-ar`
|
||||
# into libasn's link.txt, which failed with "Error running link command: no
|
||||
# such file or directory" (SDK snapshot, aarch64, 2026-09-26).
|
||||
rm -rf $(IPA_BUILD)
|
||||
mkdir -p $(IPA_BUILD)
|
||||
( cd $(IPA_BUILD) && cmake $(PKG_BUILD_DIR) \
|
||||
-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=$(ARCH) \
|
||||
-DCMAKE_C_COMPILER="$(TARGET_CROSS)gcc" \
|
||||
-DCMAKE_AR="$(TOOLCHAIN_DIR)/bin/$(TARGET_CROSS)gcc-ar" \
|
||||
-DCMAKE_RANLIB="$(TOOLCHAIN_DIR)/bin/$(TARGET_CROSS)gcc-ranlib" \
|
||||
-DCMAKE_C_FLAGS="$(TARGET_CFLAGS) $(LTO_CFLAGS) -ffunction-sections -fdata-sections -I$(PREFIX)/include" \
|
||||
-DCMAKE_EXE_LINKER_FLAGS="$(TARGET_LDFLAGS) $(LTO_CFLAGS) -Wl,--gc-sections -L$(PREFIX)/lib" \
|
||||
-DCMAKE_C_STANDARD_LIBRARIES="-L$(PREFIX)/lib -lwolfssl -lm" \
|
||||
-DIPA_SCARD_STDIO=ON -DBUILD_TESTING=OFF )
|
||||
+$(MAKE) -C $(IPA_BUILD) ipa
|
||||
endef
|
||||
|
||||
define Package/wwand-ipad/install
|
||||
$(INSTALL_DIR) $(1)/usr/lib/wwand
|
||||
$(INSTALL_BIN) $(IPA_BUILD)/src/ipa/ipa $(1)/usr/lib/wwand/ipad
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,wwand-ipad))
|
||||
@@ -0,0 +1,833 @@
|
||||
diff --git a/include/onomondo/ipa/scard_stdio.h b/include/onomondo/ipa/scard_stdio.h
|
||||
new file mode 100644
|
||||
index 0000000..fa9ec54
|
||||
--- /dev/null
|
||||
+++ b/include/onomondo/ipa/scard_stdio.h
|
||||
@@ -0,0 +1,13 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 André Valentin <avalentin@marcant.net>
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
+ */
|
||||
+
|
||||
+#pragma once
|
||||
+
|
||||
+/*! Hand a profile change to the host and wait until it has restored the data
|
||||
+ * connection, then re-open the card channel the host's SIM reset closed.
|
||||
+ * \returns 1 when the host reports the connection back, 0 when it gave up
|
||||
+ * waiting, negative when the host is gone. */
|
||||
+int ipa_scard_stdio_await_online(void);
|
||||
diff --git a/src/ipa/CMakeLists.txt b/src/ipa/CMakeLists.txt
|
||||
index 2cf4a2b..b4ee0e5 100644
|
||||
--- a/src/ipa/CMakeLists.txt
|
||||
+++ b/src/ipa/CMakeLists.txt
|
||||
@@ -5,7 +5,15 @@ add_subdirectory(libasn)
|
||||
add_subdirectory(libipa)
|
||||
|
||||
add_library(http STATIC http.c)
|
||||
-add_library(scard STATIC scard.c)
|
||||
+option(IPA_SCARD_STDIO "Reach the eUICC through a host process (lpac stdio protocol) instead of PC/SC" OFF)
|
||||
+if (IPA_SCARD_STDIO)
|
||||
+ add_library(scard STATIC scard_stdio.c)
|
||||
+ target_compile_definitions(scard PUBLIC IPA_SCARD_STDIO)
|
||||
+ set(IPA_SCARD_LIBS "")
|
||||
+else()
|
||||
+ add_library(scard STATIC scard.c)
|
||||
+ set(IPA_SCARD_LIBS pcsclite)
|
||||
+endif()
|
||||
|
||||
target_include_directories(http PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
target_include_directories(scard PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
@@ -16,7 +24,7 @@ set_property(TARGET ipa PROPERTY C_STANDARD 99)
|
||||
target_compile_options(ipa PRIVATE -Wall)
|
||||
|
||||
target_include_directories(ipa PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
-target_link_libraries(ipa libipa http scard pcsclite curl)
|
||||
+target_link_libraries(ipa libipa http scard ${IPA_SCARD_LIBS} curl)
|
||||
|
||||
if (M32)
|
||||
set_target_properties(http PROPERTIES COMPILE_FLAGS "-m32" LINK_FLAGS "-m32")
|
||||
diff --git a/src/ipa/main.c b/src/ipa/main.c
|
||||
index d4316f2..5dfa703 100644
|
||||
--- a/src/ipa/main.c
|
||||
+++ b/src/ipa/main.c
|
||||
@@ -17,6 +17,9 @@
|
||||
#include <onomondo/ipa/utils.h>
|
||||
#include <onomondo/ipa/log.h>
|
||||
#include <onomondo/ipa/ipad.h>
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+#include <onomondo/ipa/scard_stdio.h>
|
||||
+#endif
|
||||
|
||||
#define DEFAULT_READER_NUMBER 0
|
||||
#define DEFAULT_CHANNEL_NUMBER 1
|
||||
@@ -58,6 +61,9 @@ static void print_help(void)
|
||||
printf(" -I .................. disable SSL certificate verification (insecure)\n");
|
||||
printf(" -E .................. emulate IoT eUICC (compatibility mode to use consumer eUICCs)\n");
|
||||
printf(" -1 .................. force the IPAd to process only one eUICC package (debug, use with caution)\n");
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+ printf(" -H .................. hand profile changes to the host and wait for it to restore connectivity\n");
|
||||
+#endif
|
||||
}
|
||||
|
||||
struct ipa_buf *load_ber_from_file(char *dir, char *file)
|
||||
@@ -134,6 +140,22 @@ static void sig_usr1(int signum)
|
||||
running = false;
|
||||
}
|
||||
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+/* The host ends a run it has given up on with SIGTERM. Without a handler that
|
||||
+ * kills the process before ipa_free_ctx() hands back the nvstate, and in IoT
|
||||
+ * eUICC emulation the nvstate IS the eIM configuration and the replay counter.
|
||||
+ * No SA_RESTART: a read blocked on the host must return, so the card layer
|
||||
+ * fails the current exchange and main() reaches the save below. */
|
||||
+static void install_sigterm(void)
|
||||
+{
|
||||
+ struct sigaction sa = { 0 };
|
||||
+
|
||||
+ sa.sa_handler = sig_usr1;
|
||||
+ sigemptyset(&sa.sa_mask);
|
||||
+ sigaction(SIGTERM, &sa, NULL);
|
||||
+}
|
||||
+#endif
|
||||
+
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
struct ipa_config cfg = { 0 };
|
||||
@@ -146,8 +168,12 @@ int main(int argc, char **argv)
|
||||
struct ipa_buf *nvstate_load = NULL;
|
||||
struct ipa_buf *nvstate_save = NULL;
|
||||
bool getopt_one_euicc_pkg_only = false;
|
||||
+ bool getopt_host_online = false;
|
||||
|
||||
signal(SIGUSR1, sig_usr1);
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+ install_sigterm();
|
||||
+#endif
|
||||
|
||||
printf("IPAd!\n");
|
||||
|
||||
@@ -159,7 +185,7 @@ int main(int argc, char **argv)
|
||||
|
||||
/* Overwrite configuration values with user defined parameters */
|
||||
while (1) {
|
||||
- opt = getopt(argc, argv, "ht:e:r:c:f:mn:C:SIEy:a1");
|
||||
+ opt = getopt(argc, argv, "ht:e:r:c:f:mn:C:SIEy:a1H");
|
||||
if (opt == -1)
|
||||
break;
|
||||
|
||||
@@ -210,6 +236,9 @@ int main(int argc, char **argv)
|
||||
case '1':
|
||||
getopt_one_euicc_pkg_only = true;
|
||||
break;
|
||||
+ case 'H':
|
||||
+ getopt_host_online = true;
|
||||
+ break;
|
||||
default:
|
||||
printf("unhandled option: %c!\n", opt);
|
||||
break;
|
||||
@@ -284,6 +313,19 @@ int main(int argc, char **argv)
|
||||
case IPA_POLL_AGAIN_WHEN_ONLINE:
|
||||
/* ipa_poll asks us to wait with the next poll cycle until we have a stable IP
|
||||
* connection. In this example we assume that IP connectivity is always available. */
|
||||
+#ifdef IPA_SCARD_STDIO
|
||||
+ /* On a router it is not: the modem keeps the old profile until the host resets
|
||||
+ * the SIM, and the data session has to come back on the new one. The process
|
||||
+ * stays alive meanwhile, because the pending result (and a rollback, should the
|
||||
+ * eIM stay unreachable) lives in the context, not in the nvstate. Whatever the
|
||||
+ * host answers, the next ipa_poll is the one that decides: it reports to the
|
||||
+ * eIM, or rolls the profile back when it cannot. */
|
||||
+ if (getopt_host_online && ipa_scard_stdio_await_online() < 0) {
|
||||
+ IPA_LOGP(SMAIN, LERROR, "host went away during a profile change\n");
|
||||
+ rc = -EIO;
|
||||
+ goto leave;
|
||||
+ }
|
||||
+#endif
|
||||
IPA_LOGP(SMAIN, LINFO, "poll cycle continues normally (profile change)\n");
|
||||
rc = 0;
|
||||
break;
|
||||
diff --git a/src/ipa/scard_stdio.c b/src/ipa/scard_stdio.c
|
||||
new file mode 100644
|
||||
index 0000000..5b58991
|
||||
--- /dev/null
|
||||
+++ b/src/ipa/scard_stdio.c
|
||||
@@ -0,0 +1,513 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 André Valentin <avalentin@marcant.net>
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
+ *
|
||||
+ * Smartcard backend for onomondo-ipa that reaches the eUICC through a HOST
|
||||
+ * PROCESS instead of PC/SC: every card operation is one JSON line on stdout,
|
||||
+ * and the answer is one JSON line on stdin. The wire format is lpac's stdio
|
||||
+ * APDU driver (lpac driver/apdu/stdio.c, v2.3.0) byte for byte, so the host
|
||||
+ * side that already bridges lpac -- wwand's esim_bridge, which answers from
|
||||
+ * the modem's own APDU channel (QMI UIM / MBIM UICC / AT) -- bridges this too:
|
||||
+ *
|
||||
+ * -> {"type":"apdu","payload":{"func":"logic_channel_open","param":"<AID>"}}
|
||||
+ * <- {"type":"apdu","payload":{"ecode":<channel or -1>}}
|
||||
+ * -> {"type":"apdu","payload":{"func":"transmit","param":"<C-APDU>"}}
|
||||
+ * <- {"type":"apdu","payload":{"ecode":0,"data":"<R-APDU incl. SW>"}}
|
||||
+ *
|
||||
+ * The two sides do not speak the same card dialect, and this file is where
|
||||
+ * that is reconciled:
|
||||
+ *
|
||||
+ * - libipa talks raw T=0 (libipa/euicc.c): TERMINAL CAPABILITY on the basic
|
||||
+ * channel, MANAGE CHANNEL to open one, SELECT ISD-R on it and then STORE
|
||||
+ * DATA / GET RESPONSE with the channel number in CLA. A modem's APDU
|
||||
+ * service does not let the host do any of that directly: QMI UIM opens a
|
||||
+ * logical channel BY AID (OPEN_LOGICAL_CHANNEL, which selects it in the
|
||||
+ * same step) and addresses the channel by id. So MANAGE CHANNEL is
|
||||
+ * answered here, the channel is really opened when the SELECT names the
|
||||
+ * AID, and CLA is rewritten to the channel the modem handed out.
|
||||
+ * - The modem completes GET RESPONSE itself and returns the whole answer with
|
||||
+ * 9000 (the QMI UIM long-response path in wwand's sim.uc reassembles even
|
||||
+ * the chunked ones). libipa insists on seeing 61xx after the last STORE
|
||||
+ * DATA block and fetching with GET RESPONSE, and it rejects a block whose
|
||||
+ * length differs from what 61xx announced (euicc.c recv_es10x_block). So a
|
||||
+ * response with data is held here and handed out again in 61xx/GET
|
||||
+ * RESPONSE portions of exactly the announced size.
|
||||
+ *
|
||||
+ * Logging goes to stderr (libipa's log.c already writes there), so stdout
|
||||
+ * carries only the protocol. A host may still merge the two, as wwand does
|
||||
+ * (2>&1, into its syslog): every log line starts with the space-padded
|
||||
+ * subsystem ("%8s %8s ", log.c:69), never with the '{' a protocol line starts
|
||||
+ * with, so the two stay apart.
|
||||
+ */
|
||||
+
|
||||
+#include <stdio.h>
|
||||
+#include <stdlib.h>
|
||||
+#include <stdbool.h>
|
||||
+#include <stdint.h>
|
||||
+#include <string.h>
|
||||
+#include <errno.h>
|
||||
+#include <assert.h>
|
||||
+#include <onomondo/ipa/utils.h>
|
||||
+#include <onomondo/ipa/scard.h>
|
||||
+#include <onomondo/ipa/scard_stdio.h>
|
||||
+#include <onomondo/ipa/log.h>
|
||||
+#include <onomondo/ipa/mem.h>
|
||||
+
|
||||
+/* ES10 answers (a profile list, certificates) reach several kB; one line
|
||||
+ * carries the hex of the largest plus the JSON around it */
|
||||
+#define RX_MAX_LEN 32768
|
||||
+#define LINE_MAX_LEN (2 * RX_MAX_LEN + 128)
|
||||
+#define AID_MAX_LEN 16
|
||||
+
|
||||
+struct scard_ctx {
|
||||
+ /* the AID libipa selected, kept so the channel can be re-opened after
|
||||
+ * the host has reset the card (see ipa_scard_stdio_await_online) */
|
||||
+ uint8_t aid[AID_MAX_LEN];
|
||||
+ size_t aid_len;
|
||||
+
|
||||
+ /* the channel the modem handed out; -1 while none is open */
|
||||
+ int channel;
|
||||
+
|
||||
+ /* a card answer being handed out in GET RESPONSE portions */
|
||||
+ uint8_t *pending;
|
||||
+ size_t pending_len;
|
||||
+ size_t pending_off;
|
||||
+};
|
||||
+
|
||||
+/* One process drives one card, and the connectivity hook below is called from
|
||||
+ * main.c, which only holds libipa's opaque context. */
|
||||
+static struct scard_ctx *g_ctx;
|
||||
+
|
||||
+/* Set once a read from the host failed (EOF, or a SIGTERM interrupting it).
|
||||
+ * Every later exchange fails at once instead of blocking again: the shutdown
|
||||
+ * path still closes the channel and disconnects, and waiting on a host that
|
||||
+ * has gone would keep main() from ever saving the nvstate. */
|
||||
+static bool host_gone;
|
||||
+
|
||||
+static const char hexdigits[] = "0123456789ABCDEF";
|
||||
+
|
||||
+static char *to_hex(const uint8_t *data, size_t len)
|
||||
+{
|
||||
+ char *s = malloc(len * 2 + 1);
|
||||
+ size_t i;
|
||||
+
|
||||
+ assert(s);
|
||||
+ for (i = 0; i < len; i++) {
|
||||
+ s[i * 2] = hexdigits[data[i] >> 4];
|
||||
+ s[i * 2 + 1] = hexdigits[data[i] & 0x0F];
|
||||
+ }
|
||||
+ s[len * 2] = '\0';
|
||||
+ return s;
|
||||
+}
|
||||
+
|
||||
+static int hexval(char c)
|
||||
+{
|
||||
+ if (c >= '0' && c <= '9')
|
||||
+ return c - '0';
|
||||
+ if (c >= 'a' && c <= 'f')
|
||||
+ return c - 'a' + 10;
|
||||
+ if (c >= 'A' && c <= 'F')
|
||||
+ return c - 'A' + 10;
|
||||
+ return -1;
|
||||
+}
|
||||
+
|
||||
+/* Read one line from stdin. Returns false on EOF: the host is gone, and every
|
||||
+ * caller turns that into a card error rather than waiting forever. */
|
||||
+static bool read_line(char *buf, size_t size)
|
||||
+{
|
||||
+ size_t n;
|
||||
+
|
||||
+ if (host_gone)
|
||||
+ return false;
|
||||
+
|
||||
+ if (!fgets(buf, size, stdin)) {
|
||||
+ host_gone = true;
|
||||
+ return false;
|
||||
+ }
|
||||
+
|
||||
+ n = strlen(buf);
|
||||
+
|
||||
+ /* A line longer than the buffer: the rest would be read as the NEXT
|
||||
+ * answer and every later exchange would be off by one. Drain it and
|
||||
+ * report the round as failed instead. */
|
||||
+ if (n == size - 1 && buf[n - 1] != '\n') {
|
||||
+ int c;
|
||||
+
|
||||
+ while ((c = getchar()) != EOF && c != '\n')
|
||||
+ ;
|
||||
+ buf[0] = '\0';
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: answer longer than %zu bytes dropped\n", size);
|
||||
+ return true;
|
||||
+ }
|
||||
+ while (n && (buf[n - 1] == '\n' || buf[n - 1] == '\r'))
|
||||
+ buf[--n] = '\0';
|
||||
+
|
||||
+ return true;
|
||||
+}
|
||||
+
|
||||
+/* The host's answers are produced by wwand (a fixed sprintf) or lpac-compatible
|
||||
+ * bridges, so a field lookup is enough; a full JSON parser would be the only
|
||||
+ * dependency this binary had besides libcurl. */
|
||||
+static const char *field(const char *line, const char *name)
|
||||
+{
|
||||
+ char key[32];
|
||||
+ const char *p;
|
||||
+
|
||||
+ snprintf(key, sizeof(key), "\"%s\"", name);
|
||||
+ p = strstr(line, key);
|
||||
+ if (!p)
|
||||
+ return NULL;
|
||||
+ p += strlen(key);
|
||||
+ while (*p == ' ' || *p == ':')
|
||||
+ p++;
|
||||
+ return p;
|
||||
+}
|
||||
+
|
||||
+/* One request/answer round. `data` receives the decoded "data" field (may be
|
||||
+ * empty); returns the ecode, or -1 on a protocol failure. */
|
||||
+static int roundtrip(const char *func, const uint8_t *param, size_t param_len, struct ipa_buf *data)
|
||||
+{
|
||||
+ static char line[LINE_MAX_LEN];
|
||||
+ char *hex = param_len ? to_hex(param, param_len) : NULL;
|
||||
+ const char *p;
|
||||
+ int ecode;
|
||||
+
|
||||
+ if (host_gone) {
|
||||
+ free(hex);
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ fflush(stdout);
|
||||
+ printf("{\"type\":\"apdu\",\"payload\":{\"func\":\"%s\",\"param\":%s%s%s}}\n",
|
||||
+ func, hex ? "\"" : "", hex ? hex : "null", hex ? "\"" : "");
|
||||
+ fflush(stdout);
|
||||
+ free(hex);
|
||||
+
|
||||
+ if (!read_line(line, sizeof(line))) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: host closed the channel during %s\n", func);
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ p = field(line, "ecode");
|
||||
+ if (!p) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: malformed answer to %s: %s\n", func, line);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ ecode = atoi(p);
|
||||
+
|
||||
+ if (data) {
|
||||
+ data->len = 0;
|
||||
+ p = field(line, "data");
|
||||
+ if (p && *p == '"') {
|
||||
+ p++;
|
||||
+ while (hexval(p[0]) >= 0 && hexval(p[1]) >= 0) {
|
||||
+ if (data->len >= data->data_len) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: answer to %s exceeds %zu bytes\n", func,
|
||||
+ data->data_len);
|
||||
+ return -1;
|
||||
+ }
|
||||
+ data->data[data->len++] = (hexval(p[0]) << 4) | hexval(p[1]);
|
||||
+ p += 2;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return ecode;
|
||||
+}
|
||||
+
|
||||
+/* CLA with the channel number in it (ISO/IEC 7816-4 5.4.1). Channels 0-3 sit
|
||||
+ * in bits 1-2 of the first interindustry coding, 4-19 need the further
|
||||
+ * coding: bit 7 set and channel-4 in bits 1-4. Bit 8 (the proprietary class
|
||||
+ * STORE DATA uses) is kept either way. libipa sends no secure messaging, so
|
||||
+ * the SM bits are left clear. lpac writes the low nibble instead
|
||||
+ * (euicc/interface.c, v2.3.0), which is the same for 0-3 and wrong from 4 on;
|
||||
+ * a modem is free to hand out a channel above 3. */
|
||||
+static uint8_t cla_for_channel(uint8_t cla, int channel)
|
||||
+{
|
||||
+ if (channel <= 3)
|
||||
+ return (cla & 0x80) | (uint8_t) channel;
|
||||
+ return (cla & 0x80) | 0x40 | (uint8_t) (channel - 4);
|
||||
+}
|
||||
+
|
||||
+static void drop_pending(struct scard_ctx *ctx)
|
||||
+{
|
||||
+ free(ctx->pending);
|
||||
+ ctx->pending = NULL;
|
||||
+ ctx->pending_len = 0;
|
||||
+ ctx->pending_off = 0;
|
||||
+}
|
||||
+
|
||||
+static void put_sw(struct ipa_buf *res, uint16_t sw)
|
||||
+{
|
||||
+ res->data[res->len++] = sw >> 8;
|
||||
+ res->data[res->len++] = sw & 0xFF;
|
||||
+}
|
||||
+
|
||||
+/* 61xx for what is still held: xx is the size of the next portion, 00 meaning
|
||||
+ * 256 (ISO/IEC 7816-4 and ETSI TS 102 221 10.1.6, which libipa follows). */
|
||||
+static uint16_t pending_sw(const struct scard_ctx *ctx)
|
||||
+{
|
||||
+ size_t left = ctx->pending_len - ctx->pending_off;
|
||||
+
|
||||
+ if (!left)
|
||||
+ return 0x9000;
|
||||
+ return 0x6100 | (left >= 256 ? 0 : (uint8_t) left);
|
||||
+}
|
||||
+
|
||||
+static void close_channel(struct scard_ctx *ctx)
|
||||
+{
|
||||
+ uint8_t ch;
|
||||
+
|
||||
+ if (ctx->channel < 0)
|
||||
+ return;
|
||||
+ ch = (uint8_t) ctx->channel;
|
||||
+ roundtrip("logic_channel_close", &ch, 1, NULL);
|
||||
+ ctx->channel = -1;
|
||||
+}
|
||||
+
|
||||
+static int open_channel(struct scard_ctx *ctx)
|
||||
+{
|
||||
+ int ch = roundtrip("logic_channel_open", ctx->aid, ctx->aid_len, NULL);
|
||||
+
|
||||
+ /* channel 0 is the basic channel, never one the modem hands out for an
|
||||
+ * AID; lpac treats it as a failure the same way */
|
||||
+ if (ch <= 0) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: could not open a logical channel to %s (ecode %d)\n",
|
||||
+ ipa_hexdump(ctx->aid, ctx->aid_len), ch);
|
||||
+ ctx->channel = -1;
|
||||
+ return -EIO;
|
||||
+ }
|
||||
+
|
||||
+ ctx->channel = ch;
|
||||
+ IPA_LOGP(SSCARD, LINFO, "stdio: logical channel %d open to %s\n", ch, ipa_hexdump(ctx->aid, ctx->aid_len));
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+void *ipa_scard_init(unsigned int reader_num)
|
||||
+{
|
||||
+ struct scard_ctx *ctx;
|
||||
+
|
||||
+ (void)reader_num; /* the host decides which modem and slot this is */
|
||||
+
|
||||
+ if (roundtrip("connect", NULL, 0, NULL) != 0) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: the host refused the connection\n");
|
||||
+ return NULL;
|
||||
+ }
|
||||
+
|
||||
+ ctx = IPA_ALLOC_ZERO(struct scard_ctx);
|
||||
+ assert(ctx);
|
||||
+ ctx->channel = -1;
|
||||
+ g_ctx = ctx;
|
||||
+ return ctx;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_transceive(void *scard_ctx, struct ipa_buf *res, const struct ipa_buf *req)
|
||||
+{
|
||||
+ struct scard_ctx *ctx = scard_ctx;
|
||||
+ struct ipa_buf *rx = NULL;
|
||||
+ uint8_t *tx = NULL;
|
||||
+ uint8_t cla, ins, p1;
|
||||
+ uint16_t sw;
|
||||
+ size_t n;
|
||||
+ int rc = 0;
|
||||
+
|
||||
+ assert(ctx);
|
||||
+ assert(res);
|
||||
+ assert(req);
|
||||
+ res->len = 0;
|
||||
+
|
||||
+ if (req->len < 4)
|
||||
+ return -EINVAL;
|
||||
+
|
||||
+ cla = req->data[0];
|
||||
+ ins = req->data[1];
|
||||
+ p1 = req->data[2];
|
||||
+
|
||||
+ IPA_LOGP(SSCARD, LDEBUG, "stdio TX: %s\n", ipa_buf_hexdump(req));
|
||||
+
|
||||
+ /* TERMINAL CAPABILITY belongs to the terminal, which is the modem: it
|
||||
+ * has sent its own to the card at power-up, and it is not something a
|
||||
+ * host can say on the modem's behalf through a logical channel. lpac
|
||||
+ * does not send it either. */
|
||||
+ if ((cla & 0xF0) == 0x80 && ins == 0xAA) {
|
||||
+ put_sw(res, 0x9000);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ /* MANAGE CHANNEL: the modem opens channels by AID, so opening waits for
|
||||
+ * the SELECT that names one; closing closes the real channel. */
|
||||
+ if (ins == 0x70) {
|
||||
+ if (p1 & 0x80)
|
||||
+ close_channel(ctx);
|
||||
+ put_sw(res, 0x9000);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ /* SELECT by DF name: this is where the channel is really opened. Any
|
||||
+ * channel still open is closed first so a re-select does not leak one
|
||||
+ * per poll. The select response is not handed on: libipa checks only
|
||||
+ * for 61xx here (euicc.c select_isd_r) and never fetches it. */
|
||||
+ if (ins == 0xA4 && p1 == 0x04 && req->len >= 5) {
|
||||
+ size_t lc = req->data[4];
|
||||
+
|
||||
+ if (lc == 0 || lc > AID_MAX_LEN || req->len < 5 + lc) {
|
||||
+ put_sw(res, 0x6A82);
|
||||
+ goto out;
|
||||
+ }
|
||||
+ close_channel(ctx);
|
||||
+ drop_pending(ctx);
|
||||
+ memcpy(ctx->aid, req->data + 5, lc);
|
||||
+ ctx->aid_len = lc;
|
||||
+ put_sw(res, open_channel(ctx) == 0 ? 0x6100 : 0x6A82);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ /* GET RESPONSE for an answer held here: hand out exactly the announced
|
||||
+ * portion (Le 00 = 256), then 61xx for the rest or 9000. */
|
||||
+ if (ins == 0xC0 && ctx->pending) {
|
||||
+ size_t le = req->len >= 5 ? req->data[4] : 0;
|
||||
+ size_t left = ctx->pending_len - ctx->pending_off;
|
||||
+
|
||||
+ if (le == 0)
|
||||
+ le = 256;
|
||||
+ n = le < left ? le : left;
|
||||
+ if (n + 2 > res->data_len)
|
||||
+ n = res->data_len - 2;
|
||||
+ memcpy(res->data, ctx->pending + ctx->pending_off, n);
|
||||
+ res->len = n;
|
||||
+ ctx->pending_off += n;
|
||||
+ sw = pending_sw(ctx);
|
||||
+ if (sw == 0x9000)
|
||||
+ drop_pending(ctx);
|
||||
+ put_sw(res, sw);
|
||||
+ goto out;
|
||||
+ }
|
||||
+
|
||||
+ if (ctx->channel < 0) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: APDU for a card channel that is not open\n");
|
||||
+ return -EIO;
|
||||
+ }
|
||||
+
|
||||
+ /* the channel number in CLA is the modem's, not the one libipa chose */
|
||||
+ tx = malloc(req->len);
|
||||
+ assert(tx);
|
||||
+ memcpy(tx, req->data, req->len);
|
||||
+ tx[0] = cla_for_channel(tx[0], ctx->channel);
|
||||
+
|
||||
+ drop_pending(ctx);
|
||||
+ rx = ipa_buf_alloc(RX_MAX_LEN);
|
||||
+ assert(rx);
|
||||
+
|
||||
+ if (roundtrip("transmit", tx, req->len, rx) != 0 || rx->len < 2) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: transmit failed\n");
|
||||
+ rc = -EIO;
|
||||
+ goto free;
|
||||
+ }
|
||||
+
|
||||
+ sw = (rx->data[rx->len - 2] << 8) | rx->data[rx->len - 1];
|
||||
+ n = rx->len - 2;
|
||||
+
|
||||
+ /* An answer with data and a normal ending is held and announced as 61xx.
|
||||
+ * 91xx is a normal ending too (a proactive command is pending, which the
|
||||
+ * modem fetches itself); libipa knows only 9000/61xx and would abort. */
|
||||
+ if (n && (sw == 0x9000 || (sw & 0xFF00) == 0x9100)) {
|
||||
+ ctx->pending = malloc(n);
|
||||
+ assert(ctx->pending);
|
||||
+ memcpy(ctx->pending, rx->data, n);
|
||||
+ ctx->pending_len = n;
|
||||
+ ctx->pending_off = 0;
|
||||
+ put_sw(res, pending_sw(ctx));
|
||||
+ goto free;
|
||||
+ }
|
||||
+
|
||||
+ if ((sw & 0xFF00) == 0x9100)
|
||||
+ sw = 0x9000;
|
||||
+
|
||||
+ /* anything else goes to libipa as the card said it, clipped to fit */
|
||||
+ if (n + 2 > res->data_len)
|
||||
+ n = res->data_len - 2;
|
||||
+ memcpy(res->data, rx->data, n);
|
||||
+ res->len = n;
|
||||
+ put_sw(res, sw);
|
||||
+
|
||||
+free:
|
||||
+ free(tx);
|
||||
+ ipa_buf_free(rx);
|
||||
+ if (rc < 0)
|
||||
+ return rc;
|
||||
+out:
|
||||
+ IPA_LOGP(SSCARD, LDEBUG, "stdio RX: %s\n", ipa_buf_hexdump(res));
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_reset(void *scard_ctx)
|
||||
+{
|
||||
+ /* a card reset is the host's to do (it owns the modem and knows what a
|
||||
+ * reset costs the data session); from here it is a re-open */
|
||||
+ struct scard_ctx *ctx = scard_ctx;
|
||||
+
|
||||
+ assert(ctx);
|
||||
+ drop_pending(ctx);
|
||||
+ close_channel(ctx);
|
||||
+ return ctx->aid_len ? open_channel(ctx) : 0;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_atr(void *scard_ctx, struct ipa_buf *atr)
|
||||
+{
|
||||
+ /* the modem does not hand the ATR to the host over this channel */
|
||||
+ (void)scard_ctx;
|
||||
+ atr->len = 0;
|
||||
+ return -EIO;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_free(void *scard_ctx)
|
||||
+{
|
||||
+ struct scard_ctx *ctx = scard_ctx;
|
||||
+
|
||||
+ if (!ctx)
|
||||
+ return 0;
|
||||
+
|
||||
+ drop_pending(ctx);
|
||||
+ close_channel(ctx);
|
||||
+ roundtrip("disconnect", NULL, 0, NULL);
|
||||
+ if (g_ctx == ctx)
|
||||
+ g_ctx = NULL;
|
||||
+ IPA_FREE(ctx);
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+int ipa_scard_stdio_await_online(void)
|
||||
+{
|
||||
+ static char line[LINE_MAX_LEN];
|
||||
+ const char *p;
|
||||
+ int online;
|
||||
+
|
||||
+ /* The profile just changed. The host has to make the modem use it (a SIM
|
||||
+ * power cycle: without REFRESH the modem keeps running the old profile)
|
||||
+ * and wait until the data session is back; only then can the result reach
|
||||
+ * the eIM. That power cycle closes every logical channel, so ours is
|
||||
+ * re-opened here before libipa talks to the card again. */
|
||||
+ fflush(stdout);
|
||||
+ printf("{\"type\":\"event\",\"payload\":{\"event\":\"profile_changed\"}}\n");
|
||||
+ fflush(stdout);
|
||||
+
|
||||
+ if (!read_line(line, sizeof(line))) {
|
||||
+ IPA_LOGP(SSCARD, LERROR, "stdio: host closed the channel while restoring connectivity\n");
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ p = field(line, "online");
|
||||
+ online = p && strncmp(p, "true", 4) == 0;
|
||||
+ IPA_LOGP(SSCARD, LINFO, "stdio: host reports the connection %s\n", online ? "back" : "NOT back");
|
||||
+
|
||||
+ if (g_ctx) {
|
||||
+ /* no close first: the card reset already closed it, and the modem
|
||||
+ * may have handed that id to someone else since */
|
||||
+ drop_pending(g_ctx);
|
||||
+ if (g_ctx->aid_len && open_channel(g_ctx) < 0)
|
||||
+ return -1;
|
||||
+ }
|
||||
+
|
||||
+ return online;
|
||||
+}
|
||||
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
|
||||
index a3e0ef8..de33415 100644
|
||||
--- a/tests/CMakeLists.txt
|
||||
+++ b/tests/CMakeLists.txt
|
||||
@@ -6,3 +6,6 @@ enable_testing()
|
||||
add_subdirectory(activation_code)
|
||||
add_subdirectory(utils)
|
||||
add_subdirectory(bpp_segments)
|
||||
+if (IPA_SCARD_STDIO)
|
||||
+ add_subdirectory(scard_stdio)
|
||||
+endif()
|
||||
diff --git a/tests/scard_stdio/CMakeLists.txt b/tests/scard_stdio/CMakeLists.txt
|
||||
new file mode 100644
|
||||
index 0000000..639a9c7
|
||||
--- /dev/null
|
||||
+++ b/tests/scard_stdio/CMakeLists.txt
|
||||
@@ -0,0 +1,11 @@
|
||||
+# Copyright (c) 2026 André Valentin <avalentin@marcant.net>
|
||||
+# SPDX-License-Identifier: AGPL-3.0-only
|
||||
+
|
||||
+add_executable(scard_stdio_test scard_stdio_test.c)
|
||||
+set_property(TARGET scard_stdio_test PROPERTY C_STANDARD 99)
|
||||
+target_compile_options(scard_stdio_test PRIVATE -Wall)
|
||||
+target_include_directories(scard_stdio_test PRIVATE ${PROJECT_SOURCE_DIR}/include)
|
||||
+target_link_libraries(scard_stdio_test scard libipa)
|
||||
+
|
||||
+add_test(NAME scard_stdio_test
|
||||
+ COMMAND sh -c "$<TARGET_FILE:scard_stdio_test> < ${CMAKE_CURRENT_SOURCE_DIR}/scard_stdio_test.in > scard_stdio_test.out && cmp scard_stdio_test.out ${CMAKE_CURRENT_SOURCE_DIR}/scard_stdio_test.ok")
|
||||
diff --git a/tests/scard_stdio/scard_stdio_test.c b/tests/scard_stdio/scard_stdio_test.c
|
||||
new file mode 100644
|
||||
index 0000000..831320d
|
||||
--- /dev/null
|
||||
+++ b/tests/scard_stdio/scard_stdio_test.c
|
||||
@@ -0,0 +1,95 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 André Valentin <avalentin@marcant.net>
|
||||
+ *
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
+ *
|
||||
+ * The stdio card backend against a scripted host: stdin carries the host's
|
||||
+ * answers (scard_stdio_test.in), stdout the requests, which the test compares
|
||||
+ * with scard_stdio_test.ok. The card answers the way a modem's APDU service
|
||||
+ * does -- the whole answer at once with 9000 -- and the checks here are what
|
||||
+ * libipa relies on seeing instead (libipa/euicc.c).
|
||||
+ */
|
||||
+
|
||||
+#include <stdio.h>
|
||||
+#include <string.h>
|
||||
+#include <assert.h>
|
||||
+#include <onomondo/ipa/utils.h>
|
||||
+#include <onomondo/ipa/scard.h>
|
||||
+#include <onomondo/ipa/scard_stdio.h>
|
||||
+
|
||||
+static void *card;
|
||||
+
|
||||
+/* send one C-APDU (hex) and return the R-APDU as a buffer */
|
||||
+static struct ipa_buf *xfer(const char *capdu_hex)
|
||||
+{
|
||||
+ struct ipa_buf *req = ipa_buf_alloc(300);
|
||||
+ struct ipa_buf *res = ipa_buf_alloc(258);
|
||||
+
|
||||
+ req->len = ipa_binary_from_hexstr(req->data, req->data_len, capdu_hex);
|
||||
+ assert(ipa_scard_transceive(card, res, req) == 0);
|
||||
+ ipa_buf_free(req);
|
||||
+ return res;
|
||||
+}
|
||||
+
|
||||
+static unsigned sw_of(const struct ipa_buf *r)
|
||||
+{
|
||||
+ assert(r->len >= 2);
|
||||
+ return (r->data[r->len - 2] << 8) | r->data[r->len - 1];
|
||||
+}
|
||||
+
|
||||
+static void expect_sw(const char *capdu_hex, unsigned sw, size_t data_len)
|
||||
+{
|
||||
+ struct ipa_buf *r = xfer(capdu_hex);
|
||||
+
|
||||
+ if (sw_of(r) != sw || r->len - 2 != data_len) {
|
||||
+ fprintf(stderr, "%s: got sw %04X with %zu bytes, want %04X with %zu\n", capdu_hex, sw_of(r),
|
||||
+ r->len - 2, sw, data_len);
|
||||
+ assert(0);
|
||||
+ }
|
||||
+ ipa_buf_free(r);
|
||||
+}
|
||||
+
|
||||
+int main(void)
|
||||
+{
|
||||
+ card = ipa_scard_init(0);
|
||||
+ assert(card);
|
||||
+
|
||||
+ /* TERMINAL CAPABILITY and MANAGE CHANNEL never reach the host */
|
||||
+ expect_sw("80AA000005A903830107", 0x9000, 0);
|
||||
+ expect_sw("0070000100", 0x9000, 0);
|
||||
+
|
||||
+ /* SELECT ISD-R opens the real channel (the host hands out 3); libipa
|
||||
+ * accepts only 61xx here */
|
||||
+ expect_sw("01A4040410A0000005591010FFFFFFFF8900000100", 0x6100, 0);
|
||||
+
|
||||
+ /* A 300-byte answer with 9000 is announced as 61 00 and handed out as
|
||||
+ * 256 + 44, each portion exactly the size announced. CLA goes out with
|
||||
+ * the host's channel (83), not libipa's (81). */
|
||||
+ expect_sw("81E2910003BF2200", 0x6100, 0);
|
||||
+ expect_sw("01C0000000", 0x612C, 256);
|
||||
+ expect_sw("01C000002C", 0x9000, 44);
|
||||
+
|
||||
+ /* The host resets the card for a profile change: the channel it closed
|
||||
+ * is re-opened (the host now hands out 2) and later APDUs use it */
|
||||
+ assert(ipa_scard_stdio_await_online() == 1);
|
||||
+ expect_sw("81E2910003BF2B00", 0x6A88, 0);
|
||||
+
|
||||
+ /* 91xx is a normal ending too: held and announced like 9000 */
|
||||
+ expect_sw("81E2910003BF2D00", 0x6102, 0);
|
||||
+ expect_sw("01C0000002", 0x9000, 2);
|
||||
+
|
||||
+ /* the host reports it could not get the connection back, and re-opens
|
||||
+ * on channel 4: from there CLA takes the further coding, 0x40 | ch-4
|
||||
+ * (ISO/IEC 7816-4 5.4.1), so STORE DATA goes out as C0, not 84 */
|
||||
+ assert(ipa_scard_stdio_await_online() == 0);
|
||||
+ expect_sw("81E2910003BF2B00", 0x9000, 0);
|
||||
+
|
||||
+ assert(ipa_scard_free(card) == 0);
|
||||
+
|
||||
+ /* the host is gone: every exchange fails at once instead of blocking */
|
||||
+ card = ipa_scard_init(0);
|
||||
+ assert(card == NULL);
|
||||
+
|
||||
+ fprintf(stderr, "scard_stdio_test: ok\n");
|
||||
+ return 0;
|
||||
+}
|
||||
diff --git a/tests/scard_stdio/scard_stdio_test.in b/tests/scard_stdio/scard_stdio_test.in
|
||||
new file mode 100644
|
||||
index 0000000..50d2f24
|
||||
--- /dev/null
|
||||
+++ b/tests/scard_stdio/scard_stdio_test.in
|
||||
@@ -0,0 +1,12 @@
|
||||
+{"type":"apdu","payload":{"ecode":0}}
|
||||
+{"type":"apdu","payload":{"ecode":3}}
|
||||
+{"type":"apdu","payload":{"ecode":0,"data":"A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A5A59000"}}
|
||||
+{"type":"event","payload":{"online":true}}
|
||||
+{"type":"apdu","payload":{"ecode":2}}
|
||||
+{"type":"apdu","payload":{"ecode":0,"data":"6A88"}}
|
||||
+{"type":"apdu","payload":{"ecode":0,"data":"AABB9110"}}
|
||||
+{"type":"event","payload":{"online":false}}
|
||||
+{"type":"apdu","payload":{"ecode":4}}
|
||||
+{"type":"apdu","payload":{"ecode":0,"data":"9000"}}
|
||||
+{"type":"apdu","payload":{"ecode":0}}
|
||||
+{"type":"apdu","payload":{"ecode":0}}
|
||||
diff --git a/tests/scard_stdio/scard_stdio_test.ok b/tests/scard_stdio/scard_stdio_test.ok
|
||||
new file mode 100644
|
||||
index 0000000..2e653d9
|
||||
--- /dev/null
|
||||
+++ b/tests/scard_stdio/scard_stdio_test.ok
|
||||
@@ -0,0 +1,13 @@
|
||||
+{"type":"apdu","payload":{"func":"connect","param":null}}
|
||||
+{"type":"apdu","payload":{"func":"logic_channel_open","param":"A0000005591010FFFFFFFF8900000100"}}
|
||||
+{"type":"apdu","payload":{"func":"transmit","param":"83E2910003BF2200"}}
|
||||
+{"type":"event","payload":{"event":"profile_changed"}}
|
||||
+{"type":"apdu","payload":{"func":"logic_channel_open","param":"A0000005591010FFFFFFFF8900000100"}}
|
||||
+{"type":"apdu","payload":{"func":"transmit","param":"82E2910003BF2B00"}}
|
||||
+{"type":"apdu","payload":{"func":"transmit","param":"82E2910003BF2D00"}}
|
||||
+{"type":"event","payload":{"event":"profile_changed"}}
|
||||
+{"type":"apdu","payload":{"func":"logic_channel_open","param":"A0000005591010FFFFFFFF8900000100"}}
|
||||
+{"type":"apdu","payload":{"func":"transmit","param":"C0E2910003BF2B00"}}
|
||||
+{"type":"apdu","payload":{"func":"logic_channel_close","param":"04"}}
|
||||
+{"type":"apdu","payload":{"func":"disconnect","param":null}}
|
||||
+{"type":"apdu","payload":{"func":"connect","param":null}}
|
||||
@@ -0,0 +1,58 @@
|
||||
#
|
||||
# On-demand Qualcomm diag (QMDL) capture through Quectel QLog, for wwand:
|
||||
# `wwandctl qlog`. A wwand PLUGIN, kept out of the wwand sources on purpose:
|
||||
# it is not meant for upstream. wwand itself only resolves and publishes the
|
||||
# modem's diag node (status.diag_port) and never opens it.
|
||||
#
|
||||
# Sources live in https://github.com/ddimension/wwand-qlog, tagged vX.Y.Z. Pin
|
||||
# a new commit with `scripts/bump-source.sh wwand-qlog <tag|commit>`.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand-qlog
|
||||
PKG_VERSION:=0.1.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand-qlog.git
|
||||
PKG_SOURCE_VERSION:=e4ef3350ac11439259f8bdfd7f11baf002ec9659
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
UCDIR:=/usr/share/ucode/wwand
|
||||
|
||||
define Package/wwand-qlog
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=WWAN
|
||||
TITLE:=Quectel QLog diag capture for wwand (wwandctl qlog)
|
||||
# qlog: /usr/sbin/QLog and its filter profiles. QLog's device scan covers
|
||||
# Quectel modules only (its VID/PID allow-list, see the README).
|
||||
DEPENDS:=+wwand +qlog
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/wwand-qlog/description
|
||||
`wwandctl qlog`: start, inspect and stop a Qualcomm diag capture with Quectel
|
||||
QLog on the modem's DM/DIAG port, which wwand resolves. On demand only; the
|
||||
storage sink (TCP, TFTP, FTP, a directory) is QLog's own and passed through
|
||||
verbatim, and a capture without one is refused so a QMDL cannot fill the
|
||||
router's flash.
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/wwand-qlog/install
|
||||
$(INSTALL_DIR) $(1)$(UCDIR)/ctl
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/qlog.uc $(1)$(UCDIR)/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/ctl/qlog.uc $(1)$(UCDIR)/ctl/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,wwand-qlog))
|
||||
+4
-4
@@ -50,12 +50,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.6.8_p6
|
||||
PKG_RELEASE:=56
|
||||
PKG_VERSION:=1.6.8_p23
|
||||
PKG_RELEASE:=57
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=2d7803e46b95f60a8a890ccd961f5154ba672424
|
||||
PKG_SOURCE_VERSION:=cf8d5cf1cc5767d58824a02c61de4d9d2ff58cf6
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
@@ -98,7 +98,7 @@ UCDIR:=/usr/share/ucode/wwand
|
||||
WWAND_BASE_UC:=apndb.uc atcmd.uc atcmd_parse.uc atport.uc backend.uc board.uc \
|
||||
carrier_config.uc client.uc config.uc config_check.uc context_common.uc \
|
||||
context_monitor_qmi.uc ctx_settings.uc daemon.uc deps.uc discovery.uc \
|
||||
hwops.uc log.uc modem_common.uc modem_datapath_qmi.uc modem_init_qmi.uc \
|
||||
hwops.uc log.uc modem_common.uc modem_datapath_qmi.uc modem_init_qmi.uc plugins.uc \
|
||||
modem_quirks.uc modeswitch.uc ncm_vendors.uc netlink.uc netsel_ops.uc \
|
||||
protocol_switch.uc reconnect.uc recovery.uc regdetail.uc sim.uc sim_plmn.uc \
|
||||
simops.uc sms.uc sms_pdu.uc telemetry_mbim.uc telemetry_ncm.uc \
|
||||
|
||||
Reference in new issue
Block a user