🐶 Sync 2026-09-26 16:35:25

This commit is contained in:
github-actions[bot] committed 2026-09-26 16:35:25 +08:00
1 parent 1eb453ca2e
commit 3f39d2b62b
44 files changed
+2741 -2902

No files matched your search

+2 -2
View File
@@ -11,8 +11,8 @@ LUCI_DESCRIPTION:=Live firewall log table (firewall4/nft; iptables-tagged lines
LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_VERSION:=0.1.45 PKG_VERSION:=0.1.46
PKG_RELEASE:=38 PKG_RELEASE:=39
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH ifdef SOURCE_DATE_EPOCH
@@ -9,7 +9,7 @@
*/ */
return baseclass.extend({ return baseclass.extend({
/* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */ /* 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], ROW_LIMIT_OPTIONS: [25, 50, 100, 250, 500, 1000, 2000],
DEFAULT_ROW_LIMIT: 100, DEFAULT_ROW_LIMIT: 100,
FETCH_MODE_OPTIONS: ['auto', 'manual'], FETCH_MODE_OPTIONS: ['auto', 'manual'],
@@ -262,6 +262,8 @@ function buildEmptyStateNodes(state, callbacks) {
); );
} }
if (!st) return nodes;
if (blocker === 'no_wan_zone') { if (blocker === 'no_wan_zone') {
nodes.push( nodes.push(
E( E(
@@ -746,11 +746,11 @@ return view.extend({
this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true); this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true);
} catch (_e) { } catch (_e) {
if (this.viewDisposed) return; 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) if (!this.loggingNotice)
this.loggingNotice = _( this.loggingNotice = this.loggingStatus
'Could not refresh logging status; showing the last known state.' ? _('Could not refresh logging status; showing the last known state.')
); : _('Could not load logging status.');
} }
this.updateBackendUi(); this.updateBackendUi();
this.updateLoggingToolbarUi(); this.updateLoggingToolbarUi();
+22 -18
View File
@@ -160,7 +160,11 @@ msgstr "Protokollierung konnte nicht deaktiviert werden."
msgid "Could not enable logging." msgid "Could not enable logging."
msgstr "Protokollierung konnte nicht aktiviert werden." 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." msgid "Could not refresh logging status; showing the last known state."
msgstr "Protokollstatus konnte nicht aktualisiert werden; zeige den zuletzt bekannten Zustand." 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 " "möglicherweise keine Erfolgs/Fehler-CSS-Variablen; fwlive fällt auf lokale "
"Farben zurück (air-gapped, keine Daten verlassen das Gerät)." "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 "" msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in " "If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide." "Help / the enabling-logs guide."
@@ -393,15 +397,15 @@ msgid "Interface (prefix ! to exclude)"
msgstr "Schnittstelle (! voranstellen zum Ausschließen)" 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: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" msgid "I’ll configure this under Network → Firewall"
msgstr "Ich konfiguriere das unter Netzwerk → 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" msgid "Kernel log modules missing"
msgstr "Kernel-Logmodule fehlen" 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 "" msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" "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." "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" 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" 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" msgid "Logging is off on this router"
msgstr "Protokollierung ist auf diesem Router deaktiviert" msgstr "Protokollierung ist auf diesem Router deaktiviert"
@@ -447,7 +451,7 @@ msgstr ""
"Manuell nutzt weiterhin Serverschutz und Abfrageintervall; es ändert das " "Manuell nutzt weiterhin Serverschutz und Abfrageintervall; es ändert das "
"Abrufbudget, nicht das Abfrageintervall." "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):" msgid "Manual test (System → Terminal):"
msgstr "Manueller Test (System → Terminal):" msgstr "Manueller Test (System → Terminal):"
@@ -470,14 +474,14 @@ msgstr "Weitere Filter"
msgid "Network → Firewall" msgid "Network → Firewall"
msgstr "Netzwerk → 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 "" msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:" "No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
msgstr "" msgstr ""
"Keine WAN-Firewallzone in /etc/config/firewall gefunden. Zonen konfigurieren " "Keine WAN-Firewallzone in /etc/config/firewall gefunden. Zonen konfigurieren "
"unter:" "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" msgid "No WAN zone found"
msgstr "Keine WAN-Zone gefunden" msgstr "Keine WAN-Zone gefunden"
@@ -492,7 +496,7 @@ msgstr ""
msgid "Not now" msgid "Not now"
msgstr "Nicht jetzt" 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." msgid "Nothing changes until you click Enable."
msgstr "Nichts ändert sich, bis Sie auf Aktivieren klicken." msgstr "Nichts ändert sich, bis Sie auf Aktivieren klicken."
@@ -504,11 +508,11 @@ msgstr "AUSGANG"
msgid "One line" msgid "One line"
msgstr "Einzeilig" 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" msgid "Open firewall zone settings"
msgstr "Firewall-Zonen-Einstellungen öffnen" 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 "" msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. " "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/" "Live View only shows what the firewall already logs — it does not add allow/"
@@ -719,11 +723,11 @@ msgstr ""
msgid "Theme tint fallback" msgid "Theme tint fallback"
msgstr "Theme-Farb-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." msgid "Then ping the router."
msgstr "Dann den Router anpingen." 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 "" msgid ""
"This router reported a logging blocker that Live View does not recognize yet." "This router reported a logging blocker that Live View does not recognize yet."
msgstr "" msgstr ""
@@ -752,7 +756,7 @@ msgstr ""
"Über das WAN-Protokollierungs-Steuerelement in der Beobachtungsleiste wieder " "Über das WAN-Protokollierungs-Steuerelement in der Beobachtungsleiste wieder "
"deaktivieren." "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 "" msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "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 " "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." msgid "WAN drop/reject logging is off."
msgstr "WAN-Drop/Reject-Protokollierung ist aus." 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 "" msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not." "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." 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: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" msgid "WAN logging unavailable"
msgstr "WAN-Protokollierung nicht verfügbar" 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" msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN-Protokollierung nicht verfügbar: keine 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" msgid "Waiting for firewall events"
msgstr "Warte auf Firewall-Ereignisse" msgstr "Warte auf Firewall-Ereignisse"
+22 -18
View File
@@ -157,7 +157,11 @@ msgstr "Не удалось отключить журналирование."
msgid "Could not enable logging." msgid "Could not enable logging."
msgstr "Не удалось включить журналирование." 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." msgid "Could not refresh logging status; showing the last known state."
msgstr "Не удалось обновить состояние журналирования; показан последний известный статус." msgstr "Не удалось обновить состояние журналирования; показан последний известный статус."
@@ -360,7 +364,7 @@ msgstr ""
"содержать CSS-переменные success/error или info/warn; fwlive использует " "содержать 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 "" msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in " "If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide." "Help / the enabling-logs guide."
@@ -386,15 +390,15 @@ msgid "Interface (prefix ! to exclude)"
msgstr "Интерфейс (! в начале для исключения)" 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: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" msgid "I’ll configure this under Network → Firewall"
msgstr "Я настрою это в разделе Сеть → Межсетевой экран" 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" msgid "Kernel log modules missing"
msgstr "Отсутствуют модули журналирования ядра" 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 "" msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" "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." "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" msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "Живой просмотр может быть неполным — зарегистрирована устаревшая таблица iptables" 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" msgid "Logging is off on this router"
msgstr "Журналирование на этом роутере отключено" 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):" msgid "Manual test (System → Terminal):"
msgstr "Ручной тест (Система → Терминал):" msgstr "Ручной тест (Система → Терминал):"
@@ -463,14 +467,14 @@ msgstr "Больше фильтров"
msgid "Network → Firewall" msgid "Network → Firewall"
msgstr "Сеть → Межсетевой экран" 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 "" msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:" "No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
msgstr "" msgstr ""
"WAN-зона межсетевого экрана не найдена в /etc/config/firewall. Настройте " "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" msgid "No WAN zone found"
msgstr "WAN-зона не найдена" msgstr "WAN-зона не найдена"
@@ -483,7 +487,7 @@ msgstr "WAN-зона не найдена; без неё нельзя перек
msgid "Not now" msgid "Not now"
msgstr "Не сейчас" 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." msgid "Nothing changes until you click Enable."
msgstr "Ничего не изменится, пока вы не нажмёте «Включить»." msgstr "Ничего не изменится, пока вы не нажмёте «Включить»."
@@ -495,11 +499,11 @@ msgstr "ВЫХОД"
msgid "One line" msgid "One line"
msgstr "Одна строка" 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" msgid "Open firewall zone settings"
msgstr "Открыть настройки зон файрвола" 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 "" msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. " "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/" "Live View only shows what the firewall already logs — it does not add allow/"
@@ -705,11 +709,11 @@ msgstr ""
msgid "Theme tint fallback" msgid "Theme tint fallback"
msgstr "Запасной цвет темы" 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." msgid "Then ping the router."
msgstr "Затем отправьте ping на роутер." 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 "" msgid ""
"This router reported a logging blocker that Live View does not recognize yet." "This router reported a logging blocker that Live View does not recognize yet."
msgstr "" msgstr ""
@@ -737,7 +741,7 @@ msgid "Turn it back off with the WAN logging control on the watch strip."
msgstr "" msgstr ""
"Отключите её снова элементом управления журналом WAN на панели наблюдения." "Отключите её снова элементом управления журналом 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 "" msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "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 " "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." msgid "WAN drop/reject logging is off."
msgstr "Журналирование drop/reject для WAN отключено." 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 "" msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not." "here. Normal LAN browsing will not."
@@ -801,7 +805,7 @@ msgid "WAN logging on (%s). Click to disable."
msgstr "WAN-журнал: ВКЛ (%s). Нажмите, чтобы отключить." 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: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" msgid "WAN logging unavailable"
msgstr "WAN-журналирование недоступно" msgstr "WAN-журналирование недоступно"
@@ -813,7 +817,7 @@ msgstr "WAN-журналирование недоступно: отсутств
msgid "WAN logging unavailable: no WAN zone" msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN-журналирование недоступно: нет зоны WAN" 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" msgid "Waiting for firewall events"
msgstr "Ожидание событий межсетевого экрана" msgstr "Ожидание событий межсетевого экрана"
@@ -133,7 +133,11 @@ msgstr ""
msgid "Could not enable logging." msgid "Could not enable logging."
msgstr "" 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." msgid "Could not refresh logging status; showing the last known state."
msgstr "" msgstr ""
@@ -317,7 +321,7 @@ msgid ""
"data leaves the device)." "data leaves the device)."
msgstr "" 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 "" msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in " "If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide." "Help / the enabling-logs guide."
@@ -341,15 +345,15 @@ msgid "Interface (prefix ! to exclude)"
msgstr "" 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: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" msgid "I’ll configure this under Network → Firewall"
msgstr "" 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" msgid "Kernel log modules missing"
msgstr "" 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 "" msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" "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." "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" msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "" 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" msgid "Logging is off on this router"
msgstr "" msgstr ""
@@ -390,7 +394,7 @@ msgid ""
"budget, not the polling interval." "budget, not the polling interval."
msgstr "" 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):" msgid "Manual test (System → Terminal):"
msgstr "" msgstr ""
@@ -413,12 +417,12 @@ msgstr ""
msgid "Network → Firewall" msgid "Network → Firewall"
msgstr "" 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 "" msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:" "No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
msgstr "" 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" msgid "No WAN zone found"
msgstr "" msgstr ""
@@ -431,7 +435,7 @@ msgstr ""
msgid "Not now" msgid "Not now"
msgstr "" 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." msgid "Nothing changes until you click Enable."
msgstr "" msgstr ""
@@ -443,11 +447,11 @@ msgstr ""
msgid "One line" msgid "One line"
msgstr "" 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" msgid "Open firewall zone settings"
msgstr "" 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 "" msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. " "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/" "Live View only shows what the firewall already logs — it does not add allow/"
@@ -633,11 +637,11 @@ msgstr ""
msgid "Theme tint fallback" msgid "Theme tint fallback"
msgstr "" 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." msgid "Then ping the router."
msgstr "" 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 "" msgid ""
"This router reported a logging blocker that Live View does not recognize yet." "This router reported a logging blocker that Live View does not recognize yet."
msgstr "" msgstr ""
@@ -662,7 +666,7 @@ msgstr ""
msgid "Turn it back off with the WAN logging control on the watch strip." msgid "Turn it back off with the WAN logging control on the watch strip."
msgstr "" 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 "" msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "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 " "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." msgid "WAN drop/reject logging is off."
msgstr "" 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 "" msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not." "here. Normal LAN browsing will not."
@@ -715,7 +719,7 @@ msgid "WAN logging on (%s). Click to disable."
msgstr "" 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: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" msgid "WAN logging unavailable"
msgstr "" msgstr ""
@@ -727,7 +731,7 @@ msgstr ""
msgid "WAN logging unavailable: no WAN zone" msgid "WAN logging unavailable: no WAN zone"
msgstr "" 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" msgid "Waiting for firewall events"
msgstr "" msgstr ""
+22 -18
View File
@@ -148,7 +148,11 @@ msgstr "无法禁用日志记录。"
msgid "Could not enable logging." msgid "Could not enable logging."
msgstr "无法启用日志记录。" 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." msgid "Could not refresh logging status; showing the last known state."
msgstr "无法刷新日志状态;显示上次已知状态。" msgstr "无法刷新日志状态;显示上次已知状态。"
@@ -341,7 +345,7 @@ msgstr ""
"如果行着色看起来缺失,当前 LuCI 主题可能未提供成功/错误或信息/警告 CSS 变量;" "如果行着色看起来缺失,当前 LuCI 主题可能未提供成功/错误或信息/警告 CSS 变量;"
"fwlive 会回退到本地颜色(隔离运行,数据不会离开本设备)。" "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 "" msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in " "If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide." "Help / the enabling-logs guide."
@@ -367,15 +371,15 @@ msgid "Interface (prefix ! to exclude)"
msgstr "接口(前缀 ! 表示排除)" 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: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" msgid "I’ll configure this under Network → Firewall"
msgstr "稍后我会在 网络 → 防火墙 中配置" 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" msgid "Kernel log modules missing"
msgstr "缺少内核日志模块" 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 "" msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-" "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." "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" msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "实时视图可能不完整 — 已注册旧版 iptables 表" 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" msgid "Logging is off on this router"
msgstr "此路由器的日志记录已关闭" msgstr "此路由器的日志记录已关闭"
@@ -419,7 +423,7 @@ msgid ""
msgstr "" 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):" msgid "Manual test (System → Terminal):"
msgstr "手动测试(系统 → 终端):" msgstr "手动测试(系统 → 终端):"
@@ -442,13 +446,13 @@ msgstr "更多筛选"
msgid "Network → Firewall" msgid "Network → Firewall"
msgstr "网络 → 防火墙" 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 "" msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:" "No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
msgstr "" msgstr ""
"在 /etc/config/firewall 中未找到 WAN 防火墙区域。请在以下位置配置区域:" "在 /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" msgid "No WAN zone found"
msgstr "未找到 WAN 区域" msgstr "未找到 WAN 区域"
@@ -461,7 +465,7 @@ msgstr "未找到 WAN 区域;没有它无法切换日志记录。"
msgid "Not now" msgid "Not now"
msgstr "暂不" 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." msgid "Nothing changes until you click Enable."
msgstr "点击「启用」之前不会有任何更改。" msgstr "点击「启用」之前不会有任何更改。"
@@ -473,11 +477,11 @@ msgstr "出接口"
msgid "One line" msgid "One line"
msgstr "单行" 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" msgid "Open firewall zone settings"
msgstr "打开防火墙区域设置" 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 "" msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. " "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/" "Live View only shows what the firewall already logs — it does not add allow/"
@@ -673,11 +677,11 @@ msgstr "防火墙记录流量时表格会自动更新。如果滚动太快,请
msgid "Theme tint fallback" msgid "Theme tint fallback"
msgstr "主题着色回退" 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." msgid "Then ping the router."
msgstr "然后 ping 路由器。" 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 "" msgid ""
"This router reported a logging blocker that Live View does not recognize yet." "This router reported a logging blocker that Live View does not recognize yet."
msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。" msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。"
@@ -702,7 +706,7 @@ msgstr "主要通信方"
msgid "Turn it back off with the WAN logging control on the watch strip." msgid "Turn it back off with the WAN logging control on the watch strip."
msgstr "通过监视栏上的 WAN 日志控件将其重新关闭。" 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 "" msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → " "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 " "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." msgid "WAN drop/reject logging is off."
msgstr "WAN 丢弃/拒绝日志记录已关闭。" 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 "" msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up " "WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not." "here. Normal LAN browsing will not."
@@ -761,7 +765,7 @@ msgid "WAN logging on (%s). Click to disable."
msgstr "WAN 日志:开(%s)。点击可禁用。" 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: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" msgid "WAN logging unavailable"
msgstr "WAN 日志不可用" msgstr "WAN 日志不可用"
@@ -773,7 +777,7 @@ msgstr "WAN 日志不可用:缺少内核日志模块"
msgid "WAN logging unavailable: no WAN zone" msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN 日志不可用:无 WAN 区域" 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" msgid "Waiting for firewall events"
msgstr "正在等待防火墙事件" msgstr "正在等待防火墙事件"
@@ -200,7 +200,11 @@ find_wan_zone_section_state() {
_firewall_show=$(uci -q show firewall 2>/dev/null || true) _firewall_show=$(uci -q show firewall 2>/dev/null || true)
_zones=$(printf '%s\n' "$_firewall_show" \ _zones=$(printf '%s\n' "$_firewall_show" \
| sed -n 's/^firewall\.\([^.=]*\)=zone$/\1/p') | 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 [ -n "$zone" ] || continue
_name=$(uci -q get "firewall.${zone}.name" 2>/dev/null || true) _name=$(uci -q get "firewall.${zone}.name" 2>/dev/null || true)
if [ -z "$_name" ]; then if [ -z "$_name" ]; then
@@ -239,7 +243,9 @@ find_wan_zone_section_state() {
WAN_ZONE_FOUND="$zone" WAN_ZONE_FOUND="$zone"
return 0 return 0
fi fi
done done <<EOF
$_zones
EOF
return 0 return 0
} }
+174 -11
View File
@@ -105,9 +105,16 @@ uci_rule_names() {
} }
is_uci_style_name() { 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 case "$1" in
*' '*|*'"'*|*"'"*) return 1 ;; ''|*' '*|*'"'*|*"'"*) return 1 ;;
[!A-Za-z0-9_-]*) return 1 ;; *[!A-Za-z0-9_-]*) return 1 ;;
esac esac
return 0 return 0
} }
@@ -177,6 +184,8 @@ EOF
# One awk pass: prefix<TAB>comment. Last quoted log prefix / comment on # One awk pass: prefix<TAB>comment. Last quoted log prefix / comment on
# the line, including \" units (same capture as the former sed). # 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() { nft_dump_fields() {
LC_ALL=C awk ' LC_ALL=C awk '
function last_at(s, needle, p, i, found) { function last_at(s, needle, p, i, found) {
@@ -219,17 +228,60 @@ nft_dump_fields() {
comment = quoted_at($0, "comment \"") comment = quoted_at($0, "comment \"")
if (prefix == "" && comment == "") if (prefix == "" && comment == "")
next next
gsub(/\\/, "\\\\", prefix)
gsub(/\t/, "\\t", prefix)
gsub(/\\/, "\\\\", comment)
gsub(/\t/, "\\t", comment)
print prefix "\t" comment print prefix "\t" comment
} }
' || return 1 ' || 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 # pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
# an earlier cosmetic for the same prefix under first-wins. # an earlier cosmetic for the same prefix under first-wins.
# Reads TSV from nft_dump_fields; stops once the map cap trips. # Reads TSV from nft_dump_fields; stops once the map cap trips.
map_from_nft_stream() { map_from_nft_stream() {
pass="${1:-all}" pass="${1:-all}"
_tab=$(printf '\t') _tab=$(printf '\t')
_esc_t=$(printf '\\t')
_esc_b=$(printf '\\\\')
# IFS= keeps a leading tab (empty prefix + comment-only !fw4: line). # IFS= keeps a leading tab (empty prefix + comment-only !fw4: line).
while IFS= read -r line || [ -n "$line" ]; do while IFS= read -r line || [ -n "$line" ]; do
[ -n "$_map_overflow" ] && break [ -n "$_map_overflow" ] && break
@@ -239,6 +291,10 @@ map_from_nft_stream() {
*"$_tab"*) comment="${line#*"$_tab"}" ;; *"$_tab"*) comment="${line#*"$_tab"}" ;;
*) comment="" ;; *) comment="" ;;
esac esac
_nft_tsv_unescape "$prefix"
prefix="$_tsv_out"
_nft_tsv_unescape "$comment"
comment="$_tsv_out"
label='' label=''
case "$comment" in case "$comment" in
@@ -666,14 +722,14 @@ resolve_addresses() {
return 0 return 0
fi 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. # Optional JSON on $1 (rpcd may also pass argv $3 at the call site); else stdin.
input=$(read_rpc_input "$1") 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 lookups=0
truncated=0 truncated=0
OUT='' OUT=''
@@ -719,7 +775,7 @@ resolve_addresses() {
truncated=1 truncated=1
break break
fi 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. # never START a lookup past the budget.
now=$(date +%s) now=$(date +%s)
if [ $((now - start)) -ge "$RESOLVE_BUDGET" ]; then if [ $((now - start)) -ge "$RESOLVE_BUDGET" ]; then
@@ -793,6 +849,42 @@ run_selftest() {
expected=$(printf 'a\\n\\nb') expected=$(printf 'a\\n\\nb')
check_eq 'json_escape blank line' "$escaped" "$expected" "expected $expected got $escaped" || return 1 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 if is_resolvable_address 'not an ip'; then
echo 'is_resolvable_address: expected reject for spaces' >&2 echo 'is_resolvable_address: expected reject for spaces' >&2
return 1 return 1
@@ -864,10 +956,17 @@ run_selftest() {
# Escaped-quote capture: a log prefix containing \" must not be truncated # Escaped-quote capture: a log prefix containing \" must not be truncated
# at the escaped quote (nft dumps strings with backslash escapes). # at the escaped quote (nft dumps strings with backslash escapes).
_tab=$(printf '\t') _tab=$(printf '\t')
_esc_t=$(printf '\\t')
_esc_b=$(printf '\\\\')
got=$(printf '%s\n' 'log prefix "My \" Rule"' | nft_dump_fields) got=$(printf '%s\n' 'log prefix "My \" Rule"' | nft_dump_fields)
got="${got%%${_tab}*}" got="${got%%${_tab}*}"
if [ "$got" != 'My \" Rule' ]; then if [ "$got" != 'My \\" Rule' ]; then
echo "escaped-quote prefix capture: expected 'My \\\" Rule' got '$got'" >&2 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 return 1
fi fi
# Plain prefix still captures unchanged. # Plain prefix still captures unchanged.
@@ -884,6 +983,70 @@ run_selftest() {
echo "empty prefix capture: expected '' got '$got'" >&2 echo "empty prefix capture: expected '' got '$got'" >&2
return 1 return 1
fi 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") got=$(poll_clamp_lines "99999999999999999999")
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "over-long expected $POLL_LINES_MAX got $got" || return 1 check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "over-long expected $POLL_LINES_MAX got $got" || return 1
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.9.16 PKG_VERSION:=26.9.26
PKG_RELEASE:=292 PKG_RELEASE:=293
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ 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 = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0" 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 = s:option(Value, "up_mbps", translate("Max upload Mbps"))
o.rewrite_option = o.option o.rewrite_option = o.option
@@ -140,7 +140,7 @@ function gen_config(var)
tls = { tls = {
sni = node.tls_serverName, sni = node.tls_serverName,
insecure = (node.tls_allowInsecure == "1") and true or false, 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 ech = (node.ech == "1") and node.ech_config or nil
}, },
quic = { quic = {
@@ -11,6 +11,6 @@
2002::/16 2002::/16
3fff::/20 3fff::/20
5f00::/16 5f00::/16
fc00::/7 fd00::/8
fe80::/64 fe80::/64
ff00::/8 ff00::/8
@@ -11,3 +11,17 @@ google.com.tw
google.com.hk google.com.hk
gstatic.com gstatic.com
xn--ngstr-lra8j.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
+2 -2
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196 PKG_VERSION:=196
PKG_RELEASE:=76 PKG_RELEASE:=77
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -32,7 +32,7 @@ LUCI_PKGARCH:=all
LUCI_DEPENDS:= \ LUCI_DEPENDS:= \
+libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \ +libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \ +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:xray-core \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \ +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \
+PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \ +PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \
@@ -5,27 +5,24 @@
local m, s, sec, o local m, s, sec, o
local uci = require "luci.model.uci".cursor() local uci = require "luci.model.uci".cursor()
local URL = require "url" local URL = require "url"
local sys = require "luci.sys"
local util = require "luci.util"
local datatypes = require "luci.cbi.datatypes"
-- 获取 LAN IP 地址 -- 获取 LAN IP 地址
function lanip() function lanip()
local lan_ip local lan_ip
-- 尝试从 UCI 直接读取 -- 尝试从 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) -- 尝试从 LAN 接口信息中读取(优先 ifname,再 fallback 到 device)
if not lan_ip or lan_ip == "" then 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 \ 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']]) | grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
end end
-- 取任意一个 global IPv4 地址 -- 取任意一个 global IPv4 地址
if not lan_ip or lan_ip == "" then 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 \ 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']]) | grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
end end
@@ -34,9 +31,10 @@ ip -4 addr show scope global 2>/dev/null \
end end
local lan_ip = lanip() local lan_ip = lanip()
local validation = require "luci.cbi.datatypes"
local clash_nodes = {} local clash_nodes = {}
local function is_finded(e) 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 end
local function clash_display_name(s) local function clash_display_name(s)
@@ -50,28 +48,6 @@ local function clash_display_name(s)
return "[CLASH]" return "[CLASH]"
end 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 = 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" 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:value("0", translate("Use Local DNS Service listen port 5335"))
o.default = 1 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 = 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.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("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.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("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:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("pdnsd_enable", "1")
o:depends("pdnsd_enable", "7") o:depends("pdnsd_enable", "7")
o.datatype = nil o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
o.description = o.datatype = "ip4addrport"
"<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.default = "8.8.4.4:53" 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 = 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")) 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 = {} local parts = {}
for part in string.gmatch(value, "[^,]+") do for part in string.gmatch(value, "[^,]+") do
part = part:gsub("^%s*", ""):gsub("%s*$", "") 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)")) return nil, translate("Expecting: %s"):format(translate("valid address:port (comma separated)"))
end end
table.insert(parts, part) table.insert(parts, part)
@@ -304,7 +236,7 @@ if is_finded("chinadns-ng") then
return table.concat(parts, ",") return table.concat(parts, ",")
end end
if datatypes.ip4addrport(value) then if validation.ip4addrport(value) then
return value return value
end end
@@ -1,11 +1,8 @@
require "luci.ip" require "luci.ip"
require "nixio.fs" require "nixio.fs"
require "luci.sys" require "luci.sys"
local ucic = luci.model.uci.cursor()
local m, s, o local m, s, o
local enable_mihomo = ucic:get_first('shadowsocksr', 'server_subscribe', 'enable_mihomo') or ''
m = Map("shadowsocksr") m = Map("shadowsocksr")
s = m:section(TypedSection, "access_control") s = m:section(TypedSection, "access_control")
@@ -119,7 +116,7 @@ o.rows = 13
o.wrap = "off" o.wrap = "off"
o.rmempty = true o.rmempty = true
o.cfgvalue = function(self, section) o.cfgvalue = function(self, section)
return nixio.fs.readfile(denydomainconf) or "" return nixio.fs.readfile(denydomainconf) or " "
end end
o.write = function(self, section, value) o.write = function(self, section, value)
nixio.fs.writefile(denydomainconf, value:gsub("\r\n", "\n")) nixio.fs.writefile(denydomainconf, value:gsub("\r\n", "\n"))
@@ -128,54 +125,6 @@ o.remove = function(self, section, value)
nixio.fs.writefile(denydomainconf, "") nixio.fs.writefile(denydomainconf, "")
end 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 if luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 then
m.apply_on_parse = true m.apply_on_parse = true
function m.on_apply(self) function m.on_apply(self)
@@ -188,10 +188,9 @@ local function save_uploaded_clash_node(upload_name, final_path)
local alias local alias
if not sid then if not sid then
local sid_output = luci.sys.exec("uci add shadowsocksr servers 2>/dev/null") sid = uci:add("shadowsocksr", "servers")
sid = trim(sid_output)
end end
if not sid or sid == "" then if not sid then
return nil return nil
end end
@@ -201,50 +200,20 @@ local function save_uploaded_clash_node(upload_name, final_path)
alias = upload_alias(upload_name) alias = upload_alias(upload_name)
end end
local switch_enable = uci:get("shadowsocksr", sid, "switch_enable") uci:set("shadowsocksr", sid, "type", "clash")
if not switch_enable or switch_enable == "" then uci:set("shadowsocksr", sid, "alias", alias)
switch_enable = uci:get_first("shadowsocksr", "server_subscribe", "switch", "1") or "1" uci:set("shadowsocksr", sid, "server", "127.0.0.1")
end uci:set("shadowsocksr", sid, "server_port", "0")
uci:delete("shadowsocksr", sid, "clash_url")
local clash_user_agent = uci:get("shadowsocksr", sid, "clash_user_agent") uci:set("shadowsocksr", sid, "clash_path", final_path)
if not clash_user_agent or clash_user_agent == "" then uci:set("shadowsocksr", sid, "clash_user_agent", uci:get("shadowsocksr", sid, "clash_user_agent") or "clash")
clash_user_agent = "clash" if not uci:get("shadowsocksr", sid, "switch_enable") then
end uci:set("shadowsocksr", sid, "switch_enable", uci:get_first("shadowsocksr", "server_subscribe", "switch", "1") or "1")
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
end 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) 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))) 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 tmp_output = string.format("%s/.upload-%d-%d.yaml", CLASH_YAML_DIR, nixio.getpid(), os.time())
local sid local sid
local alias local alias
local redirect_message_url
nixio.fs.mkdirr(CLASH_YAML_DIR) nixio.fs.mkdirr(CLASH_YAML_DIR)
nixio.fs.remove(tmp_output) nixio.fs.remove(tmp_output)
if preprocess_clash_yaml(upload_tmp_path, tmp_output) then if preprocess_clash_yaml(upload_tmp_path, tmp_output) then
hash = hash_file(tmp_output) 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.") upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
else else
final_path = string.format("%s/%s.yaml", CLASH_YAML_DIR, hash) 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))) 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)
sid, alias = save_uploaded_clash_node(upload_filename, final_path) if sid then
if sid then upload_message = string.format(translate("Custom YAML imported successfully: %s"), alias or sid)
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
else else
upload_errmessage = translate("Failed to move uploaded YAML to target directory.") upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
end end
end end
else 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) nixio.fs.remove(tmp_output)
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
end end
end
if redirect_message_url then if upload_tmp_path then
luci.http.redirect(redirect_message_url) nixio.fs.remove(upload_tmp_path)
return
end
end end
end end
end end
@@ -454,11 +407,8 @@ end
m = Map("shadowsocksr", translate("Servers subscription and manage")) m = Map("shadowsocksr", translate("Servers subscription and manage"))
if upload_errmessage then if upload_errmessage then
m.errmessage = upload_errmessage m.errmessage = upload_errmessage
else elseif upload_message then
local message = luci.http.formvalue("upload_message") m.message = upload_message
if message and message ~= "" then
m.message = message
end
end end
local style_section = m:section(SimpleSection) local style_section = m:section(SimpleSection)
@@ -616,29 +566,6 @@ if has_mihomo then
o.rmempty = true o.rmempty = true
o.description = translate("Disabled means use Xray or ShadowSocks-Rust core.") 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 = s:option(Flag, "sub_convert", translate("Subscribe Convert Online"))
o.description = translate("Convert subscriptions to Clash/Mihomo YAML with a subscription template URL.") o.description = translate("Convert subscriptions to Clash/Mihomo YAML with a subscription template URL.")
o.default = "0" o.default = "0"
@@ -1,4 +1,4 @@
<%+cbi/valueheader%> <%+cbi/valueheader%>
<input class="cbi-input-file" style="width: 400px" type="file" id="clash_yaml_file" name="clash_yaml_file" accept=".yaml,.yml" /> <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%> <%+cbi/valuefooter%>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+36 -78
View File
@@ -608,10 +608,6 @@ prepare_clash_runtime_config() {
local dns_mode local dns_mode
local client_policy_stats local client_policy_stats
local client_rules local client_rules
local enable_fake_ip
local socks5_auth
local socks5_user
local socks5_pass
cache_file="$(get_clash_cache_file "$sid")" cache_file="$(get_clash_cache_file "$sid")"
workdir="$(get_clash_workdir "$sid")" workdir="$(get_clash_workdir "$sid")"
@@ -627,7 +623,6 @@ prepare_clash_runtime_config() {
dns_mode="$(uci_get_by_type global pdnsd_enable 0)" dns_mode="$(uci_get_by_type global pdnsd_enable 0)"
client_policy_stats="" client_policy_stats=""
client_rules=0 client_rules=0
enable_fake_ip="$(uci_get_by_type server_subscribe enable_fake_ip "")"
[ -s "$cache_file" ] || return 1 [ -s "$cache_file" ] || return 1
@@ -645,35 +640,15 @@ prepare_clash_runtime_config() {
routing-mark: $CLASH_OUTBOUND_MARK routing-mark: $CLASH_OUTBOUND_MARK
profile: profile:
store-selected: true store-selected: true
store-fake-ip: true
tun: tun:
enable: false 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 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 if [ -n "$socks_port" ] && [ "$socks_port" != "0" ]; then
echo "socks-port: $socks_port" >>"$overlay_file" echo "socks-port: $socks_port" >>"$overlay_file"
@@ -1273,24 +1248,14 @@ start_dns() {
ssrplus_dns="$(uci_get_by_type global pdnsd_enable 0)" ssrplus_dns="$(uci_get_by_type global pdnsd_enable 0)"
global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)" global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)"
builtin_dns_enabled=0 builtin_dns_enabled=0
dnsserver="$(uci_get_by_type global tunnel_forward 8.8.4.4:53)"
if [ "$ssrplus_dns" = "5" ]; then if [ "$ssrplus_dns" = "5" ]; then
ssrplus_dns="$(force_dns2tcp_fallback "旧版 DNSPROXY 模式已移除")" || return 1 ssrplus_dns="$(force_dns2tcp_fallback "旧版 DNSPROXY 模式已移除")" || return 1
uci_set_by_type global pdnsd_enable "$ssrplus_dns" 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 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)" run_mode="$(normalize_run_mode)"
if [ "$ssrplus_dns" = "4" ] && ! is_finded "mosdns"; then if [ "$ssrplus_dns" = "4" ] && ! is_finded "mosdns"; then
@@ -1556,15 +1521,15 @@ generate_xray_config() {
{ {
"log": { "log": {
"loglevel": "warning" "loglevel": "warning"
}, },
"inbounds": [$inbound_json], "inbounds": [$inbound_json],
"outbounds": [ "outbounds": [
{ {
"protocol": "freedom", "protocol": "freedom",
"settings": {} "settings": {}
} }
] ]
} }
EOF EOF
return 0 return 0
} }
@@ -2940,18 +2905,12 @@ start_rules() {
local ac_ips local ac_ips
local custom_ports local custom_ports
local proxyport local proxyport
local enable_fake_ip
server=$(get_host_ip "$GLOBAL_SERVER") server=$(get_host_ip "$GLOBAL_SERVER")
local_port=$(get_default_node_local_port) local_port=$(get_default_node_local_port)
lan_ac_ips=$(uci_get_by_type access_control lan_ac_ips) lan_ac_ips=$(uci_get_by_type access_control lan_ac_ips)
lan_ac_mode=$(uci_get_by_type access_control lan_ac_mode) 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 if [ "$kcp_enable_flag" = "0" ] && [ "$redir_udp" = "1" ]; then
udp_server=$(get_host_ip "$UDP_RELAY_SERVER") udp_server=$(get_host_ip "$UDP_RELAY_SERVER")
udp_local_port=$tmp_udp_port udp_local_port=$tmp_udp_port
@@ -2996,27 +2955,26 @@ start_rules() {
fi fi
elif [ "$USE_TABLES" = "iptables" ]; then elif [ "$USE_TABLES" = "iptables" ]; then
ARG_A="" ARG_A=""
fi fi
/usr/share/shadowsocksr/gfw2ipset.sh /usr/share/shadowsocksr/gfw2ipset.sh
/usr/bin/ssr-rules \ /usr/bin/ssr-rules \
-s "$server" \ -s "$server" \
-l "$local_port" \ -l "$local_port" \
-S "$udp_server" \ -S "$udp_server" \
-L "$udp_local_port" \ -L "$udp_local_port" \
-a "$ac_ips" \ -a "$ac_ips" \
-i "/etc/ssrplus/china_ssr.txt" \ -i "/etc/ssrplus/china_ssr.txt" \
-b "$(uci_get_by_type access_control wan_bp_ips)" \ -b "$(uci_get_by_type access_control wan_bp_ips)" \
-w "$(uci_get_by_type access_control wan_fw_ips)" \ -w "$(uci_get_by_type access_control wan_fw_ips)" \
-B "$(uci_get_by_type access_control lan_bp_ips)" \ -B "$(uci_get_by_type access_control lan_bp_ips)" \
-p "$(uci_get_by_type access_control lan_fp_ips)" \ -p "$(uci_get_by_type access_control lan_fp_ips)" \
-G "$(uci_get_by_type access_control lan_gm_ips)" \ -G "$(uci_get_by_type access_control lan_gm_ips)" \
-m "$(uci_get_by_type access_control Interface)" \ -m "$(uci_get_by_type access_control Interface)" \
-D "$proxyport" \ -D "$proxyport" \
-E "$enable_fake_ip" \ "$(get_arg_out)" "$(gfwmode)" "$ARG_UDP" "$ARG_UDP_RULES" "$ARG_A"
"$(get_arg_out)" "$(gfwmode)" "$ARG_UDP" "$ARG_UDP_RULES" "$ARG_A"
return $? return $?
} }
start() { start() {
local run_mode 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' uci -q set shadowsocksr.@server_subscribe[0].auto_update_min_time='0'
fi 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 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' uci -q set shadowsocksr.@server_subscribe[0].config_auto_update_mode='0'
fi fi
+77 -280
View File
@@ -46,7 +46,6 @@ CHECK_STATUS=0
RESTORE_RULES=0 RESTORE_RULES=0
FLUSH_RULES=0 FLUSH_RULES=0
CLEANUP_PERSISTENCE=0 CLEANUP_PERSISTENCE=0
ENABLE_FAKE_IP=0
# ASCII code for SSRP.Use whatever,just not the same. # ASCII code for SSRP.Use whatever,just not the same.
FWMARK="0x53535250" FWMARK="0x53535250"
@@ -84,15 +83,14 @@ usage() {
-m <Interface> Interface name -m <Interface> Interface name
-I <ip_list_file> a file content is bypassed shunt ip list -I <ip_list_file> a file content is bypassed shunt ip list
-e <extra_options> extra options for iptables -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 rules to the OUTPUT chain
-O apply the global 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, TPROXY is required
-U enable udprelay mode, using different IP -U enable udprelay mode, using different IP
and ports for TCP and UDP and ports for TCP and UDP
-y disable all auxiliary UDP rules when UDP -y disable all auxiliary UDP rules when UDP
transparent proxy is unavailable transparent proxy is unavailable
-f flush the rules -f flush the rules
-g gfwlist mode -g gfwlist mode
-r router mode -r router mode
-z all mode -z all mode
@@ -234,12 +232,6 @@ cleanup_persistence_files() {
loger 5 "Removed xhttp hash file: /tmp/.last_xhttp_hash" loger 5 "Removed xhttp hash file: /tmp/.last_xhttp_hash"
fi 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" loger 5 "Persistence cleanup completed"
return 0 return 0
} }
@@ -323,64 +315,25 @@ flush_nftables() {
flush_iptables_legacy() { flush_iptables_legacy() {
flush_iptables() { flush_iptables() {
local table=$1 local ipt="iptables -t $1"
local ipt="iptables -t $table" local DAT=$(iptables-save -t $1)
local DAT eval $(echo "$DAT" | grep "$TAG" | sed -e 's/^-A/$ipt -D/' -e 's/$/;/')
for chain in $(echo "$DAT" | awk '/^:SS_SPEC/{print $1}'); do
DAT=$(iptables-save -t $table 2>/dev/null) $ipt -F ${chain:1} 2>/dev/null && $ipt -X ${chain:1}
[ -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"
done 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 nat
flush_iptables mangle flush_iptables mangle
if ip rule show | grep -Eq "fwmark ${FWMARK}.*lookup 999"; then
# 清理策略路由与路由表
if ip rule show | grep -Eiq "fwmark ${FWMARK}.*lookup 999"; then
ip rule del fwmark ${FWMARK} table 999 2>/dev/null ip rule del fwmark ${FWMARK} table 999 2>/dev/null
fi fi
if ip route show table 999 | grep -Eq "^local.*dev lo"; then 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 ip route del local 0.0.0.0/0 dev lo table 999 2>/dev/null
fi fi
# 清理 IPSET 集合(先 -F 清空,后 -X 销毁)
for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \ for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \
china fplan bplan gmlan whitelist blacklist gfwlist music; do china fplan bplan gmlan whitelist blacklist gfwlist music; do
ipset -F $setname 2>/dev/null
ipset -X $setname 2>/dev/null ipset -X $setname 2>/dev/null
done done
[ -n "$FWI" ] && echo '#!/bin/sh' >$FWI [ -n "$FWI" ] && echo '#!/bin/sh' >$FWI
return 0 return 0
} }
@@ -571,11 +524,6 @@ fw_rule() {
} }
fw_rule_nft() { 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 # redirect/translation: when PROXY_PORTS present, redirect those tcp ports to local_port
if [ -n "$PROXY_PORTS" ]; then if [ -n "$PROXY_PORTS" ]; then
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //') 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 $IPT -A SS_SPEC_WAN_FW -d "$net" -j RETURN
done 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 || { $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." loger 3 "Can't redirect TCP, please check the iptables."
exit 1 exit 1
@@ -667,33 +611,6 @@ ac_rule_nft() {
fi fi
$NFT flush chain inet ss_spec ss_spec_prerouting 2>/dev/null $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 # Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_prerouting >/dev/null 2>&1; then 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 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 # 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
# Block UDP port 443 when TPROXY not Enable # Block UDP port 443 when TPROXY not Enable
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then
# Add UDP 443 block rule # Add UDP 443 block rule
@@ -731,17 +658,6 @@ ac_rule_nft() {
done done
fi fi
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 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 # 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 if [ -n "$MATCH_SET" ]; then
@@ -767,16 +683,11 @@ ac_rule_nft() {
case "$OUTPUT" in case "$OUTPUT" in
1) 1)
# Create ss_spec_output tcp chain # Create ss_spec_output tcp chain
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then 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; }' $NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null $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 $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
# Exclude special local addresses # Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
@@ -797,16 +708,11 @@ ac_rule_nft() {
;; ;;
2) 2)
# Create ss_spec_output tcp chain # Create ss_spec_output tcp chain
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then 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; }' $NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null $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 $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
# Exclude special local addresses # Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
@@ -848,38 +754,11 @@ ac_rule_iptables() {
return 2 return 2
;; ;;
esac 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 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 # Block UDP port 443 when TPROXY not Enable
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then
@@ -905,67 +784,42 @@ ac_rule_iptables() {
done done
fi fi
fi fi
if [ -z "$Interface" ]; then
# 拦截 Fake-IP 流量 (198.18.0.0/16),受 Interface 和 LAN_AC 控制 # Global rules
if [ "$ENABLE_FAKE_IP" = "1" ]; then if [ -n "$MATCH_SET" ]; then
if [ -z "$Interface" ]; then $IPT -I PREROUTING 1 -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
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
else else
for name in $Interface; do $IPT -I PREROUTING 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
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
fi 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 fi
case "$OUTPUT" in case "$OUTPUT" in
1) 1)
# fake-ip first $IPT -I OUTPUT 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
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
;; ;;
2) 2)
ipset -! -R <<-EOF || return 1 ipset -! -R <<-EOF || return 1
create ssr_gen_router hash:net create ssr_gen_router hash:net
$(gen_spec_iplist | sed -e "s/^/add ssr_gen_router /") $(gen_spec_iplist | sed -e "s/^/add ssr_gen_router /")
EOF EOF
$IPT -N SS_SPEC_ROUTER 2>/dev/null $IPT -N SS_SPEC_ROUTER 2>/dev/null
$IPT -F 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 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 -m set --match-set ssr_gen_router dst -j RETURN && \
$IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW $IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
# fake-ip first $IPT -I OUTPUT 1 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
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
;; ;;
esac esac
return $? return $?
@@ -1001,29 +855,29 @@ tp_rule_nft() {
fi fi
local MATCH_SET="" local MATCH_SET=""
local EXT_ARGS=""
if [ -n "$PROXY_PORTS" ]; then if [ -n "$PROXY_PORTS" ]; then
local PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //') PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
[ -n "$PORTS_ARGS" ] && EXT_ARGS="udp dport { $PORTS_ARGS }" if [ -n "$PORTS_ARGS" ]; then
EXT_ARGS="udp dport { $PORTS_ARGS }"
else
EXT_ARGS=""
fi
fi fi
if [ -n "$LAN_AC_IP" ]; then if [ -n "$LAN_AC_IP" ]; then
# Create LAN access control set if needed # 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 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 $NFT add set ip ss_spec_mangle ss_spec_lan_ac '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
else else
$NFT flush set ip ss_spec_mangle ss_spec_lan_ac 2>/dev/null $NFT flush set ip ss_spec_mangle ss_spec_lan_ac 2>/dev/null
fi 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 [ -n "$ip" ] && $NFT add element ip ss_spec_mangle ss_spec_lan_ac "{ $ip }" 2>/dev/null
done done
case "$mode" in case "${LAN_AC_IP%${LAN_AC_IP#?}}" in
w | W) w | W)
MATCH_SET="ip saddr @ss_spec_lan_ac" MATCH_SET="ip saddr @ss_spec_lan_ac"
;; ;;
@@ -1052,10 +906,8 @@ tp_rule_nft() {
fi fi
# Bulk import xhttp ip list into nft whitelist (server + shunt) # Bulk import xhttp ip list into nft whitelist (server + shunt)
local xhttp_file="${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then
if [ -s "$xhttp_file" ]; then $NFT add element ip ss_spec_mangle whitelist "{ $(tr '\n' ',' < "${xhttp_ip}" | sed 's/,$//') }" 2>/dev/null
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
fi fi
# use priority mangle for compatibility with other rules # 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 $NFT flush chain ip ss_spec_mangle ss_spec_tproxy 2>/dev/null
fi 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 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 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 $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 $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 # avoid redirecting to udp server address
# if [ -n "$server" ]; then 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 $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport != 53 ip daddr "$server" return 2>/dev/null
# fi fi
# if server != SERVER add SERVER to whitelist set (so tproxy won't touch it) # if server != SERVER add SERVER to whitelist set (so tproxy won't touch it)
if [ -n "$server" ]; then if [ -n "$server" ]; then
@@ -1100,9 +946,6 @@ tp_rule_nft() {
$NFT add element ip ss_spec_mangle whitelist "{ $SERVER }" 2>/dev/null $NFT add element ip ss_spec_mangle whitelist "{ $SERVER }" 2>/dev/null
fi fi
# 显式豁免白名单集合中的 IP
$NFT add rule ip ss_spec_mangle ss_spec_tproxy ip daddr @whitelist return 2>/dev/null
# access control and tproxy rules # 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 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 $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 @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 $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 $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 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 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 $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 $NFT add set ip ss_spec_mangle gfwlist '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
fi fi
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @china 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 $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 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 $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 $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 # finally, ensure prerouting hook entry to jump to tproxy chain
if ! $NFT list chain ip ss_spec_mangle prerouting >/dev/null 2>&1; then 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; }' $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 fi
# add prerouting jump (idempotent) # add prerouting jump (idempotent)
@@ -1219,14 +1056,8 @@ tp_rule_iptables() {
do do
$ipt -A SS_SPEC_TPROXY -p udp -d "$net" -j RETURN $ipt -A SS_SPEC_TPROXY -p udp -d "$net" -j RETURN
done 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" ] && $ipt -A SS_SPEC_TPROXY -p udp ! --dport 53 -d "$SERVER" -j RETURN
[ -n "$SERVER" ] && [ "$server" != "$SERVER" ] && ipset -! add whitelist "$SERVER" [ -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 if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then
while IFS= read -r ip; do while IFS= read -r ip; do
[ -n "$ip" ] && ipset add whitelist "$ip" -exist [ -n "$ip" ] && ipset add whitelist "$ip" -exist
@@ -1234,7 +1065,6 @@ tp_rule_iptables() {
fi fi
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set bplan src -j RETURN $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 $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 case "$RUNMODE" in
router) router)
ipset -! -R <<-EOF || return 1 ipset -! -R <<-EOF || return 1
@@ -1243,27 +1073,15 @@ tp_rule_iptables() {
EOF 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 ss_spec_wan_ac dst -j RETURN
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set china 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 $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
$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 -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 -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} $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) gfw)
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set china 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 $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
$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 $PROXY_PORTS -m set --match-set gfwlist dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK} $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} $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 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 case "$arg" in
m) m)
Interface=$OPTARG 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) e)
EXT_ARGS=$OPTARG EXT_ARGS=$OPTARG
;; ;;
E)
ENABLE_FAKE_IP=$OPTARG
;;
a) a)
LAN_AC_IP=$OPTARG LAN_AC_IP=$OPTARG
;; ;;
@@ -1874,14 +1689,6 @@ if [ -n "$local_port" ]; then
if [ "$USE_NFT" = "1" ]; then if [ "$USE_NFT" = "1" ]; then
# NFTables # 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 # Save previous TPROXY state file
TPROXY_STATE_FILE="/tmp/.last_tproxy" TPROXY_STATE_FILE="/tmp/.last_tproxy"
if [ -f "$TPROXY_STATE_FILE" ]; then if [ -f "$TPROXY_STATE_FILE" ]; then
@@ -1971,13 +1778,6 @@ if [ -n "$local_port" ]; then
LAST_XHTTP_FILE_HASH="" LAST_XHTTP_FILE_HASH=""
fi 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) # STEP 1: Check if TPROXY has value (1 or 2)
if [ "$TPROXY" = "1" ] || [ "$TPROXY" = "2" ]; then if [ "$TPROXY" = "1" ] || [ "$TPROXY" = "2" ]; then
TPROXY_HAS_VALUE=1 TPROXY_HAS_VALUE=1
@@ -2073,9 +1873,7 @@ if [ -n "$local_port" ]; then
# 1. TPROXY changes from empty ↔ has value # 1. TPROXY changes from empty ↔ has value
# 2. PROXY_PORTS changes from empty ↔ has value # 2. PROXY_PORTS changes from empty ↔ has value
# 3. Any IP list has changed # 3. Any IP list has changed
# 4. ENABLE_FAKE_IP state changed if [ "$TPROXY_HAS_VALUE" != "$LAST_HAS_VALUE" ] || \
if [ "$ENABLE_FAKE_IP_CHANGED" = "1" ] || \
[ "$TPROXY_HAS_VALUE" != "$LAST_HAS_VALUE" ] || \
[ "$PROXY_HAS_VALUE" != "$LAST_PROXY_HAS_VALUE" ] || \ [ "$PROXY_HAS_VALUE" != "$LAST_PROXY_HAS_VALUE" ] || \
[ "$ANY_IP_LIST_CHANGED" = "1" ]; then [ "$ANY_IP_LIST_CHANGED" = "1" ]; then
FORCE_RECREATE=1 FORCE_RECREATE=1
@@ -2093,7 +1891,6 @@ if [ -n "$local_port" ]; then
fi fi
# STEP 4: Save current state # STEP 4: Save current state
echo "$ENABLE_FAKE_IP" > "$ENABLE_FAKE_IP_STATE_FILE"
echo "$TPROXY" > "$TPROXY_STATE_FILE" echo "$TPROXY" > "$TPROXY_STATE_FILE"
echo "$PROXY_PORTS" > "$PROXY_PORTS_STATE_FILE" echo "$PROXY_PORTS" > "$PROXY_PORTS_STATE_FILE"
echo "$(normalize_ip_list "$WAN_BP_IP")" > "$WAN_BP_IP_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" require "luci.sys"
local ucursor = require "luci.model.uci".cursor() local ucursor = require "luci.model.uci".cursor()
local json = require "luci.jsonc" 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 -- 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 -- 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) return host .. ":" .. tostring(port)
end 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) local function is_finded(e)
return luci.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)) ~= ""
@@ -448,141 +391,31 @@ local Xray = {
} }
if server.type == "v2ray" and dns_mode == "7" and os.getenv("SSR_SWITCH_PROBE") ~= "1" then 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 = { Xray.dns = {
queryStrategy = (dns_ipv4_only == "1") and "UseIPv4" or "UseIP", 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, { table.insert(Xray.inbounds, {
listen = "127.0.0.1", listen = "127.0.0.1",
port = 5335, port = 5335,
protocol = "dokodemo-door", protocol = "dokodemo-door",
settings = { settings = {
address = address_str, address = dns_host,
-- address = first_host, -- 单个地址 port = dns_port,
port = target_port,
-- port = first_port, -- 对应端口
network = "tcp,udp" network = "tcp,udp"
}, },
tag = "builtin-dns-in" tag = "builtin-dns-in"
}) })
xray_builtin_dns = { xray_builtin_dns = {
address = address_str, address = dns_host,
-- address = outbound_address, --dokodemo-door 入站的 address 单个地址时使用。 port = dns_port
port = target_port
} }
end end
-- 传入连接 -- 传入连接
@@ -27,9 +27,6 @@ config server_subscribe
option auto_update_week_time '*' option auto_update_week_time '*'
option auto_update_day_time '2' option auto_update_day_time '2'
option auto_update_min_time '0' 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 url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99' option user_agent 'v2rayN/9.99'
option filter_words '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国' option filter_words '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国'
+3 -3
View File
@@ -17,12 +17,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wwand PKG_NAME:=luci-app-wwand
PKG_VERSION:=1.6.8_p1 PKG_VERSION:=1.6.8_p4
PKG_RELEASE:=29 PKG_RELEASE:=30
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.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_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only PKG_LICENSE:=GPL-2.0-only
@@ -360,6 +360,7 @@ return baseclass.extend({
visible: true, visible: true,
value: band value: band
}, },
hostname : { hostname : {
title: _('Hostname'), title: _('Hostname'),
visible: true, visible: true,
@@ -56,7 +56,10 @@ function probeColor(expr) {
* shared with `luci-app-*`, and an app's unlayered `span { color: … !important }` outranks * 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 * 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. */ * 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;' _probe.style.cssText = 'position:fixed!important;left:-9999px!important;top:0!important;'
+ 'width:0!important;height:0!important;overflow:hidden!important;' + 'width:0!important;height:0!important;overflow:hidden!important;'
+ 'pointer-events:none!important;'; + 'pointer-events:none!important;';
@@ -110,6 +110,12 @@ let _probe = null;
function resolveLen(token, dflt) { function resolveLen(token, dflt) {
if (!_probe) { if (!_probe) {
_probe = document.createElement('div'); _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'); _probe.setAttribute('aria-hidden', 'true');
/* out of flow, no box, no ink: it must never affect layout, scroll extent or hit-testing */ /* 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;' + _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; let _wired = false;
/* the pathname whose view is currently rendered; popstate compares against it to tell a real /* the pathname whose view is currently rendered; popstate compares against it to tell a real
* navigation from a fragment change */ * 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) */ * only exit leaving both pages correct is a real navigation (fs-sheets.js) */
if (sheets.documentPoisoned()) return false; 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 /* …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) */ * correctly is the login form, and only a real navigation gets there (watchSession) */
if (_expired) return false; 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 /* 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 * pointer/focus triggers above can see. The edge points search -> router, because the router
* must keep no dependency on the palette. */ * 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) %} {% const fs_page = entityencode(join('-', length(ctx.path) ? ctx.path : ctx.request_path), true) %}
<body class="lang_{{ dispatcher.lang }}" data-page="{{ fs_page }}"> <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 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, uploaded SVG takes the theme's ink. A mask applies to an element's whole rendering,
+2 -2
View File
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437 MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
PKG_NAME:=mise PKG_NAME:=mise
PKG_VERSION:=2026.9.13 PKG_VERSION:=2026.9.14
PKG_RELEASE:=20 PKG_RELEASE:=21
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz 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_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
PKG_HASH:=skip PKG_HASH:=skip
+2 -2
View File
@@ -2,8 +2,8 @@ include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/kernel.mk include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=tcp-brutal PKG_NAME:=tcp-brutal
PKG_VERSION:=2.0.0 PKG_VERSION:=2.0.1
PKG_RELEASE:=2 PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/apernet/tcp-brutal/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/apernet/tcp-brutal/tar.gz/v$(PKG_VERSION)?
+102
View File
@@ -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))
+154
View File
@@ -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))
+833
View File
@@ -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}}
+58
View File
@@ -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
View File
@@ -50,12 +50,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=wwand PKG_NAME:=wwand
PKG_VERSION:=1.6.8_p6 PKG_VERSION:=1.6.8_p23
PKG_RELEASE:=56 PKG_RELEASE:=57
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=2d7803e46b95f60a8a890ccd961f5154ba672424 PKG_SOURCE_VERSION:=cf8d5cf1cc5767d58824a02c61de4d9d2ff58cf6
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only 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 \ 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 \ 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 \ 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 \ 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 \ 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 \ simops.uc sms.uc sms_pdu.uc telemetry_mbim.uc telemetry_ncm.uc \