🐶 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_PKGARCH:=all
PKG_VERSION:=0.1.45
PKG_RELEASE:=38
PKG_VERSION:=0.1.46
PKG_RELEASE:=39
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH
@@ -9,7 +9,7 @@
*/
return baseclass.extend({
/* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */
APP_VERSION: '0.1.45',
APP_VERSION: '0.1.46',
ROW_LIMIT_OPTIONS: [25, 50, 100, 250, 500, 1000, 2000],
DEFAULT_ROW_LIMIT: 100,
FETCH_MODE_OPTIONS: ['auto', 'manual'],
@@ -262,6 +262,8 @@ function buildEmptyStateNodes(state, callbacks) {
);
}
if (!st) return nodes;
if (blocker === 'no_wan_zone') {
nodes.push(
E(
@@ -746,11 +746,11 @@ return view.extend({
this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true);
} catch (_e) {
if (this.viewDisposed) return;
/* Keep last toolbar state. Do not clobber a toggle success/failure notice. */
/* Keep last-known toolbar; unknown until the first successful fetch. */
if (!this.loggingNotice)
this.loggingNotice = _(
'Could not refresh logging status; showing the last known state.'
);
this.loggingNotice = this.loggingStatus
? _('Could not refresh logging status; showing the last known state.')
: _('Could not load logging status.');
}
this.updateBackendUi();
this.updateLoggingToolbarUi();
+22 -18
View File
@@ -160,7 +160,11 @@ msgstr "Protokollierung konnte nicht deaktiviert werden."
msgid "Could not enable logging."
msgstr "Protokollierung konnte nicht aktiviert werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:753
msgid "Could not load logging status."
msgstr "Protokollstatus konnte nicht geladen werden."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:752
msgid "Could not refresh logging status; showing the last known state."
msgstr "Protokollstatus konnte nicht aktualisiert werden; zeige den zuletzt bekannten Zustand."
@@ -367,7 +371,7 @@ msgstr ""
"möglicherweise keine Erfolgs/Fehler-CSS-Variablen; fwlive fällt auf lokale "
"Farben zurück (air-gapped, keine Daten verlassen das Gerät)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323
msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide."
@@ -393,15 +397,15 @@ msgid "Interface (prefix ! to exclude)"
msgstr "Schnittstelle (! voranstellen zum Ausschließen)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361
msgid "I’ll configure this under Network → Firewall"
msgstr "Ich konfiguriere das unter Netzwerk → Firewall"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:284
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286
msgid "Kernel log modules missing"
msgstr "Kernel-Logmodule fehlen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:287
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289
msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
@@ -426,7 +430,7 @@ msgstr "Eingeschränkte Diagnose — timeout-Befehl fehlt"
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "Die Live-Ansicht kann unvollständig sein — eine veraltete iptables-Tabelle ist registriert"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:330
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332
msgid "Logging is off on this router"
msgstr "Protokollierung ist auf diesem Router deaktiviert"
@@ -447,7 +451,7 @@ msgstr ""
"Manuell nutzt weiterhin Serverschutz und Abfrageintervall; es ändert das "
"Abrufbudget, nicht das Abfrageintervall."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:378
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380
msgid "Manual test (System → Terminal):"
msgstr "Manueller Test (System → Terminal):"
@@ -470,14 +474,14 @@ msgstr "Weitere Filter"
msgid "Network → Firewall"
msgstr "Netzwerk → Firewall"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:275
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277
msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
msgstr ""
"Keine WAN-Firewallzone in /etc/config/firewall gefunden. Zonen konfigurieren "
"unter:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:270
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272
msgid "No WAN zone found"
msgstr "Keine WAN-Zone gefunden"
@@ -492,7 +496,7 @@ msgstr ""
msgid "Not now"
msgstr "Nicht jetzt"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:353
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355
msgid "Nothing changes until you click Enable."
msgstr "Nichts ändert sich, bis Sie auf Aktivieren klicken."
@@ -504,11 +508,11 @@ msgstr "AUSGANG"
msgid "One line"
msgstr "Einzeilig"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:326
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328
msgid "Open firewall zone settings"
msgstr "Firewall-Zonen-Einstellungen öffnen"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:333
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. "
"Live View only shows what the firewall already logs — it does not add allow/"
@@ -719,11 +723,11 @@ msgstr ""
msgid "Theme tint fallback"
msgstr "Theme-Farb-Fallback"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:386
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388
msgid "Then ping the router."
msgstr "Dann den Router anpingen."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306
msgid ""
"This router reported a logging blocker that Live View does not recognize yet."
msgstr ""
@@ -752,7 +756,7 @@ msgstr ""
"Über das WAN-Protokollierungs-Steuerelement in der Beobachtungsleiste wieder "
"deaktivieren."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:347
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349
msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
@@ -786,7 +790,7 @@ msgstr "Ansicht"
msgid "WAN drop/reject logging is off."
msgstr "WAN-Drop/Reject-Protokollierung ist aus."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:314
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316
msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not."
@@ -815,7 +819,7 @@ msgid "WAN logging on (%s). Click to disable."
msgstr "WAN-Protokollierung: EIN (%s). Klicken zum Deaktivieren."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:301
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
msgid "WAN logging unavailable"
msgstr "WAN-Protokollierung nicht verfügbar"
@@ -827,7 +831,7 @@ msgstr "WAN-Protokollierung nicht verfügbar: fehlende Kernel-Log-Module"
msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN-Protokollierung nicht verfügbar: keine WAN-Zone"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:311
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
msgid "Waiting for firewall events"
msgstr "Warte auf Firewall-Ereignisse"
+22 -18
View File
@@ -157,7 +157,11 @@ msgstr "Не удалось отключить журналирование."
msgid "Could not enable logging."
msgstr "Не удалось включить журналирование."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:753
msgid "Could not load logging status."
msgstr "Не удалось загрузить состояние журналирования."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:752
msgid "Could not refresh logging status; showing the last known state."
msgstr "Не удалось обновить состояние журналирования; показан последний известный статус."
@@ -360,7 +364,7 @@ msgstr ""
"содержать CSS-переменные success/error или info/warn; fwlive использует "
"локальные цвета (работает автономно, данные не покидают устройство)."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323
msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide."
@@ -386,15 +390,15 @@ msgid "Interface (prefix ! to exclude)"
msgstr "Интерфейс (! в начале для исключения)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361
msgid "I’ll configure this under Network → Firewall"
msgstr "Я настрою это в разделе Сеть → Межсетевой экран"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:284
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286
msgid "Kernel log modules missing"
msgstr "Отсутствуют модули журналирования ядра"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:287
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289
msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
@@ -419,7 +423,7 @@ msgstr "Ограниченная диагностика — отсутствуе
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "Живой просмотр может быть неполным — зарегистрирована устаревшая таблица iptables"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:330
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332
msgid "Logging is off on this router"
msgstr "Журналирование на этом роутере отключено"
@@ -440,7 +444,7 @@ msgstr ""
"«Вручную» по-прежнему использует защиту сервера и частоту опроса; меняется "
"бюджет выборки, а не интервал опроса."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:378
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380
msgid "Manual test (System → Terminal):"
msgstr "Ручной тест (Система → Терминал):"
@@ -463,14 +467,14 @@ msgstr "Больше фильтров"
msgid "Network → Firewall"
msgstr "Сеть → Межсетевой экран"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:275
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277
msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
msgstr ""
"WAN-зона межсетевого экрана не найдена в /etc/config/firewall. Настройте "
"зоны в разделе:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:270
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272
msgid "No WAN zone found"
msgstr "WAN-зона не найдена"
@@ -483,7 +487,7 @@ msgstr "WAN-зона не найдена; без неё нельзя перек
msgid "Not now"
msgstr "Не сейчас"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:353
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355
msgid "Nothing changes until you click Enable."
msgstr "Ничего не изменится, пока вы не нажмёте «Включить»."
@@ -495,11 +499,11 @@ msgstr "ВЫХОД"
msgid "One line"
msgstr "Одна строка"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:326
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328
msgid "Open firewall zone settings"
msgstr "Открыть настройки зон файрвола"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:333
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. "
"Live View only shows what the firewall already logs — it does not add allow/"
@@ -705,11 +709,11 @@ msgstr ""
msgid "Theme tint fallback"
msgstr "Запасной цвет темы"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:386
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388
msgid "Then ping the router."
msgstr "Затем отправьте ping на роутер."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306
msgid ""
"This router reported a logging blocker that Live View does not recognize yet."
msgstr ""
@@ -737,7 +741,7 @@ msgid "Turn it back off with the WAN logging control on the watch strip."
msgstr ""
"Отключите её снова элементом управления журналом WAN на панели наблюдения."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:347
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349
msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
@@ -771,7 +775,7 @@ msgstr "Вид"
msgid "WAN drop/reject logging is off."
msgstr "Журналирование drop/reject для WAN отключено."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:314
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316
msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not."
@@ -801,7 +805,7 @@ msgid "WAN logging on (%s). Click to disable."
msgstr "WAN-журнал: ВКЛ (%s). Нажмите, чтобы отключить."
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:301
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
msgid "WAN logging unavailable"
msgstr "WAN-журналирование недоступно"
@@ -813,7 +817,7 @@ msgstr "WAN-журналирование недоступно: отсутств
msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN-журналирование недоступно: нет зоны WAN"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:311
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
msgid "Waiting for firewall events"
msgstr "Ожидание событий межсетевого экрана"
@@ -133,7 +133,11 @@ msgstr ""
msgid "Could not enable logging."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:753
msgid "Could not load logging status."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:752
msgid "Could not refresh logging status; showing the last known state."
msgstr ""
@@ -317,7 +321,7 @@ msgid ""
"data leaves the device)."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323
msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide."
@@ -341,15 +345,15 @@ msgid "Interface (prefix ! to exclude)"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361
msgid "I’ll configure this under Network → Firewall"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:284
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286
msgid "Kernel log modules missing"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:287
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289
msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
@@ -371,7 +375,7 @@ msgstr ""
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:330
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332
msgid "Logging is off on this router"
msgstr ""
@@ -390,7 +394,7 @@ msgid ""
"budget, not the polling interval."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:378
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380
msgid "Manual test (System → Terminal):"
msgstr ""
@@ -413,12 +417,12 @@ msgstr ""
msgid "Network → Firewall"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:275
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277
msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:270
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272
msgid "No WAN zone found"
msgstr ""
@@ -431,7 +435,7 @@ msgstr ""
msgid "Not now"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:353
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355
msgid "Nothing changes until you click Enable."
msgstr ""
@@ -443,11 +447,11 @@ msgstr ""
msgid "One line"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:326
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328
msgid "Open firewall zone settings"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:333
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. "
"Live View only shows what the firewall already logs — it does not add allow/"
@@ -633,11 +637,11 @@ msgstr ""
msgid "Theme tint fallback"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:386
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388
msgid "Then ping the router."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306
msgid ""
"This router reported a logging blocker that Live View does not recognize yet."
msgstr ""
@@ -662,7 +666,7 @@ msgstr ""
msgid "Turn it back off with the WAN logging control on the watch strip."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:347
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349
msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
@@ -690,7 +694,7 @@ msgstr ""
msgid "WAN drop/reject logging is off."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:314
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316
msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not."
@@ -715,7 +719,7 @@ msgid "WAN logging on (%s). Click to disable."
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:301
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
msgid "WAN logging unavailable"
msgstr ""
@@ -727,7 +731,7 @@ msgstr ""
msgid "WAN logging unavailable: no WAN zone"
msgstr ""
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:311
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
msgid "Waiting for firewall events"
msgstr ""
+22 -18
View File
@@ -148,7 +148,11 @@ msgstr "无法禁用日志记录。"
msgid "Could not enable logging."
msgstr "无法启用日志记录。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:751
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:753
msgid "Could not load logging status."
msgstr "无法加载日志状态。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:752
msgid "Could not refresh logging status; showing the last known state."
msgstr "无法刷新日志状态;显示上次已知状态。"
@@ -341,7 +345,7 @@ msgstr ""
"如果行着色看起来缺失,当前 LuCI 主题可能未提供成功/错误或信息/警告 CSS 变量;"
"fwlive 会回退到本地颜色(隔离运行,数据不会离开本设备)。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:321
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:323
msgid ""
"If the WAN is quiet, wait for probes or use the optional ping check in "
"Help / the enabling-logs guide."
@@ -367,15 +371,15 @@ msgid "Interface (prefix ! to exclude)"
msgstr "接口(前缀 ! 表示排除)"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:244
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:359
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:361
msgid "I’ll configure this under Network → Firewall"
msgstr "稍后我会在 网络 → 防火墙 中配置"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:284
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:286
msgid "Kernel log modules missing"
msgstr "缺少内核日志模块"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:287
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:289
msgid ""
"Kernel netfilter log modules are missing. Install kmod-nf-log-ipv4 and kmod-"
"nf-log-ipv6 (or kmod-nf-log / kmod-nf-log6), then reload the firewall."
@@ -399,7 +403,7 @@ msgstr "诊断功能受限 — 缺少 timeout 命令"
msgid "Live view may be incomplete — a legacy iptables table is registered"
msgstr "实时视图可能不完整 — 已注册旧版 iptables 表"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:330
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:332
msgid "Logging is off on this router"
msgstr "此路由器的日志记录已关闭"
@@ -419,7 +423,7 @@ msgid ""
msgstr ""
"手动模式仍受服务器保护和轮询节奏约束;它改变的是抓取配额,而不是轮询间隔。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:378
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:380
msgid "Manual test (System → Terminal):"
msgstr "手动测试(系统 → 终端):"
@@ -442,13 +446,13 @@ msgstr "更多筛选"
msgid "Network → Firewall"
msgstr "网络 → 防火墙"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:275
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:277
msgid ""
"No WAN firewall zone found in /etc/config/firewall. Configure zones under:"
msgstr ""
"在 /etc/config/firewall 中未找到 WAN 防火墙区域。请在以下位置配置区域:"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:270
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:272
msgid "No WAN zone found"
msgstr "未找到 WAN 区域"
@@ -461,7 +465,7 @@ msgstr "未找到 WAN 区域;没有它无法切换日志记录。"
msgid "Not now"
msgstr "暂不"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:353
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:355
msgid "Nothing changes until you click Enable."
msgstr "点击「启用」之前不会有任何更改。"
@@ -473,11 +477,11 @@ msgstr "出接口"
msgid "One line"
msgstr "单行"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:326
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:328
msgid "Open firewall zone settings"
msgstr "打开防火墙区域设置"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:333
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:335
msgid ""
"OpenWrt does not write firewall events to the log until you turn logging on. "
"Live View only shows what the firewall already logs — it does not add allow/"
@@ -673,11 +677,11 @@ msgstr "防火墙记录流量时表格会自动更新。如果滚动太快,请
msgid "Theme tint fallback"
msgstr "主题着色回退"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:386
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:388
msgid "Then ping the router."
msgstr "然后 ping 路由器。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:304
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:306
msgid ""
"This router reported a logging blocker that Live View does not recognize yet."
msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。"
@@ -702,7 +706,7 @@ msgstr "主要通信方"
msgid "Turn it back off with the WAN logging control on the watch strip."
msgstr "通过监视栏上的 WAN 日志控件将其重新关闭。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:347
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:349
msgid ""
"Turns on WAN zone drop/reject logging (same as Network → Firewall → wan → "
"Log). Rate-limited by the zone log_limit (OpenWrt default 10/minute). Normal "
@@ -732,7 +736,7 @@ msgstr "视图"
msgid "WAN drop/reject logging is off."
msgstr "WAN 丢弃/拒绝日志记录已关闭。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:314
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:316
msgid ""
"WAN drop/reject logging is on. Blocked inbound WAN traffic will show up "
"here. Normal LAN browsing will not."
@@ -761,7 +765,7 @@ msgid "WAN logging on (%s). Click to disable."
msgstr "WAN 日志:开(%s)。点击可禁用。"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:301
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
msgid "WAN logging unavailable"
msgstr "WAN 日志不可用"
@@ -773,7 +777,7 @@ msgstr "WAN 日志不可用:缺少内核日志模块"
msgid "WAN logging unavailable: no WAN zone"
msgstr "WAN 日志不可用:无 WAN 区域"
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:311
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:313
msgid "Waiting for firewall events"
msgstr "正在等待防火墙事件"
@@ -200,7 +200,11 @@ find_wan_zone_section_state() {
_firewall_show=$(uci -q show firewall 2>/dev/null || true)
_zones=$(printf '%s\n' "$_firewall_show" \
| sed -n 's/^firewall\.\([^.=]*\)=zone$/\1/p')
for zone in $_zones; do
# Line-wise here-doc (not an unquoted for-loop): @zone[N] is a glob
# character class and would expand when cwd contains @zoneN. The
# while-read stays in the current shell (here-doc, not a pipeline)
# so WAN_ZONE_* mutations and return reach the caller.
while IFS= read -r zone || [ -n "$zone" ]; do
[ -n "$zone" ] || continue
_name=$(uci -q get "firewall.${zone}.name" 2>/dev/null || true)
if [ -z "$_name" ]; then
@@ -239,7 +243,9 @@ find_wan_zone_section_state() {
WAN_ZONE_FOUND="$zone"
return 0
fi
done
done <<EOF
$_zones
EOF
return 0
}
+174 -11
View File
@@ -105,9 +105,16 @@ uci_rule_names() {
}
is_uci_style_name() {
# Domain: non-empty UCI-style identifier, every character in
# [A-Za-z0-9_-]. That is the map-key set this helper admits
# (names parseRuleHint can look up). nft comments can print
# anything; skip names outside this set so they do not become
# dead map keys. parseRuleHint also allows '.' — not accepted
# here. The charset glob is *[!class]* (whole string);
# [!class]* only tested the first character.
case "$1" in
*' '*|*'"'*|*"'"*) return 1 ;;
[!A-Za-z0-9_-]*) return 1 ;;
''|*' '*|*'"'*|*"'"*) return 1 ;;
*[!A-Za-z0-9_-]*) return 1 ;;
esac
return 0
}
@@ -177,6 +184,8 @@ EOF
# One awk pass: prefix<TAB>comment. Last quoted log prefix / comment on
# the line, including \" units (same capture as the former sed).
# Raw tabs in prefix/comment become the two-character sequence \t so the
# separator tab is unique. Backslashes are not re-encoded.
nft_dump_fields() {
LC_ALL=C awk '
function last_at(s, needle, p, i, found) {
@@ -219,17 +228,60 @@ nft_dump_fields() {
comment = quoted_at($0, "comment \"")
if (prefix == "" && comment == "")
next
gsub(/\\/, "\\\\", prefix)
gsub(/\t/, "\\t", prefix)
gsub(/\\/, "\\\\", comment)
gsub(/\t/, "\\t", comment)
print prefix "\t" comment
}
' || return 1
}
# Restore nft_dump_fields TSV encoding. Walk left to right: \\ is a
# literal backslash, \t is a tab. Other backslash pairs stay as written.
# Do not put backslash sequences in case patterns (they are escapes).
# Fast-path strings with no backslash: dash ${var#?} walks are O(n^2) and
# the map byte-cap tests feed ~60k prefixes with none.
# Result is _tsv_out (no fork).
_nft_tsv_unescape() {
case "$1" in
*\\*) ;;
*)
_tsv_out="$1"
return 0
;;
esac
_tab=$(printf '\t')
_in="$1"
_out=
while [ -n "$_in" ]; do
_head=${_in%"${_in#?}"}
_in=${_in#?}
if [ "$_head" = "\\" ]; then
_n=${_in%"${_in#?}"}
_in=${_in#?}
if [ "$_n" = t ]; then
_out="${_out}${_tab}"
elif [ "$_n" = "\\" ]; then
_out="${_out}\\"
else
_out="${_out}\\${_n}"
fi
else
_out="${_out}${_head}"
fi
done
_tsv_out="$_out"
}
# pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
# an earlier cosmetic for the same prefix under first-wins.
# Reads TSV from nft_dump_fields; stops once the map cap trips.
map_from_nft_stream() {
pass="${1:-all}"
_tab=$(printf '\t')
_esc_t=$(printf '\\t')
_esc_b=$(printf '\\\\')
# IFS= keeps a leading tab (empty prefix + comment-only !fw4: line).
while IFS= read -r line || [ -n "$line" ]; do
[ -n "$_map_overflow" ] && break
@@ -239,6 +291,10 @@ map_from_nft_stream() {
*"$_tab"*) comment="${line#*"$_tab"}" ;;
*) comment="" ;;
esac
_nft_tsv_unescape "$prefix"
prefix="$_tsv_out"
_nft_tsv_unescape "$comment"
comment="$_tsv_out"
label=''
case "$comment" in
@@ -666,14 +722,14 @@ resolve_addresses() {
return 0
fi
# Wall-clock budget clock starts at the TRUE function entry: the budget
# must bound the whole call — including read_rpc_input (stalled stdin
# must not escape the budget) and json parsing. `date +%s` can jump
# under NTP sync — accepted (worst case the budget over- or under-runs
# by the jump; worker starvation is still prevented).
start=$(date +%s)
# Optional JSON on $1 (rpcd may also pass argv $3 at the call site); else stdin.
input=$(read_rpc_input "$1")
# Wall-clock budget starts after the input read and bounds the lookup
# loop only. Stalled stdin is not interrupted — production rpcd closes
# a finite payload. `date +%s` can jump under NTP sync — accepted
# (worst case the budget over- or under-runs by the jump; worker
# starvation is still prevented).
start=$(date +%s)
lookups=0
truncated=0
OUT=''
@@ -719,7 +775,7 @@ resolve_addresses() {
truncated=1
break
fi
# Abort cleanly once the whole-call wall-clock budget is spent;
# Abort cleanly once the lookup-loop wall-clock budget is spent;
# never START a lookup past the budget.
now=$(date +%s)
if [ $((now - start)) -ge "$RESOLVE_BUDGET" ]; then
@@ -793,6 +849,42 @@ run_selftest() {
expected=$(printf 'a\\n\\nb')
check_eq 'json_escape blank line' "$escaped" "$expected" "expected $expected got $escaped" || return 1
# Charset applies to every character, not only the first.
if ! is_uci_style_name 'Rule'; then
echo 'is_uci_style_name: expected accept Rule' >&2
return 1
fi
if ! is_uci_style_name 'Allow-SSH'; then
echo 'is_uci_style_name: expected accept Allow-SSH' >&2
return 1
fi
if ! is_uci_style_name '-lead'; then
echo 'is_uci_style_name: expected accept -lead' >&2
return 1
fi
if is_uci_style_name 'My Rule'; then
echo 'is_uci_style_name: expected reject for space' >&2
return 1
fi
# First character is legal; later $ / ; / . must still fail.
# shellcheck disable=SC2016
if is_uci_style_name 'rule$x'; then
echo 'is_uci_style_name: expected reject rule$x (later-char illegal)' >&2
return 1
fi
if is_uci_style_name 'rule;id'; then
echo 'is_uci_style_name: expected reject rule;id (later-char illegal)' >&2
return 1
fi
if is_uci_style_name 'rule.x'; then
echo 'is_uci_style_name: expected reject rule.x (later-char illegal)' >&2
return 1
fi
if is_uci_style_name ''; then
echo 'is_uci_style_name: expected reject empty' >&2
return 1
fi
if is_resolvable_address 'not an ip'; then
echo 'is_resolvable_address: expected reject for spaces' >&2
return 1
@@ -864,10 +956,17 @@ run_selftest() {
# Escaped-quote capture: a log prefix containing \" must not be truncated
# at the escaped quote (nft dumps strings with backslash escapes).
_tab=$(printf '\t')
_esc_t=$(printf '\\t')
_esc_b=$(printf '\\\\')
got=$(printf '%s\n' 'log prefix "My \" Rule"' | nft_dump_fields)
got="${got%%${_tab}*}"
if [ "$got" != 'My \" Rule' ]; then
echo "escaped-quote prefix capture: expected 'My \\\" Rule' got '$got'" >&2
if [ "$got" != 'My \\" Rule' ]; then
echo "escaped-quote prefix capture: expected encoded 'My \\\\\" Rule' got '$got'" >&2
return 1
fi
_nft_tsv_unescape "$got"
if [ "$_tsv_out" != 'My \" Rule' ]; then
echo "escaped-quote prefix unescape: expected 'My \\\" Rule' got '$_tsv_out'" >&2
return 1
fi
# Plain prefix still captures unchanged.
@@ -884,6 +983,70 @@ run_selftest() {
echo "empty prefix capture: expected '' got '$got'" >&2
return 1
fi
# Guest nft list emits a raw tab inside the quoted prefix
# (log prefix "fwlive<TAB>tabbed"). Encode it in the TSV field.
got=$(printf '%s\n' "log prefix \"fwlive${_tab}tabbed\"" | nft_dump_fields)
case "$got" in
*"${_tab}"*"${_tab}"*)
echo "tabbed prefix TSV: expected one separator tab, got extra" >&2
return 1
;;
*"${_tab}"*) ;;
*)
echo "tabbed prefix TSV: missing separator tab, got '$got'" >&2
return 1
;;
esac
got_prefix="${got%%${_tab}*}"
if [ "$got_prefix" != "fwlive${_esc_t}tabbed" ]; then
echo "tabbed prefix TSV: expected fwlive\\ttabbed got '$got_prefix'" >&2
return 1
fi
_nft_tsv_unescape "$got_prefix"
if [ "$_tsv_out" != "fwlive${_tab}tabbed" ]; then
echo "tabbed prefix unescape: expected fwlive<TAB>tabbed" >&2
return 1
fi
# Literal backslash-t must round-trip and not become a tab.
got=$(printf '%s\n' "log prefix \"fwlive${_esc_t}lit\"" | nft_dump_fields)
got_prefix="${got%%${_tab}*}"
if [ "$got_prefix" != "fwlive${_esc_b}tlit" ]; then
echo "literal \\\\t TSV: expected fwlive\\\\\\\\tlit got '$got_prefix'" >&2
return 1
fi
_nft_tsv_unescape "$got_prefix"
if [ "$_tsv_out" != "fwlive${_esc_t}lit" ]; then
echo "literal \\\\t unescape: expected fwlive\\\\tlit got '$_tsv_out'" >&2
return 1
fi
# Tabbed prefix plus !fw4: label: map key is the full prefix.
got=$(printf '%s\n' "log prefix \"fwlive${_tab}tabbed\" comment \"!fw4: TabRule\"" | nft_dump_fields)
OUT=''
_map_keys=0
_map_overflow=''
map_from_nft_stream all <<EOF
$got
EOF
if [ "$OUT" != '"fwlive'"${_esc_t}"'tabbed":"TabRule"' ]; then
echo "tabbed prefix map: expected full prefix -> TabRule, got $OUT" >&2
return 1
fi
# Comment-only / !fw4: label path is unchanged when there is no tab.
got=$(printf '%s\n' 'comment "!fw4: GoodName"' | nft_dump_fields)
if [ "$got" != "${_tab}!fw4: GoodName" ]; then
echo "comment-only TSV: expected leading tab + !fw4: GoodName got '$got'" >&2
return 1
fi
OUT=''
_map_keys=0
_map_overflow=''
map_from_nft_stream all <<EOF
$got
EOF
if [ "$OUT" != '"GoodName":"GoodName","goodname":"GoodName"' ]; then
echo "comment-only map: expected GoodName mapping, got $OUT" >&2
return 1
fi
got=$(poll_clamp_lines "99999999999999999999")
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "over-long expected $POLL_LINES_MAX got $got" || return 1
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.9.16
PKG_RELEASE:=292
PKG_VERSION:=26.9.26
PKG_RELEASE:=293
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -101,7 +101,7 @@ o = s:option(Value, "tls_serverName", translate("Domain"))
o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped."))
o.default = "0"
o = s:option(Value, "tls_pinSHA256", translate("PinSHA256"),translate("Certificate fingerprint"))
o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)"))
o = s:option(Value, "up_mbps", translate("Max upload Mbps"))
o.rewrite_option = o.option
@@ -140,7 +140,7 @@ function gen_config(var)
tls = {
sni = node.tls_serverName,
insecure = (node.tls_allowInsecure == "1") and true or false,
pinSHA256 = (node.tls_pinSHA256) and node.tls_pinSHA256 or nil,
pinSHA256 = (node.tls_pinSHA256 and node.tls_pinSHA256 ~= "") and api.sha256_sb_xray(node.tls_pinSHA256) or nil,
ech = (node.ech == "1") and node.ech_config or nil
},
quic = {
@@ -11,6 +11,6 @@
2002::/16
3fff::/20
5f00::/16
fc00::/7
fd00::/8
fe80::/64
ff00::/8
@@ -11,3 +11,17 @@ google.com.tw
google.com.hk
gstatic.com
xn--ngstr-lra8j.com
#DNS-Leak
dnsleaktest.com
dnsleak.com
expressvpn.com
nordvpn.com
surfshark.com
ipleak.net
perfect-privacy.com
browserleaks.com
browserleaks.org
vpnunlimited.com
whoer.net
whrq.net
+2 -2
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196
PKG_RELEASE:=76
PKG_RELEASE:=77
PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -32,7 +32,7 @@ LUCI_PKGARCH:=all
LUCI_DEPENDS:= \
+libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
+resolveip +bind-dig +curl +nping +unzip +xz-utils +xz \
+resolveip +curl +nping +unzip +xz-utils +xz \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \
+PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \
+PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \
@@ -5,27 +5,24 @@
local m, s, sec, o
local uci = require "luci.model.uci".cursor()
local URL = require "url"
local sys = require "luci.sys"
local util = require "luci.util"
local datatypes = require "luci.cbi.datatypes"
-- 获取 LAN IP 地址
function lanip()
local lan_ip
-- 尝试从 UCI 直接读取
lan_ip = sys.exec("uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}' | tr -d '\\n'")
lan_ip = luci.sys.exec("uci -q get network.lan.ipaddr 2>/dev/null | awk -F'/' '{print $1}' | tr -d '\\n'")
-- 尝试从 LAN 接口信息中读取(优先 ifname,再 fallback 到 device)
if not lan_ip or lan_ip == "" then
lan_ip = sys.exec([[
lan_ip = luci.sys.exec([[
ip -4 addr show $(uci -q -p /tmp/state get network.lan.device || uci -q -p /tmp/state get network.lan.device) 2>/dev/null \
| grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
end
-- 取任意一个 global IPv4 地址
if not lan_ip or lan_ip == "" then
lan_ip = sys.exec([[
lan_ip = luci.sys.exec([[
ip -4 addr show scope global 2>/dev/null \
| grep -w 'inet' | awk '{print $2}' | cut -d'/' -f1 | grep -v '^127\.' | head -n1 | tr -d '\n']])
end
@@ -34,9 +31,10 @@ ip -4 addr show scope global 2>/dev/null \
end
local lan_ip = lanip()
local validation = require "luci.cbi.datatypes"
local clash_nodes = {}
local function is_finded(e)
return sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
end
local function clash_display_name(s)
@@ -50,28 +48,6 @@ local function clash_display_name(s)
return "[CLASH]"
end
local function is_valid_dns(str)
if datatypes.ip4addrport(str) then
return true
end
local scheme, target = str:match("^([a-zA-Z0-9%+%-%.]+)://(.+)$")
if scheme and target and #target > 0 then
return true
end
local host = str:match("^([^:]+):%d+$") or str
if host:match("[a-zA-Z]")
and not host:match("[^%w%.%-]")
and not host:match("^[%.%-]")
and not host:match("[%.%-]$")
and not host:match("%.%.") then
return true
end
return false
end
m = Map("shadowsocksr", translate("ShadowSocksR Plus+ Settings"), translate("<h3>Support SS/SSR/V2RAY/XRAY/TROJAN/TUIC/HYSTERIA2/NAIVEPROXY/SOCKS5/CLASH etc.</h3>"))
m:section(SimpleSection).template = "shadowsocksr/status"
@@ -155,23 +131,6 @@ o:value("7", translate("Prefer module built-in DNS"))
o:value("0", translate("Use Local DNS Service listen port 5335"))
o.default = 1
o = s:option(Value, "dns2tcp_tunnel_forward", translate("Anti-pollution DNS Server"))
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)"))
o:value("208.67.222.222:53", translate("OpenDNS (208.67.222.222)"))
o:value("208.67.220.220:53", translate("OpenDNS (208.67.220.220)"))
o:value("209.244.0.3:53", translate("Level 3 Public DNS (209.244.0.3)"))
o:value("209.244.0.4:53", translate("Level 3 Public DNS (209.244.0.4)"))
o:value("4.2.2.1:53", translate("Level 3 Public DNS (4.2.2.1)"))
o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("pdnsd_enable", "1")
o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
o.datatype = "ip4addrport"
o.default = "8.8.4.4:53"
o = s:option(Value, "tunnel_forward", translate("Anti-pollution DNS Server"))
o:value("8.8.4.4:53", translate("Google Public DNS (8.8.4.4)"))
o:value("8.8.8.8:53", translate("Google Public DNS (8.8.8.8)"))
@@ -184,38 +143,11 @@ o:value("4.2.2.2:53", translate("Level 3 Public DNS (4.2.2.2)"))
o:value("4.2.2.3:53", translate("Level 3 Public DNS (4.2.2.3)"))
o:value("4.2.2.4:53", translate("Level 3 Public DNS (4.2.2.4)"))
o:value("1.1.1.1:53", translate("Cloudflare DNS (1.1.1.1)"))
o:depends("pdnsd_enable", "1")
o:depends("pdnsd_enable", "7")
o.datatype = nil
o.description =
"<ul>" ..
"<li>" .. translate("Custom DNS Server (supports format IP:PORT, domain name, or scheme://...).") .. "</li>" ..
"<li>" .. translate("Muitiple DNS server can saperate with ','") .. "</li>" ..
"<li>" .. translate("Note: IP addresses MUST specify a port.") .. "</li>" ..
"</ul>"
o.description = translate("Custom DNS Server format as IP:PORT (default: 8.8.4.4:53)")
o.datatype = "ip4addrport"
o.default = "8.8.4.4:53"
o.validate = function(self, value, section)
if section and value and value ~= "" then
local parts = {}
for part in (value .. ","):gmatch("(.-),") do
part = part:gsub("^%s*", ""):gsub("%s*$", "")
if part ~= "" then
if not is_valid_dns(part) then
return nil, translate("Expecting: %s"):format(translate("IP:PORT, Domain, or Protocol URL (e.g. 8.8.8.8:53, dns.google, https://...).") ..
"<br>".. translate("Note: Pure IP without port is not allowed."))
end
table.insert(parts, part)
end
end
if #parts > 0 then
return table.concat(parts, ",")
end
return nil, translate("Expecting: %s"):format(translate("valid address:port")) -- 有效的地址:端口
end
return value
end
o = s:option(Value, "tunnel_forward_mosdns", translate("Anti-pollution DNS Server"))
o:value("tcp://8.8.4.4:53,tcp://8.8.8.8:53", translate("Google Public DNS"))
@@ -293,7 +225,7 @@ if is_finded("chinadns-ng") then
local parts = {}
for part in string.gmatch(value, "[^,]+") do
part = part:gsub("^%s*", ""):gsub("%s*$", "")
if not datatypes.ip4addrport(part) then
if not validation.ip4addrport(part) then
return nil, translate("Expecting: %s"):format(translate("valid address:port (comma separated)"))
end
table.insert(parts, part)
@@ -304,7 +236,7 @@ if is_finded("chinadns-ng") then
return table.concat(parts, ",")
end
if datatypes.ip4addrport(value) then
if validation.ip4addrport(value) then
return value
end
@@ -1,11 +1,8 @@
require "luci.ip"
require "nixio.fs"
require "luci.sys"
local ucic = luci.model.uci.cursor()
local m, s, o
local enable_mihomo = ucic:get_first('shadowsocksr', 'server_subscribe', 'enable_mihomo') or ''
m = Map("shadowsocksr")
s = m:section(TypedSection, "access_control")
@@ -119,7 +116,7 @@ o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(denydomainconf) or ""
return nixio.fs.readfile(denydomainconf) or " "
end
o.write = function(self, section, value)
nixio.fs.writefile(denydomainconf, value:gsub("\r\n", "\n"))
@@ -128,54 +125,6 @@ o.remove = function(self, section, value)
nixio.fs.writefile(denydomainconf, "")
end
if enable_mihomo == "1" then
s:tab("fake_ip_filter", translate("Fake-IP Filter and Fallback Lists"))
local fake_ip_filter_conf = "/etc/ssrplus/fake_ip_filter.list"
o = s:taboption("fake_ip_filter", TextValue, "fake_ip_filter_conf", "", "<font style=color:red>" .. translate("Specifically for editing the Fake-IP filter list.") .. "</font>") -- 专用于编辑 Fake-IP 过滤规则文件。
o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(fake_ip_filter_conf) or ""
end
o.write = function(self, section, value)
nixio.fs.writefile(fake_ip_filter_conf, value:gsub("\r\n", "\n"))
end
o.remove = function(self, section, value)
nixio.fs.writefile(fake_ip_filter_conf, "")
end
local fallback_ipcidr_conf = "/etc/ssrplus/fallback_ipcidr.list"
o = s:taboption("fake_ip_filter", TextValue, "fallback_ipcidr_conf", "", "<font style=color:red>" .. translate("Specifically for editing the Fallback IP-CIDR list.") .. "</font>") -- 专用于编辑 Fallback IP-CIDR 规则文件。
o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(fallback_ipcidr_conf) or ""
end
o.write = function(self, section, value)
nixio.fs.writefile(fallback_ipcidr_conf, value:gsub("\r\n", "\n"))
end
o.remove = function(self, section, value)
nixio.fs.writefile(fallback_ipcidr_conf, "")
end
local fallback_domain_conf = "/etc/ssrplus/fallback_domain.list"
o = s:taboption("fake_ip_filter", TextValue, "fallback_domain_conf", "", "<font style=color:red>" .. translate("Specifically for editing the Fallback domain filter list.") .. "</font>") -- 专用于编辑 Fallback 域名过滤规则文件。
o.rows = 13
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
return nixio.fs.readfile(fallback_domain_conf) or ""
end
o.write = function(self, section, value)
nixio.fs.writefile(fallback_domain_conf, value:gsub("\r\n", "\n"))
end
o.remove = function(self, section, value)
nixio.fs.writefile(fallback_domain_conf, "")
end
end
if luci.sys.call('[ -f "/www/luci-static/resources/uci.js" ]') == 0 then
m.apply_on_parse = true
function m.on_apply(self)
@@ -188,10 +188,9 @@ local function save_uploaded_clash_node(upload_name, final_path)
local alias
if not sid then
local sid_output = luci.sys.exec("uci add shadowsocksr servers 2>/dev/null")
sid = trim(sid_output)
sid = uci:add("shadowsocksr", "servers")
end
if not sid or sid == "" then
if not sid then
return nil
end
@@ -201,50 +200,20 @@ local function save_uploaded_clash_node(upload_name, final_path)
alias = upload_alias(upload_name)
end
local switch_enable = uci:get("shadowsocksr", sid, "switch_enable")
if not switch_enable or switch_enable == "" then
switch_enable = uci:get_first("shadowsocksr", "server_subscribe", "switch", "1") or "1"
end
local clash_user_agent = uci:get("shadowsocksr", sid, "clash_user_agent")
if not clash_user_agent or clash_user_agent == "" then
clash_user_agent = "clash"
end
local safe_sid = luci.util.shellquote(sid)
local safe_alias = luci.util.shellquote(alias)
local safe_path = luci.util.shellquote(final_path)
local safe_upload_name = luci.util.shellquote(upload_name)
local safe_switch_enable = luci.util.shellquote(switch_enable)
local safe_user_agent = luci.util.shellquote(clash_user_agent)
local cmd = string.format(
"uci set shadowsocksr.%s.type=clash && " ..
"uci set shadowsocksr.%s.alias=%s && " ..
"uci set shadowsocksr.%s.server=127.0.0.1 && " ..
"uci set shadowsocksr.%s.server_port=0 && " ..
"uci delete shadowsocksr.%s.clash_url 2>/dev/null; " ..
"uci set shadowsocksr.%s.clash_path=%s && " ..
"uci set shadowsocksr.%s.clash_user_agent=%s && " ..
"uci set shadowsocksr.%s.switch_enable=%s && " ..
"uci set shadowsocksr.%s.yaml_upload=1 && " ..
"uci set shadowsocksr.%s.yaml_upload_name=%s && " ..
"uci commit shadowsocksr",
safe_sid,
safe_sid, safe_alias,
safe_sid,
safe_sid,
safe_sid,
safe_sid, safe_path,
safe_sid, safe_user_agent,
safe_sid, safe_switch_enable,
safe_sid,
safe_sid, safe_upload_name
)
if luci.sys.call(cmd) ~= 0 then
return nil
uci:set("shadowsocksr", sid, "type", "clash")
uci:set("shadowsocksr", sid, "alias", alias)
uci:set("shadowsocksr", sid, "server", "127.0.0.1")
uci:set("shadowsocksr", sid, "server_port", "0")
uci:delete("shadowsocksr", sid, "clash_url")
uci:set("shadowsocksr", sid, "clash_path", final_path)
uci:set("shadowsocksr", sid, "clash_user_agent", uci:get("shadowsocksr", sid, "clash_user_agent") or "clash")
if not uci:get("shadowsocksr", sid, "switch_enable") then
uci:set("shadowsocksr", sid, "switch_enable", uci:get_first("shadowsocksr", "server_subscribe", "switch", "1") or "1")
end
uci:set("shadowsocksr", sid, "yaml_upload", "1")
uci:set("shadowsocksr", sid, "yaml_upload_name", upload_name)
uci:save("shadowsocksr")
uci:commit("shadowsocksr")
cleanup_old_clash_path(old_path, final_path, sid)
luci.sys.call(string.format("/etc/init.d/shadowsocksr clash_cache %s >/dev/null 2>&1 &", luci.util.shellquote(sid)))
@@ -298,48 +267,32 @@ if has_mihomo then
local tmp_output = string.format("%s/.upload-%d-%d.yaml", CLASH_YAML_DIR, nixio.getpid(), os.time())
local sid
local alias
local redirect_message_url
nixio.fs.mkdirr(CLASH_YAML_DIR)
nixio.fs.remove(tmp_output)
if preprocess_clash_yaml(upload_tmp_path, tmp_output) then
hash = hash_file(tmp_output)
if not hash or hash == "" then
if hash == "" then
nixio.fs.remove(tmp_output)
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
else
final_path = string.format("%s/%s.yaml", CLASH_YAML_DIR, hash)
local mv_ret = luci.sys.call(string.format("mv -f %s %s", luci.util.shellquote(tmp_output), luci.util.shellquote(final_path)))
if mv_ret == 0 and nixio.fs.access(final_path) then
sid, alias = save_uploaded_clash_node(upload_filename, final_path)
if sid then
local message = string.format(translate("Custom YAML imported successfully: %s"), alias or sid)
local redirect_url = luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers")
redirect_message_url = redirect_url .. "?upload_message=" .. luci.http.urlencode(message)
else
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
if final_path and nixio.fs.access(final_path) then
nixio.fs.remove(final_path)
end
end
luci.sys.call(string.format("mv -f %s %s", luci.util.shellquote(tmp_output), luci.util.shellquote(final_path)))
sid, alias = save_uploaded_clash_node(upload_filename, final_path)
if sid then
upload_message = string.format(translate("Custom YAML imported successfully: %s"), alias or sid)
else
upload_errmessage = translate("Failed to move uploaded YAML to target directory.")
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
end
end
else
upload_errmessage = upload_errmessage or translate("Uploaded YAML validation or preprocessing failed.")
end
if upload_tmp_path and nixio.fs.access(upload_tmp_path) then
nixio.fs.remove(upload_tmp_path)
end
if tmp_output and nixio.fs.access(tmp_output) then
nixio.fs.remove(tmp_output)
upload_errmessage = translate("Uploaded YAML validation or preprocessing failed.")
end
end
if redirect_message_url then
luci.http.redirect(redirect_message_url)
return
end
if upload_tmp_path then
nixio.fs.remove(upload_tmp_path)
end
end
end
@@ -454,11 +407,8 @@ end
m = Map("shadowsocksr", translate("Servers subscription and manage"))
if upload_errmessage then
m.errmessage = upload_errmessage
else
local message = luci.http.formvalue("upload_message")
if message and message ~= "" then
m.message = message
end
elseif upload_message then
m.message = upload_message
end
local style_section = m:section(SimpleSection)
@@ -616,29 +566,6 @@ if has_mihomo then
o.rmempty = true
o.description = translate("Disabled means use Xray or ShadowSocks-Rust core.")
o = s:option(Flag, "enable_fake_ip", string.format("<b><span style='color:red;'>%s</span></b>", translate("Enable Fake-IP Mode")))
o.default = "0"
o.rmempty = true
o:depends("enable_mihomo", "1")
o.description = translate(
"<ul>" ..
"<li>" .. translate("Enable Fake-IP mode; disable to use redir-host mode.") .. "</li>" ..
"<li>" .. translate("Enable Fake-IP mode for better performance.") .. "</li>" ..
"<li>" .. translate("If disabled, Redir-Host mode will be used which returns real IP addresses.") .. "</li>" ..
"<li>" .. translate("Tips: Fake-IP Filter and Fallback Lists:") .. " " ..
"<span id='fake_ip_filter_control_link'></span>" ..
"<script type='text/javascript'>" ..
"(function() {" ..
"var url = '" .. luci.dispatcher.build_url("admin", "services", "shadowsocksr", "control") .. "?tab=fake_ip_filter#fake_ip_filter';" ..
"var linkText = '<font style=\"color:green\"><b>" .. translate("Click here to manage") .. "</b></font>';" ..
"var elem = document.getElementById('fake_ip_filter_control_link');" ..
"if (elem) elem.innerHTML = '<a href=\"' + url + '\">' + linkText + '</a>';" ..
"})();" ..
"</script>" ..
"</li>" ..
"</ul>"
)
o = s:option(Flag, "sub_convert", translate("Subscribe Convert Online"))
o.description = translate("Convert subscriptions to Clash/Mihomo YAML with a subscription template URL.")
o.default = "0"
@@ -1,4 +1,4 @@
<%+cbi/valueheader%>
<input class="cbi-input-file" style="width: 400px" type="file" id="clash_yaml_file" name="clash_yaml_file" accept=".yaml,.yml" />
<input type="submit" class="bt cbi-button cbi-input-apply" name="upload_clash_yaml" value="<%:Upload%>" />
<input type="submit" class="cbi-button cbi-input-apply" name="upload_clash_yaml" value="<%:Upload%>" />
<%+cbi/valuefooter%>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+36 -78
View File
@@ -608,10 +608,6 @@ prepare_clash_runtime_config() {
local dns_mode
local client_policy_stats
local client_rules
local enable_fake_ip
local socks5_auth
local socks5_user
local socks5_pass
cache_file="$(get_clash_cache_file "$sid")"
workdir="$(get_clash_workdir "$sid")"
@@ -627,7 +623,6 @@ prepare_clash_runtime_config() {
dns_mode="$(uci_get_by_type global pdnsd_enable 0)"
client_policy_stats=""
client_rules=0
enable_fake_ip="$(uci_get_by_type server_subscribe enable_fake_ip "")"
[ -s "$cache_file" ] || return 1
@@ -645,35 +640,15 @@ prepare_clash_runtime_config() {
routing-mark: $CLASH_OUTBOUND_MARK
profile:
store-selected: true
store-fake-ip: true
tun:
enable: false
dns:
enable: $( [ "$dns_mode" = "7" ] && echo "true" || echo "false" )
enhanced-mode: redir-host
listen: 127.0.0.1:$dns_port
ipv6: $( [ "$dns_ipv4_only" = "1" ] && echo "false" || echo "true" )
EOF
# 根据 enable_fake_ip 决定 enhanced-mode
if [ "$enable_fake_ip" = "1" ]; then
ENHANCED_MODE="fake-ip"
else
ENHANCED_MODE="redir-host"
fi
# 根据 dns_mode 添加不同的 dns 配置
if [ "$dns_mode" = "7" ]; then
cat >> "$overlay_file" <<-EOF
dns:
enable: true
enhanced-mode: $ENHANCED_MODE
listen: 127.0.0.1:$dns_port
ipv6: $( [ "$dns_ipv4_only" = "1" ] && echo "false" || echo "true" )
EOF
else
cat >> "$overlay_file" <<-EOF
dns:
enable: true
enhanced-mode: $ENHANCED_MODE
ipv6: $( [ "$dns_ipv4_only" = "1" ] && echo "false" || echo "true" )
EOF
fi
if [ -n "$socks_port" ] && [ "$socks_port" != "0" ]; then
echo "socks-port: $socks_port" >>"$overlay_file"
@@ -1273,24 +1248,14 @@ start_dns() {
ssrplus_dns="$(uci_get_by_type global pdnsd_enable 0)"
global_server_type="$(uci_get_by_name "$GLOBAL_SERVER" type)"
builtin_dns_enabled=0
dnsserver="$(uci_get_by_type global tunnel_forward 8.8.4.4:53)"
if [ "$ssrplus_dns" = "5" ]; then
ssrplus_dns="$(force_dns2tcp_fallback "旧版 DNSPROXY 模式已移除")" || return 1
uci_set_by_type global pdnsd_enable "$ssrplus_dns"
elif [ "$ssrplus_dns" = "6" ]; then
dnsserver="$(uci_get_by_type global chinadns_ng_tunnel_forward 8.8.4.4:53)"
fi
case "$ssrplus_dns" in
1)
dnsserver="$(uci_get_by_type global dns2tcp_tunnel_forward 8.8.4.4:53)"
;;
6)
dnsserver="$(uci_get_by_type global chinadns_ng_tunnel_forward 8.8.4.4:53)"
;;
7)
dnsserver="$(uci_get_by_type global tunnel_forward 8.8.4.4:53)"
;;
*)
;;
esac
run_mode="$(normalize_run_mode)"
if [ "$ssrplus_dns" = "4" ] && ! is_finded "mosdns"; then
@@ -1556,15 +1521,15 @@ generate_xray_config() {
{
"log": {
"loglevel": "warning"
},
"inbounds": [$inbound_json],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
}
]
}
},
"inbounds": [$inbound_json],
"outbounds": [
{
"protocol": "freedom",
"settings": {}
}
]
}
EOF
return 0
}
@@ -2940,18 +2905,12 @@ start_rules() {
local ac_ips
local custom_ports
local proxyport
local enable_fake_ip
server=$(get_host_ip "$GLOBAL_SERVER")
local_port=$(get_default_node_local_port)
lan_ac_ips=$(uci_get_by_type access_control lan_ac_ips)
lan_ac_mode=$(uci_get_by_type access_control lan_ac_mode)
enable_fake_ip=$(uci_get_by_type server_subscribe enable_fake_ip "")
if [ -z "$enable_fake_ip" ]; then
enable_fake_ip="0"
fi
if [ "$kcp_enable_flag" = "0" ] && [ "$redir_udp" = "1" ]; then
udp_server=$(get_host_ip "$UDP_RELAY_SERVER")
udp_local_port=$tmp_udp_port
@@ -2996,27 +2955,26 @@ start_rules() {
fi
elif [ "$USE_TABLES" = "iptables" ]; then
ARG_A=""
fi
/usr/share/shadowsocksr/gfw2ipset.sh
/usr/bin/ssr-rules \
-s "$server" \
-l "$local_port" \
-S "$udp_server" \
-L "$udp_local_port" \
-a "$ac_ips" \
-i "/etc/ssrplus/china_ssr.txt" \
-b "$(uci_get_by_type access_control wan_bp_ips)" \
-w "$(uci_get_by_type access_control wan_fw_ips)" \
-B "$(uci_get_by_type access_control lan_bp_ips)" \
-p "$(uci_get_by_type access_control lan_fp_ips)" \
-G "$(uci_get_by_type access_control lan_gm_ips)" \
-m "$(uci_get_by_type access_control Interface)" \
-D "$proxyport" \
-E "$enable_fake_ip" \
"$(get_arg_out)" "$(gfwmode)" "$ARG_UDP" "$ARG_UDP_RULES" "$ARG_A"
fi
/usr/share/shadowsocksr/gfw2ipset.sh
/usr/bin/ssr-rules \
-s "$server" \
-l "$local_port" \
-S "$udp_server" \
-L "$udp_local_port" \
-a "$ac_ips" \
-i "/etc/ssrplus/china_ssr.txt" \
-b "$(uci_get_by_type access_control wan_bp_ips)" \
-w "$(uci_get_by_type access_control wan_fw_ips)" \
-B "$(uci_get_by_type access_control lan_bp_ips)" \
-p "$(uci_get_by_type access_control lan_fp_ips)" \
-G "$(uci_get_by_type access_control lan_gm_ips)" \
-m "$(uci_get_by_type access_control Interface)" \
-D "$proxyport" \
"$(get_arg_out)" "$(gfwmode)" "$ARG_UDP" "$ARG_UDP_RULES" "$ARG_A"
return $?
}
return $?
}
start() {
local run_mode
@@ -1,78 +0,0 @@
*.lan
*.localdomain
*.example
*.invalid
*.localhost
*.test
*.local
*.home.arpa
time.*.com
time.*.gov
time.*.edu.cn
time.*.apple.com
time1.*.com
time2.*.com
time3.*.com
time4.*.com
time5.*.com
time6.*.com
time7.*.com
ntp.*.com
ntp1.*.com
ntp2.*.com
ntp3.*.com
ntp4.*.com
ntp5.*.com
ntp6.*.com
ntp7.*.com
*.time.edu.cn
*.ntp.org.cn
+.pool.ntp.org
time1.cloud.tencent.com
stun.*.*
stun.*.*.*
swscan.apple.com
mesu.apple.com
music.163.com
*.music.163.com
*.126.net
musicapi.taihe.com
music.taihe.com
songsearch.kugou.com
trackercdn.kugou.com
*.kuwo.cn
api-jooxtt.sanook.com
api.joox.com
y.qq.com
*.y.qq.com
streamoc.music.tc.qq.com
mobileoc.music.tc.qq.com
isure.stream.qqmusic.qq.com
dl.stream.qqmusic.qq.com
aqqmusic.tc.qq.com
amobile.music.tc.qq.com
localhost.ptlogin2.qq.com
*.msftconnecttest.com
*.msftncsi.com
*.xiami.com
*.music.migu.cn
music.migu.cn
+.wotgame.cn
+.wggames.cn
+.wowsgame.cn
+.wargaming.net
*.*.*.srv.nintendo.net
*.*.stun.playstation.net
xbox.*.*.microsoft.com
*.*.xboxlive.com
*.ipv6.microsoft.com
teredo.*.*.*
teredo.*.*
speedtest.cros.wr.pvp.net
+.jjvip8.com
www.douyu.com
activityapi.huya.com
activityapi.huya.com.w.cdngslb.com
www.bilibili.com
api.bilibili.com
a.w.bilicdn1.com
@@ -1,9 +0,0 @@
+.google.com
+.facebook.com
+.youtube.com
+.twitter.com
+.telegram.org
+.githubusercontent.com
+.googlevideo.com
+.msftconnecttest.com
+.msftncsi.com
@@ -1,15 +0,0 @@
0.0.0.0/8
10.0.0.0/8
100.64.0.0/10
127.0.0.0/8
169.254.0.0/16
172.16.0.0/12
192.0.0.0/24
192.0.2.0/24
192.88.99.0/24
192.168.0.0/16
198.51.100.0/24
203.0.113.0/24
224.0.0.0/4
240.0.0.0/4
255.255.255.255/32
@@ -47,18 +47,6 @@ if [ -s "/etc/config/shadowsocksr" ]; then
uci -q set shadowsocksr.@server_subscribe[0].auto_update_min_time='0'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].enable_mihomo > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].enable_mihomo='1'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].enable_fake_ip > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].enable_fake_ip='1'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].sub_convert > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].sub_convert='1'
fi
if ! uci -q get shadowsocksr.@server_subscribe[0].config_auto_update_mode > /dev/null; then
uci -q set shadowsocksr.@server_subscribe[0].config_auto_update_mode='0'
fi
+77 -280
View File
@@ -46,7 +46,6 @@ CHECK_STATUS=0
RESTORE_RULES=0
FLUSH_RULES=0
CLEANUP_PERSISTENCE=0
ENABLE_FAKE_IP=0
# ASCII code for SSRP.Use whatever,just not the same.
FWMARK="0x53535250"
@@ -84,15 +83,14 @@ usage() {
-m <Interface> Interface name
-I <ip_list_file> a file content is bypassed shunt ip list
-e <extra_options> extra options for iptables
-E <0|1> enable_fake_ip status
-o apply the rules to the OUTPUT chain
-O apply the global rules to the OUTPUT chain
-u enable udprelay mode, TPROXY is required
-U enable udprelay mode, using different IP
and ports for TCP and UDP
-y disable all auxiliary UDP rules when UDP
transparent proxy is unavailable
-f flush the rules
-u enable udprelay mode, TPROXY is required
-U enable udprelay mode, using different IP
and ports for TCP and UDP
-y disable all auxiliary UDP rules when UDP
transparent proxy is unavailable
-f flush the rules
-g gfwlist mode
-r router mode
-z all mode
@@ -234,12 +232,6 @@ cleanup_persistence_files() {
loger 5 "Removed xhttp hash file: /tmp/.last_xhttp_hash"
fi
# Remove Fake_IP Mode state file
if [ -f "$ENABLE_FAKE_IP_STATE_FILE" ]; then
rm -f "$ENABLE_FAKE_IP_STATE_FILE" 2>/dev/null
loger 5 "Removed Fake_IP Mode state file: $ENABLE_FAKE_IP_STATE_FILE"
fi
loger 5 "Persistence cleanup completed"
return 0
}
@@ -323,64 +315,25 @@ flush_nftables() {
flush_iptables_legacy() {
flush_iptables() {
local table=$1
local ipt="iptables -t $table"
local DAT
DAT=$(iptables-save -t $table 2>/dev/null)
[ -z "$DAT" ] && return 0
# 1. 解除所有引用 SS_SPEC* 链的规则
echo "$DAT" | grep -E "^-A .*SS_SPEC" | while read -r line; do
local chain rule
chain=$(echo "$line" | awk '{print $2}')
rule=$(echo "$line" | sed 's/^-A [^ ]* //')
[ -n "$chain" ] && [ -n "$rule" ] && \
eval "$ipt -D \"$chain\" $rule 2>/dev/null"
local ipt="iptables -t $1"
local DAT=$(iptables-save -t $1)
eval $(echo "$DAT" | grep "$TAG" | sed -e 's/^-A/$ipt -D/' -e 's/$/;/')
for chain in $(echo "$DAT" | awk '/^:SS_SPEC/{print $1}'); do
$ipt -F ${chain:1} 2>/dev/null && $ipt -X ${chain:1}
done
# 2. 删除带 TAG 或 mark 的残留规则(同样加上 eval)
echo "$DAT" | grep -E "^-A " | grep -E "$TAG|-m mark|--mark" | while read -r line; do
local chain rule
chain=$(echo "$line" | awk '{print $2}')
rule=$(echo "$line" | sed 's/^-A [^ ]* //')
[ -n "$chain" ] && [ -n "$rule" ] && \
eval "$ipt -D \"$chain\" $rule 2>/dev/null"
done
# 3. 提取所有以 SS_SPEC 开头的自定义链
local chains
chains=$(echo "$DAT" | awk '/^:SS_SPEC/{sub(/^:/,"",$1); print $1}')
# 4. 清空并彻底删除 SS_SPEC* 自定义链
for chain in $chains; do
$ipt -F "$chain" 2>/dev/null
$ipt -X "$chain" 2>/dev/null
done
return 0
}
# 依次对 nat 和 mangle 表执行清理
flush_iptables nat
flush_iptables mangle
# 清理策略路由与路由表
if ip rule show | grep -Eiq "fwmark ${FWMARK}.*lookup 999"; then
if ip rule show | grep -Eq "fwmark ${FWMARK}.*lookup 999"; then
ip rule del fwmark ${FWMARK} table 999 2>/dev/null
fi
if ip route show table 999 | grep -Eq "^local.*dev lo"; then
ip route del local 0.0.0.0/0 dev lo table 999 2>/dev/null
fi
# 清理 IPSET 集合(先 -F 清空,后 -X 销毁)
for setname in ss_spec_lan_ac ss_spec_wan_ac ssr_gen_router \
china fplan bplan gmlan whitelist blacklist gfwlist music; do
ipset -F $setname 2>/dev/null
ipset -X $setname 2>/dev/null
done
[ -n "$FWI" ] && echo '#!/bin/sh' >$FWI
return 0
}
@@ -571,11 +524,6 @@ fw_rule() {
}
fw_rule_nft() {
# 优先将 Fake-IP (198.18.0.0/16) 的 TCP 流量重定向至代理端口
if [ "$ENABLE_FAKE_IP" = "1" ]; then
$NFT add rule inet ss_spec ss_spec_wan_fw ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
fi
# redirect/translation: when PROXY_PORTS present, redirect those tcp ports to local_port
if [ -n "$PROXY_PORTS" ]; then
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
@@ -611,10 +559,6 @@ fw_rule_iptables() {
$IPT -A SS_SPEC_WAN_FW -d "$net" -j RETURN
done
if [ "$ENABLE_FAKE_IP" = "1" ]; then
$IPT -A SS_SPEC_WAN_FW -d 198.18.0.0/16 -p tcp -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
fi
$IPT -A SS_SPEC_WAN_FW -p tcp $PROXY_PORTS -j REDIRECT --to-ports "$local_port" 2>/dev/null || {
loger 3 "Can't redirect TCP, please check the iptables."
exit 1
@@ -667,33 +611,6 @@ ac_rule_nft() {
fi
$NFT flush chain inet ss_spec ss_spec_prerouting 2>/dev/null
# Fake-IP 重定向处理 (引入 Interface 匹配,防止 WAN 口越权)
if [ "$ENABLE_FAKE_IP" = "1" ]; then
if [ -z "$Interface" ]; then
if [ -n "$MATCH_SET" ]; then
$NFT add rule inet ss_spec ss_spec_prerouting ip daddr 198.18.0.0/16 $MATCH_SET meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
$NFT add rule inet ss_spec ss_spec_prerouting ip daddr 198.18.0.0/16 $MATCH_SET meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
else
$NFT add rule inet ss_spec ss_spec_prerouting ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
$NFT add rule inet ss_spec ss_spec_prerouting ip daddr 198.18.0.0/16 meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
fi
else
for name in $Interface; do
local IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
if [ -n "$IFNAME" ]; then
if [ -n "$MATCH_SET" ]; then
$NFT add rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" ip daddr 198.18.0.0/16 $MATCH_SET meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
$NFT add rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" ip daddr 198.18.0.0/16 $MATCH_SET meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
else
$NFT add rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
$NFT add rule inet ss_spec ss_spec_prerouting meta iifname "$IFNAME" ip daddr 198.18.0.0/16 meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
fi
fi
done
fi
fi
# Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_prerouting >/dev/null 2>&1; then
for net in 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4; do
@@ -708,6 +625,16 @@ ac_rule_nft() {
# done
#fi
# Build a rule in the prerouting hook chain that jumps to business chain with conditions
if [ -n "$PROXY_PORTS" ]; then
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
if [ -n "$PORTS_ARGS" ]; then
TCP_EXT_ARGS="meta l4proto tcp tcp dport { $PORTS_ARGS }"
fi
else
TCP_EXT_ARGS="meta l4proto tcp"
fi
# Block UDP port 443 when TPROXY not Enable
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then
# Add UDP 443 block rule
@@ -731,17 +658,6 @@ ac_rule_nft() {
done
fi
fi
# Build a rule in the prerouting hook chain that jumps to business chain with conditions
if [ -n "$PROXY_PORTS" ]; then
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
if [ -n "$PORTS_ARGS" ]; then
TCP_EXT_ARGS="meta l4proto tcp tcp dport { $PORTS_ARGS }"
fi
else
TCP_EXT_ARGS="meta l4proto tcp"
fi
if [ -z "$Interface" ]; then
# generic prerouting jump already exists (see ipset_nft), but if we have MATCH_SET_CONDITION we add a more specific rule
if [ -n "$MATCH_SET" ]; then
@@ -767,16 +683,11 @@ ac_rule_nft() {
case "$OUTPUT" in
1)
# Create ss_spec_output tcp chain
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
if [ "$ENABLE_FAKE_IP" = "1" ]; then
$NFT add rule inet ss_spec ss_spec_output ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output ip daddr 198.18.0.0/16 meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
fi
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
# Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
@@ -797,16 +708,11 @@ ac_rule_nft() {
;;
2)
# Create ss_spec_output tcp chain
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
if [ "$ENABLE_FAKE_IP" = "1" ]; then
$NFT add rule inet ss_spec ss_spec_output ip daddr 198.18.0.0/16 meta l4proto tcp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output ip daddr 198.18.0.0/16 meta l4proto udp redirect to :$local_port comment "\"$TAG\"" 2>/dev/null
fi
if ! $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
$NFT add chain inet ss_spec ss_spec_output '{ type nat hook output priority 0; policy accept; }'
fi
$NFT flush chain inet ss_spec ss_spec_output 2>/dev/null
$NFT add rule inet ss_spec ss_spec_output meta mark 255 return 2>/dev/null
# Exclude special local addresses
if $NFT list chain inet ss_spec ss_spec_output >/dev/null 2>&1; then
@@ -848,38 +754,11 @@ ac_rule_iptables() {
return 2
;;
esac
local ip_list="${LAN_AC_IP#?}"
ipset -! create ss_spec_lan_ac hash:net 2>/dev/null
ipset flush ss_spec_lan_ac 2>/dev/null
if [ -n "$ip_list" ]; then
for ip in $ip_list; do
ipset add ss_spec_lan_ac "$ip" 2>/dev/null
done
fi
fi
# 2. PREROUTING 链处理
if [ -z "$Interface" ]; then
# Global rules
if [ -n "$MATCH_SET" ]; then
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
else
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
fi
else
for name in $Interface; do
local IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
if [ -n "$IFNAME" ]; then
if [ -n "$MATCH_SET" ]; then
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
else
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
fi
fi
done
fi
ipset -! -R <<-EOF || return 1
create ss_spec_lan_ac hash:net
$(for ip in ${LAN_AC_IP#?}; do echo "add ss_spec_lan_ac $ip"; done)
EOF
# Block UDP port 443 when TPROXY not Enable
if [ -z "$TPROXY" ] && [ "$DISABLE_UDP_RULES" != "1" ]; then
@@ -905,67 +784,42 @@ ac_rule_iptables() {
done
fi
fi
# 拦截 Fake-IP 流量 (198.18.0.0/16),受 Interface 和 LAN_AC 控制
if [ "$ENABLE_FAKE_IP" = "1" ]; then
if [ -z "$Interface" ]; then
if [ -n "$MATCH_SET" ]; then
$IPT -I PREROUTING 1 -p tcp -d 198.18.0.0/16 $MATCH_SET -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
$IPT -I PREROUTING 2 -p udp -d 198.18.0.0/16 $MATCH_SET -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
else
$IPT -I PREROUTING 1 -p tcp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
$IPT -I PREROUTING 2 -p udp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
fi
if [ -z "$Interface" ]; then
# Global rules
if [ -n "$MATCH_SET" ]; then
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
else
for name in $Interface; do
local IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
if [ -n "$IFNAME" ]; then
if [ -n "$MATCH_SET" ]; then
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp -d 198.18.0.0/16 $MATCH_SET -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
$IPT -I PREROUTING 2 ${IFNAME:+-i $IFNAME} -p udp -d 198.18.0.0/16 $MATCH_SET -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
else
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
$IPT -I PREROUTING 2 ${IFNAME:+-i $IFNAME} -p udp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
fi
fi
done
$IPT -I PREROUTING 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
fi
else
for name in $Interface; do
local IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
[ -z "$IFNAME" ] && IFNAME=$(uci -P /var/state get network."$name".device 2>/dev/null)
if [ -n "$IFNAME" ]; then
if [ -n "$MATCH_SET" ]; then
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS $MATCH_SET -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
else
$IPT -I PREROUTING 1 ${IFNAME:+-i $IFNAME} -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
fi
fi
done
fi
case "$OUTPUT" in
1)
# fake-ip first
if [ "$ENABLE_FAKE_IP" = "1" ]; then
$IPT -I OUTPUT 1 -m mark --mark 255 -j RETURN 2>/dev/null
$IPT -I OUTPUT 2 -p tcp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
$IPT -I OUTPUT 3 -p udp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
$IPT -I OUTPUT 4 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
else
$IPT -I OUTPUT 1 -m mark --mark 255 -j RETURN 2>/dev/null
$IPT -I OUTPUT 2 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
fi
$IPT -I OUTPUT 1 -p tcp $EXT_ARGS -m comment --comment "$TAG" -j SS_SPEC_WAN_AC
;;
2)
ipset -! -R <<-EOF || return 1
create ssr_gen_router hash:net
$(gen_spec_iplist | sed -e "s/^/add ssr_gen_router /")
EOF
$IPT -N SS_SPEC_ROUTER 2>/dev/null
$IPT -F SS_SPEC_ROUTER 2>/dev/null
# $IPT -A SS_SPEC_ROUTER -m mark --mark 255 -j RETURN
$IPT -A SS_SPEC_ROUTER -m set --match-set ssr_gen_router dst -j RETURN
$IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
# fake-ip first
if [ "$ENABLE_FAKE_IP" = "1" ]; then
$IPT -I OUTPUT 1 -m mark --mark 255 -j RETURN 2>/dev/null
$IPT -I OUTPUT 2 -p tcp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
$IPT -I OUTPUT 3 -p udp -d 198.18.0.0/16 -j REDIRECT --to-ports "$local_port" -m comment --comment "$TAG"
$IPT -I OUTPUT 4 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
else
$IPT -I OUTPUT 1 -m mark --mark 255 -j RETURN 2>/dev/null
$IPT -I OUTPUT 2 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
fi
$IPT -N SS_SPEC_ROUTER 2>/dev/null
$IPT -F SS_SPEC_ROUTER 2>/dev/null
$IPT -A SS_SPEC_ROUTER -m mark --mark 255 -j RETURN && \
$IPT -A SS_SPEC_ROUTER -m set --match-set ssr_gen_router dst -j RETURN && \
$IPT -A SS_SPEC_ROUTER -j SS_SPEC_WAN_FW
$IPT -I OUTPUT 1 -p tcp -m comment --comment "$TAG" -j SS_SPEC_ROUTER
;;
esac
return $?
@@ -1001,29 +855,29 @@ tp_rule_nft() {
fi
local MATCH_SET=""
local EXT_ARGS=""
if [ -n "$PROXY_PORTS" ]; then
local PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
[ -n "$PORTS_ARGS" ] && EXT_ARGS="udp dport { $PORTS_ARGS }"
PORTS_ARGS=$(echo "$PROXY_PORTS" | sed 's/-m multiport --dports //')
if [ -n "$PORTS_ARGS" ]; then
EXT_ARGS="udp dport { $PORTS_ARGS }"
else
EXT_ARGS=""
fi
fi
if [ -n "$LAN_AC_IP" ]; then
# Create LAN access control set if needed
local mode="${LAN_AC_IP:0:1}"
local ips="${LAN_AC_IP:1}"
if ! $NFT list set ip ss_spec_mangle ss_spec_lan_ac >/dev/null 2>&1; then
$NFT add set ip ss_spec_mangle ss_spec_lan_ac '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
else
$NFT flush set ip ss_spec_mangle ss_spec_lan_ac 2>/dev/null
fi
for ip in $ips; do
for ip in ${LAN_AC_IP#?}; do
[ -n "$ip" ] && $NFT add element ip ss_spec_mangle ss_spec_lan_ac "{ $ip }" 2>/dev/null
done
case "$mode" in
case "${LAN_AC_IP%${LAN_AC_IP#?}}" in
w | W)
MATCH_SET="ip saddr @ss_spec_lan_ac"
;;
@@ -1052,10 +906,8 @@ tp_rule_nft() {
fi
# Bulk import xhttp ip list into nft whitelist (server + shunt)
local xhttp_file="${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}"
if [ -s "$xhttp_file" ]; then
local xhttp_content=$(tr '\n' ',' < "$xhttp_file" | sed 's/,$//')
[ -n "$xhttp_content" ] && $NFT add element ip ss_spec_mangle whitelist "{ $xhttp_content }" 2>/dev/null
if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then
$NFT add element ip ss_spec_mangle whitelist "{ $(tr '\n' ',' < "${xhttp_ip}" | sed 's/,$//') }" 2>/dev/null
fi
# use priority mangle for compatibility with other rules
@@ -1065,12 +917,6 @@ tp_rule_nft() {
$NFT flush chain ip ss_spec_mangle ss_spec_tproxy 2>/dev/null
fi
# 优先对 Fake-IP 进行 TPROXY 重定向 (无需受限于 PROXY_PORTS 限制)
if [ "$ENABLE_FAKE_IP" = "1" ]; then
$NFT add rule ip ss_spec_mangle ss_spec_tproxy ip daddr 198.18.0.0/16 counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
fi
# Exclude special local addresses
if $NFT list chain ip ss_spec_mangle ss_spec_tproxy >/dev/null 2>&1; then
for net in 0.0.0.0/8 10.0.0.0/8 127.0.0.0/8 169.254.0.0/16 172.16.0.0/12 192.168.0.0/16 224.0.0.0/4 240.0.0.0/4; do
$NFT add rule ip ss_spec_mangle ss_spec_tproxy ip daddr $net return 2>/dev/null
@@ -1088,9 +934,9 @@ tp_rule_nft() {
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport 53 return 2>/dev/null
# avoid redirecting to udp server address
# if [ -n "$server" ]; then
# $NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport != 53 ip daddr "$server" return 2>/dev/null
# fi
if [ -n "$server" ]; then
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp dport != 53 ip daddr "$server" return 2>/dev/null
fi
# if server != SERVER add SERVER to whitelist set (so tproxy won't touch it)
if [ -n "$server" ]; then
@@ -1100,9 +946,6 @@ tp_rule_nft() {
$NFT add element ip ss_spec_mangle whitelist "{ $SERVER }" 2>/dev/null
fi
# 显式豁免白名单集合中的 IP
$NFT add rule ip ss_spec_mangle ss_spec_tproxy ip daddr @whitelist return 2>/dev/null
# access control and tproxy rules
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @bplan return 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip saddr @fplan counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
@@ -1122,9 +965,7 @@ tp_rule_nft() {
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @ss_spec_wan_ac return 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @china return 2>/dev/null
# 80 端口规则:非国内 IP 的 80 端口,走 TPROXY
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp udp dport 80 counter drop comment "\"$TAG\"" 2>/dev/null
# 443 端口规则:非国内 IP 的 443 端口,走 TPROXY
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp udp dport 443 counter drop comment "\"$TAG\"" 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr != @ss_spec_wan_ac counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
@@ -1134,9 +975,7 @@ tp_rule_nft() {
$NFT add set ip ss_spec_mangle gfwlist '{ type ipv4_addr; flags interval; auto-merge; }' 2>/dev/null
fi
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip daddr @china return 2>/dev/null
# 80 端口规则:非国内 IP 的 80 端口,走 TPROXY
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp udp dport 80 counter drop comment "\"$TAG\"" 2>/dev/null
# 443 端口规则:非国内 IP 的 443 端口,走 TPROXY
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp udp dport 443 counter drop comment "\"$TAG\"" 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp $EXT_ARGS ip daddr @gfwlist counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
$NFT add rule ip ss_spec_mangle ss_spec_tproxy meta l4proto udp ip saddr @gmlan ip daddr != @china counter tproxy ip to :"$LOCAL_PORT" meta mark set ${FWMARK} 2>/dev/null
@@ -1149,8 +988,6 @@ tp_rule_nft() {
# finally, ensure prerouting hook entry to jump to tproxy chain
if ! $NFT list chain ip ss_spec_mangle prerouting >/dev/null 2>&1; then
$NFT add chain ip ss_spec_mangle prerouting '{ type filter hook prerouting priority mangle; policy accept; }'
else
$NFT flush chain ip ss_spec_mangle prerouting 2>/dev/null
fi
# add prerouting jump (idempotent)
@@ -1219,14 +1056,8 @@ tp_rule_iptables() {
do
$ipt -A SS_SPEC_TPROXY -p udp -d "$net" -j RETURN
done
# fake-ip 规则
if [ "$ENABLE_FAKE_IP" = "1" ]; then
$ipt -A SS_SPEC_TPROXY -p udp $MATCH_SET -d 198.18.0.0/16 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
fi
[ -n "$SERVER" ] && $ipt -A SS_SPEC_TPROXY -p udp ! --dport 53 -d "$SERVER" -j RETURN
[ -n "$SERVER" ] && [ "$server" != "$SERVER" ] && ipset -! add whitelist "$SERVER"
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set whitelist dst -j RETURN
if [ -f "${xhttp_ip:=/etc/ssrplus/xhttp_address.txt}" ]; then
while IFS= read -r ip; do
[ -n "$ip" ] && ipset add whitelist "$ip" -exist
@@ -1234,7 +1065,6 @@ tp_rule_iptables() {
fi
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set bplan src -j RETURN
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set --match-set fplan src -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set blacklist dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
case "$RUNMODE" in
router)
ipset -! -R <<-EOF || return 1
@@ -1243,27 +1073,15 @@ tp_rule_iptables() {
EOF
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set ss_spec_wan_ac dst -j RETURN
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set china dst -j RETURN
# 80 端口规则:非国内 IP 的 80 端口,走 TPROXY
$ipt -A SS_SPEC_TPROXY -p udp --dport 80 -j DROP
# 443 端口规则:非国内 IP 的 443 端口,走 TPROXY
$ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j DROP
# 保留海外 QUIC 代理
# $ipt -A SS_SPEC_TPROXY -p udp --dport 80 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
# $ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
# $ipt -A SS_SPEC_TPROXY -p udp -m set ! --match-set china dst --dport 80 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
# $ipt -A SS_SPEC_TPROXY -p udp -m set ! --match-set china dst --dport 443 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
$ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j DROP
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set gmlan src -m set ! --match-set china dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set ! --match-set ss_spec_wan_ac dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
;;
gfw)
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set china dst -j RETURN
# 80 端口规则:非国内 IP 的 80 端口,走 TPROXY
$ipt -A SS_SPEC_TPROXY -p udp --dport 80 -j DROP
# 443 端口规则:非国内 IP 的 443 端口,走 TPROXY
$ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j DROP
# 保留海外 QUIC 代理
# $ipt -A SS_SPEC_TPROXY -p udp --dport 80 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
# $ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
$ipt -A SS_SPEC_TPROXY -p udp --dport 443 -j DROP
$ipt -A SS_SPEC_TPROXY -p udp $PROXY_PORTS -m set --match-set gfwlist dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
$ipt -A SS_SPEC_TPROXY -p udp -m set --match-set gmlan src -m set ! --match-set china dst -j TPROXY --on-port "$LOCAL_PORT" --tproxy-mark ${FWMARK}
;;
@@ -1645,7 +1463,7 @@ restore_from_persistence() {
fi
}
while getopts ":m:s:l:S:L:i:e:E:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do
while getopts ":m:s:l:S:L:i:e:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do
case "$arg" in
m)
Interface=$OPTARG
@@ -1668,9 +1486,6 @@ while getopts ":m:s:l:S:L:i:e:E:a:B:b:w:p:G:D:F:N:M:I:oOuUyfgrzAKPCRXh" arg; do
e)
EXT_ARGS=$OPTARG
;;
E)
ENABLE_FAKE_IP=$OPTARG
;;
a)
LAN_AC_IP=$OPTARG
;;
@@ -1874,14 +1689,6 @@ if [ -n "$local_port" ]; then
if [ "$USE_NFT" = "1" ]; then
# NFTables
# Detect Fake_IP Mode state change (execute first)
ENABLE_FAKE_IP_STATE_FILE="/tmp/.last_enable_fake_ip"
if [ -f "$ENABLE_FAKE_IP_STATE_FILE" ]; then
LAST_ENABLE_FAKE_IP=$(cat "$ENABLE_FAKE_IP_STATE_FILE")
else
LAST_ENABLE_FAKE_IP=""
fi
# Save previous TPROXY state file
TPROXY_STATE_FILE="/tmp/.last_tproxy"
if [ -f "$TPROXY_STATE_FILE" ]; then
@@ -1971,13 +1778,6 @@ if [ -n "$local_port" ]; then
LAST_XHTTP_FILE_HASH=""
fi
# Check if Fake_IP Mode state has changed
ENABLE_FAKE_IP_CHANGED=0
if [ "$LAST_ENABLE_FAKE_IP" != "$ENABLE_FAKE_IP" ]; then
loger 6 "Fake_IP Mode state changed: $LAST_ENABLE_FAKE_IP -> $ENABLE_FAKE_IP"
ENABLE_FAKE_IP_CHANGED=1
fi
# STEP 1: Check if TPROXY has value (1 or 2)
if [ "$TPROXY" = "1" ] || [ "$TPROXY" = "2" ]; then
TPROXY_HAS_VALUE=1
@@ -2073,9 +1873,7 @@ if [ -n "$local_port" ]; then
# 1. TPROXY changes from empty ↔ has value
# 2. PROXY_PORTS changes from empty ↔ has value
# 3. Any IP list has changed
# 4. ENABLE_FAKE_IP state changed
if [ "$ENABLE_FAKE_IP_CHANGED" = "1" ] || \
[ "$TPROXY_HAS_VALUE" != "$LAST_HAS_VALUE" ] || \
if [ "$TPROXY_HAS_VALUE" != "$LAST_HAS_VALUE" ] || \
[ "$PROXY_HAS_VALUE" != "$LAST_PROXY_HAS_VALUE" ] || \
[ "$ANY_IP_LIST_CHANGED" = "1" ]; then
FORCE_RECREATE=1
@@ -2093,7 +1891,6 @@ if [ -n "$local_port" ]; then
fi
# STEP 4: Save current state
echo "$ENABLE_FAKE_IP" > "$ENABLE_FAKE_IP_STATE_FILE"
echo "$TPROXY" > "$TPROXY_STATE_FILE"
echo "$PROXY_PORTS" > "$PROXY_PORTS_STATE_FILE"
echo "$(normalize_ip_list "$WAN_BP_IP")" > "$WAN_BP_IP_STATE_FILE"
File diff suppressed because it is too large. Load diff
@@ -3,7 +3,6 @@
require "luci.sys"
local ucursor = require "luci.model.uci".cursor()
local json = require "luci.jsonc"
local datatypes = require "luci.cbi.datatypes"
-- An omitted value reaches us either as a missing argument or as an empty
-- string, depending on whether the caller quoted the expansion. Empty strings
@@ -182,62 +181,6 @@ local function format_host_port(host, port)
return host .. ":" .. tostring(port)
end
local function format_dns_server(proxy)
if not proxy or proxy == "" then
return proxy
end
local scheme, rest = proxy:match("^([a-zA-Z0-9%+%-%.]+)://(.+)$")
if scheme then
scheme = scheme:lower()
if scheme == "tls" or scheme == "dot" then
if not rest:match(":%d+$") then
rest = rest .. ":853"
end
return "tls://" .. rest
elseif scheme == "https" or scheme == "doh" or scheme == "dohl" or scheme == "doq" or scheme == "doql" then
local host, path = rest:match("^([^/]+)(.*)$")
host = host or rest
path = path or ""
if path == "" or path == "/" then
path = "/dns-query"
end
return "https://" .. host .. path
elseif scheme == "tcp" or scheme == "udp" then
if not rest:match(":%d+$") then
rest = rest .. ":53"
end
return scheme .. "://" .. rest
else
return proxy
end
end
local host, port = proxy:match("^([^:]+):(%d+)$")
if host and port then
if port == "853" then
return "tls://" .. host .. ":853"
elseif port == "443" then
return "https://" .. host .. "/dns-query"
elseif port == "53" then
return host -- 53 端口输出纯 IP / Host (如 8.8.4.4)
else
return "udp://" .. host .. ":" .. port
end
end
-- 确保包含字母,防止把 IP 当成域名
if proxy:match("[a-zA-Z]") and not proxy:match("[^%w%.%-]") then
local path = ""
if not proxy:match("/") then
path = "/dns-query"
end
return "https://" .. proxy .. path
end
return proxy
end
-- 确保正确判断程序是否存在
local function is_finded(e)
return luci.sys.exec(string.format('type -t -p "%s" -p "/usr/libexec/%s" 2>/dev/null', e, e)) ~= ""
@@ -448,141 +391,31 @@ local Xray = {
}
if server.type == "v2ray" and dns_mode == "7" and os.getenv("SSR_SWITCH_PROBE") ~= "1" then
local raw_servers_str = builtin_dns_server or "8.8.4.4:53"
local dns_host = builtin_dns_server:match("^([^:]+)") or "8.8.4.4"
local dns_port = tonumber(builtin_dns_server:match(":(%d+)$")) or 53
local dns_servers = {}
local host_list = {}
local port_list = {}
local first_host = nil
local first_port = nil
-- 按逗号或空格分割多个 DNS 地址
for raw_server in string.gmatch(raw_servers_str, "[^,%s\r\n]+") do
if raw_server and raw_server ~= "" then
-- 1. 先进行格式化处理
local formatted = format_dns_server(raw_server)
local host = nil
local port = nil
-- 2. 解析 host 和对应的实际端口
if formatted:match("^https://") then
local target = formatted:match("^https://([^/]+)")
if target:match(":%d+$") then
host, port = target:match("^([^:]+):(%d+)$")
port = tonumber(port)
else
host = target
port = 443
end
elseif formatted:match("^tls://") then
local target = formatted:match("^tls://([^/]+)")
if target:match(":%d+$") then
host, port = target:match("^([^:]+):(%d+)$")
port = tonumber(port)
else
host = target
port = 853
end
elseif formatted:match("^tcp://") or formatted:match("^udp://") then
local target = formatted:match("^[a-zA-Z0-9%+%-%.]+://([^/]+)")
if target:match(":%d+$") then
host, port = target:match("^([^:]+):(%d+)$")
port = tonumber(port)
else
host = target
port = 53
end
else
if formatted:match(":%d+$") then
host, port = formatted:match("^([^:]+):(%d+)$")
port = tonumber(port)
else
host = formatted
port = 53
end
end
-- 保存格式化后的 Xray.dns.servers 条目
table.insert(dns_servers, formatted)
-- 分别收集 Host 与 Port
if host and port then
table.insert(host_list, host)
table.insert(port_list, port)
-- 保存第一个 DNS 的 host 和 port
-- if not first_host then --dokodemo-door 入站的 address 单个地址使用。
-- first_host = host --dokodemo-door 入站的 address 单个地址使用。
-- first_port = port --dokodemo-door 入站的 address 单个地址使用。
-- end
end
end
end
-- 如果没有有效的 DNS 服务器,使用默认值
if #dns_servers == 0 then
table.insert(dns_servers, "8.8.4.4")
table.insert(host_list, "8.8.4.4")
table.insert(port_list, 53)
-- first_host = "8.8.4.4" --dokodemo-door 入站的 address 单个地址使用。
-- first_port = 53 --dokodemo-door 入站的 address 单个地址使用。
end
-- 构建 dns 出站地址(支持逗号分隔的多个地址)
-- 检查所有 DNS 端口是否一致
--local outbound_address = "" --dokodemo-door 入站的 address 单个地址使用。
local all_same_port = true
local common_port = port_list[1]
for i = 2, #port_list do
if port_list[i] ~= common_port then
all_same_port = false
break
end
end
-- 根据端口一致性计算 address 与 target_port
local address_str = ""
local target_port = 0
if all_same_port then
-- outbound_address = table.concat(host_list, ",") --dokodemo-door 入站的 address 单个地址时使用。
address_str = table.concat(host_list, ",")
target_port = common_port
else
local combined = {}
for i = 1, #host_list do
table.insert(combined, string.format("%s:%d", host_list[i], port_list[i]))
end
-- outbound_address = table.concat(combined, ",") --dokodemo-door 入站的 address 单个地址时使用。
address_str = table.concat(combined, ",")
target_port = 0
end
-- 构建 Xray.dns 配置
Xray.dns = {
queryStrategy = (dns_ipv4_only == "1") and "UseIPv4" or "UseIP",
servers = dns_servers
servers = {
string.format("tcp://%s:%d", dns_host, dns_port)
}
}
-- 构建 dokodemo-door 入站
table.insert(Xray.inbounds, {
listen = "127.0.0.1",
port = 5335,
protocol = "dokodemo-door",
settings = {
address = address_str,
-- address = first_host, -- 单个地址
port = target_port,
-- port = first_port, -- 对应端口
address = dns_host,
port = dns_port,
network = "tcp,udp"
},
tag = "builtin-dns-in"
})
xray_builtin_dns = {
address = address_str,
-- address = outbound_address, --dokodemo-door 入站的 address 单个地址时使用。
port = target_port
address = dns_host,
port = dns_port
}
end
-- 传入连接
@@ -27,9 +27,6 @@ config server_subscribe
option auto_update_week_time '*'
option auto_update_day_time '2'
option auto_update_min_time '0'
option enable_mihomo '1'
option enable_fake_ip '1'
option sub_convert '1'
option url_test_url 'https://www.google.com/generate_204'
option user_agent 'v2rayN/9.99'
option filter_words '过期/重置/套餐/剩余/网址/QQ群/官网/防失联/回国'
+3 -3
View File
@@ -17,12 +17,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-wwand
PKG_VERSION:=1.6.8_p1
PKG_RELEASE:=29
PKG_VERSION:=1.6.8_p4
PKG_RELEASE:=30
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
PKG_SOURCE_VERSION:=f4e1c02e71936eb67981734f3a97a710f0aab264
PKG_SOURCE_VERSION:=3d7585f3afe7eca05d55a179f4acc56653d8eac3
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
@@ -360,6 +360,7 @@ return baseclass.extend({
visible: true,
value: band
},
hostname : {
title: _('Hostname'),
visible: true,
@@ -56,7 +56,10 @@ function probeColor(expr) {
* shared with `luci-app-*`, and an app's unlayered `span { color: … !important }` outranks
* a layer and a plain inline style alike. A probe that loses its own colour reports the
* app's, which then becomes the admin's saved axis on the next confirm. */
_probe = E('span', { 'aria-hidden': 'true' });
/* the id is this probe's escape from fs-router.js's strayBodyNode() (issue #56): it is
* appended once and never removed, so without an fs-* id it would read as body litter and
* force every navigation away from this page into a full load. */
_probe = E('span', { 'id': 'fs-appearance-probe', 'aria-hidden': 'true' });
_probe.style.cssText = 'position:fixed!important;left:-9999px!important;top:0!important;'
+ 'width:0!important;height:0!important;overflow:hidden!important;'
+ 'pointer-events:none!important;';
@@ -110,6 +110,12 @@ let _probe = null;
function resolveLen(token, dflt) {
if (!_probe) {
_probe = document.createElement('div');
/* the id is fs-router.js's strayBodyNode() (issue #56) reading an fs-* id as "ours", which is
* a different question from the chrome mark above: this stays unmarked (no data-fs-chrome,
* so the fence still does not spare it from a hostile !important) but still needs SOME fs-*
* name, or an element appended once and never removed would read as body litter and force
* every navigation off this page into a full load. */
_probe.id = 'fs-chrome-geom-probe';
_probe.setAttribute('aria-hidden', 'true');
/* out of flow, no box, no ink: it must never affect layout, scroll extent or hit-testing */
_probe.style.cssText = 'position:absolute!important;visibility:hidden!important;' +
@@ -338,6 +338,66 @@ function discard(el) {
}
}
/* ---- a page that leaves nodes as direct children of <body> hands the next page over by full load,
* the same way an invasive foreign sheet does (docs/third-party-apps.md, Rule 2 extended from CSS to
* the DOM — issue #56) ----
*
* luci-app-bandix's render() appends 7 tooltip/modal nodes to document.body on every visit,
* unconditionally (reproduced on owrt2512 and owrt2410) — nothing here sweeps them; the reasoning is
* docs/spa-router.md, "Foreign view DOM". The recorder is header.ut's first <body> <script>, filling
* window.__fsBodyAdds; this file only reads it. */
/* -> true if `el` is a node the theme must treat as stray body litter: not the theme's own chrome,
* not something stock LuCI parks there itself, not a node type that can never paint. Exported and
* pure (no DOM writes, no window.__fsBodyAdds read) so tests/body-litter.test.mjs can drive it
* without a router or a recorder. */
function strayBodyNode(el) {
if (!el || el.nodeType !== 1) return false;
/* never rendered on their own, wherever a script parks them */
switch (el.nodeName) {
case 'SCRIPT': case 'STYLE': case 'LINK': case 'TEMPLATE': case 'NOSCRIPT': case 'META':
return false;
}
/* zone 1, ours (docs/third-party-apps.md): the chrome mark, or an fs-* id/class. All of these
* parent straight to <body> and must never be read as litter: this file's own #fs-nav-progress,
* fs-search.js's #fs-search-ov (which also carries the mark), and the geometry/colour probes
* fs-chrome.js and fs-appearance.js each park on <body> once and never remove — an fs-* id is
* what tells them from bandix's own unmarked nodes below. */
/* read through `dataset`, not a literal `hasAttribute()` call: `tools/chrome-fence.mjs` counts
* every single-quoted occurrence of the chrome mark's own name (as a JS string) across this
* whole tree as a JS-BUILT chrome root (EXPECT_JS_ROOTS) — right for fs-search.js's palette,
* which mounts one, and a false positive here, which only READS the mark on somebody else's
* node and builds nothing. camelCase avoids the exact spelling the gate scans for. */
if (el.dataset && el.dataset.fsChrome !== undefined) return false;
if (el.id && el.id.indexOf('fs-') === 0) return false;
for (const c of el.classList) if (c.indexOf('fs-') === 0) return false;
/* stock LuCI's own ui.js parents these to <body> at its own __init__ and keeps them for the life
* of the document: #modal_overlay (showModal's host) and the shared .cbi-tooltip */
if (el.id === 'modal_overlay') return false;
if (el.classList.contains('cbi-tooltip')) return false;
/* ui.js's handleDownload() appends a hidden `<a download>`, clicks it once and revokes its object
* URL, but never removes the element itself. Every app that downloads the same way (wireguard,
* filemanager, snmpd, banip/adblock's feed export) calls a.remove() of its own accord and is
* pruned by bodyLittered() below before ever reaching this function. */
if (el.nodeName === 'A' && el.hasAttribute('download') && el.style.display === 'none') return false;
return true;
}
/* -> true iff the document holds a stray body node right now. A recording only ever grows — the
* recorder cannot know a node was later removed or moved inside #view — so this is also the one
* place that prunes it: an entry whose element is no longer a direct child of <body> is dropped for
* good, or the list would grow for the life of the document and re-judge nodes nobody can reach. */
function bodyLittered() {
const adds = window.__fsBodyAdds;
if (!adds || !adds.length) return false;
let littered = false;
for (let i = adds.length - 1; i >= 0; i--) {
if (!document.body || adds[i].parentNode !== document.body) { adds.splice(i, 1); continue; }
if (strayBodyNode(adds[i])) littered = true;
}
return littered;
}
let _wired = false;
/* the pathname whose view is currently rendered; popstate compares against it to tell a real
* navigation from a fragment change */
@@ -887,6 +947,10 @@ function navigate(pathname, push, kbd) {
* only exit leaving both pages correct is a real navigation (fs-sheets.js) */
if (sheets.documentPoisoned()) return false;
/* …and a page that left its own nodes as direct children of <body> is spent the same way — Rule 2
* extended to the DOM, issue #56. See strayBodyNode()/bodyLittered() above. */
if (bodyLittered()) return false;
/* …and a document whose session has died is spent the same way: the only page it can render
* correctly is the login form, and only a real navigation gets there (watchSession) */
if (_expired) return false;
@@ -1495,5 +1559,13 @@ return baseclass.extend({
/* fs-search warms its recents and the arrow-key-highlighted result, neither of which the
* pointer/focus triggers above can see. The edge points search -> router, because the router
* must keep no dependency on the palette. */
prefetchSegs
prefetchSegs,
/* out-of-package, like the three probe exports above (contractBreaks, clearViewIntervals,
* sessionExpired) — prefetchSegs just above is not one of them, it is a real in-package export
* fs-search.js calls. tests/body-litter.test.mjs drives the classifier and the counter directly,
* and navigate() itself to prove the early return actually reaches them rather than just sitting
* beside it unused */
strayBodyNode, /* fs:probe */
bodyLittered, /* fs:probe */
navigate, /* fs:probe */
});
@@ -85,6 +85,46 @@
#}
{% const fs_page = entityencode(join('-', length(ctx.path) ? ctx.path : ctx.request_path), true) %}
<body class="lang_{{ dispatcher.lang }}" data-page="{{ fs_page }}">
<script>
/* Records every node a script parks directly on <body> (issue #56): luci-app-bandix's
* render() appends 7 tooltip/modal nodes unconditionally on every visit, never
* checking whether they already exist. fs-router.js's strayBodyNode()/bodyLittered()
* read window.__fsBodyAdds to decide whether the document is still fit to navigate
* away from — Rule 2 (docs/third-party-apps.md), extended from CSS to the DOM. First
* byte of <body>, before any chrome element or the dispatched page's own <div id="view">.
* Own property of this <body> instance, never Node.prototype, and its gap:
* docs/spa-router.md, "Foreign view DOM". */
(() => {
const body = document.body;
if (!body || body.__fsBodyHooked) return;
body.__fsBodyHooked = true;
const adds = window.__fsBodyAdds || (window.__fsBodyAdds = []);
const names = [ 'appendChild', 'insertBefore', 'replaceChild', 'append', 'prepend',
'insertAdjacentElement', 'insertAdjacentHTML' ];
names.forEach((name) => {
const orig = body[name];
if (typeof orig !== 'function') return;
try {
Object.defineProperty(body, name, {
configurable: true,
writable: true,
value() {
const before = Array.prototype.slice.call(this.children);
const ret = orig.apply(this, arguments);
const after = this.children;
for (let i = 0; i < after.length; i++) {
if (before.indexOf(after[i]) === -1) adds.push(after[i]);
}
return ret;
}
});
}
/* a host that refuses to redefine this one name leaves it unwrapped; the rest
* still are */
catch (e) {}
});
})();
</script>
{#
The wallpaper pattern's paint surface: 15-wallpaper.css masks this element so an
uploaded SVG takes the theme's ink. A mask applies to an element's whole rendering,
+2 -2
View File
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
PKG_NAME:=mise
PKG_VERSION:=2026.9.13
PKG_RELEASE:=20
PKG_VERSION:=2026.9.14
PKG_RELEASE:=21
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
PKG_HASH:=skip
+2 -2
View File
@@ -2,8 +2,8 @@ include $(TOPDIR)/rules.mk
include $(INCLUDE_DIR)/kernel.mk
PKG_NAME:=tcp-brutal
PKG_VERSION:=2.0.0
PKG_RELEASE:=2
PKG_VERSION:=2.0.1
PKG_RELEASE:=3
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/apernet/tcp-brutal/tar.gz/v$(PKG_VERSION)?
+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
PKG_NAME:=wwand
PKG_VERSION:=1.6.8_p6
PKG_RELEASE:=56
PKG_VERSION:=1.6.8_p23
PKG_RELEASE:=57
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=2d7803e46b95f60a8a890ccd961f5154ba672424
PKG_SOURCE_VERSION:=cf8d5cf1cc5767d58824a02c61de4d9d2ff58cf6
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only
@@ -98,7 +98,7 @@ UCDIR:=/usr/share/ucode/wwand
WWAND_BASE_UC:=apndb.uc atcmd.uc atcmd_parse.uc atport.uc backend.uc board.uc \
carrier_config.uc client.uc config.uc config_check.uc context_common.uc \
context_monitor_qmi.uc ctx_settings.uc daemon.uc deps.uc discovery.uc \
hwops.uc log.uc modem_common.uc modem_datapath_qmi.uc modem_init_qmi.uc \
hwops.uc log.uc modem_common.uc modem_datapath_qmi.uc modem_init_qmi.uc plugins.uc \
modem_quirks.uc modeswitch.uc ncm_vendors.uc netlink.uc netsel_ops.uc \
protocol_switch.uc reconnect.uc recovery.uc regdetail.uc sim.uc sim_plmn.uc \
simops.uc sms.uc sms_pdu.uc telemetry_mbim.uc telemetry_ncm.uc \