Compare commits

...

80 Commits

Author SHA1 Message Date
Kirill Sobakin
64369a93b0 Merge pull request #263 from kjljxybr/main
Translation update for the installation script
2025-12-03 11:48:57 +03:00
Kirill Sobakin
53a3c943f0 Merge pull request #265 from itdoginfo/fix/service_listen_address
Fix/service listen address
2025-12-03 11:46:53 +03:00
Andrey Petelin
7c7e1c6244 fix: take first LAN IP address and strip CIDR suffix 2025-12-03 10:21:51 +05:00
Andrey Petelin
7fc1f39dd6 fix: have service_listen_address option override automatic detection of listening IP address 2025-12-03 09:58:28 +05:00
Artem Kireev
1c4285dfa8 translations 2025-12-02 10:34:38 +03:00
Kirill Sobakin
ea1273e05e Fix: UDP is lost. Double function call 2025-12-01 23:30:33 +03:00
Kirill Sobakin
5fc3c95928 Merge pull request #262 from itdoginfo/feat/hy2
Feat/hy2
2025-12-01 17:44:40 +03:00
divocat
dd3e70153a fix: correct small points 2025-12-01 16:38:26 +02:00
divocat
622e092317 feat: add hy2 validator 2025-11-30 18:35:06 +02:00
Kirill Sobakin
c045f8f224 Add grpc mode example from #259 2025-11-28 00:44:21 +03:00
Kirill Sobakin
b45088dad7 Merge pull request #259 from kokoc26/main
Feat: service_name for gRPC
2025-11-28 00:43:34 +03:00
Andrey Petelin
82345047cb feat: Add Hysteria2 outbound support 2025-11-26 21:04:46 +05:00
Andrey Petelin
0a4ed367bc refactor: add url_get_scheme and simplify url_get_host/url_get_port using parameter expansion 2025-11-26 21:01:33 +05:00
Andrey Petelin
c3f322ae61 Merge branch 'main' into feat/hy2 2025-11-26 17:06:27 +05:00
Kokoc
eb9239696e feat: add support for optional gRPC service name in outbound transport configuration 2025-11-26 14:52:13 +03:00
Kirill Sobakin
5b3421498e Merge pull request #258 from itdoginfo/refactor/dnsmasq
Refactor/dnsmasq
2025-11-26 14:14:02 +03:00
Andrey Petelin
6a48a060e1 refactor: remove sing-box start exit check 2025-11-26 16:01:41 +05:00
Andrey Petelin
14f704fcb8 fix: use echolog for sing-box start failure 2025-11-26 15:47:12 +05:00
Andrey Petelin
ff43f477e9 chore: restore shutdown_correctly logic 2025-11-26 14:14:27 +05:00
Andrey Petelin
576e58fd17 chore: restore start_main and stop_main; have reload call them instead of full start/stop 2025-11-26 13:56:10 +05:00
Andrey Petelin
d72c98a254 chore: clarify and standardize argument type annotations and optional flags 2025-11-26 10:14:06 +05:00
Andrey Petelin
7a497f1e31 fix: reload PODKOP_CONFIG after uci commit to refresh config on shutdown 2025-11-25 17:05:25 +05:00
Andrey Petelin
d52f6e26ae refactor: add configurable DNS/curl timeouts and retries, detect service proxy, and improve connection checks 2025-11-25 17:04:31 +05:00
Andrey Petelin
68c61aed50 refactor: use uci wrappers 2025-11-25 14:10:18 +05:00
Andrey Petelin
626ac981eb refactor: configuring dnsmasq after starting sing-box 2025-11-25 13:53:24 +05:00
Kirill Sobakin
352d10a047 Fix: HY2 links 2025-11-25 11:32:50 +03:00
Kirill Sobakin
031c419ffb Merge pull request #252 from itdoginfo/fix/argument-list-too-long
jq: Argument list too long
2025-11-24 18:13:43 +03:00
Kirill Sobakin
c13fdf5785 HY2 examples 2025-11-24 18:05:40 +03:00
Andrey Petelin
1b7ab606ba refactor: unify source ruleset preparation and list handlers; make ruleset creation idempotent and atomic updates 2025-11-21 20:37:19 +05:00
Andrey Petelin
2bf208ecac fix: import remote plain domain and subnet lists using chunked processing 2025-11-16 13:21:51 +05:00
Andrey Petelin
e256e4bee5 chore: shorten Text List option label by removing the detailed format hint 2025-11-16 09:56:12 +05:00
Andrey Petelin
32c385b309 fix: load large plain domain/subnet lists in chunks; move ruleset logic to rulesets.sh and nft chunker to nft.sh 2025-11-16 09:55:44 +05:00
Kirill Sobakin
56829c74c8 Merge pull request #246 from itdoginfo/fix/listening_address 2025-11-10 12:58:10 +03:00
Andrey Petelin
9d78cd2ce4 style: add missing semicolons to o.depends calls in luci-app-podkop settings.js 2025-11-06 21:20:05 +05:00
Andrey Petelin
d9ce3b361e chore: correct typo "spedifying" to "specifying" in REST API secret comment 2025-11-06 21:18:15 +05:00
divocat
c67aadf267 feat: add yacd_secret_key support for ws 2025-11-06 16:52:08 +02:00
divocat
ac4d7570f3 feat: add translations for new keys 2025-11-06 16:20:35 +02:00
Andrey Petelin
86897fd0af fix: bind mixed proxy and Clash API to service IP (no 0.0.0.0); add YACD WAN toggle and secret key 2025-11-06 16:33:03 +05:00
Andrey Petelin
230ffbce46 feat: Add optional secret for RESTful API to experimental.clash_api config 2025-11-06 16:30:42 +05:00
Kirill Sobakin
dd5ddd1a14 Merge pull request #240 from itdoginfo/fix/long-nft-command
Import large subnet lists in chunks into nft sets
2025-10-30 16:01:14 +03:00
Andrey Petelin
cc947f9734 fix: import large subnet lists in chunks into nft sets 2025-10-30 14:07:12 +05:00
Kirill Sobakin
f8510cd828 Merge pull request #239 from itdoginfo/fix/crlf-clean
BUG: Clearing CRLF from SRS files
2025-10-29 21:15:47 +03:00
Andrey Petelin
23cbe7be4a fix: include filename in log and remove temp file on CRLF-to-LF conversion 2025-10-29 22:11:29 +05:00
Andrey Petelin
f168fb7e31 refactor: fetch remote JSON to temp files and parse ip_cidr into subnets; remove download_to_stream 2025-10-29 21:52:44 +05:00
Andrey Petelin
fe84b3154f fix: convert Windows CRLF line endings to LF for downloaded files 2025-10-29 21:36:46 +05:00
Kirill Sobakin
d09fdc0b95 Merge pull request #235 from itdoginfo/feat/urltest
feat/urltest
2025-10-27 16:07:13 +03:00
divocat
835cd85970 feat: increase timeouts for delays 2s->5s & 5s -> 10s 2025-10-27 14:56:10 +02:00
divocat
8a3b41ec9c Update fe-app-podkop/locales/podkop.ru.po
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 14:18:36 +02:00
divocat
10d7617739 fix: run linter 2025-10-27 14:15:19 +02:00
divocat
68010ed5f7 Update luci-app-podkop/htdocs/luci-static/resources/view/podkop/section.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 14:13:31 +02:00
divocat
557e3666eb Update luci-app-podkop/po/ru/podkop.po
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-27 14:13:20 +02:00
Andrey Petelin
01bff8ccfb chore: refine Russian translations for 'URLTest Testing URL' and 'URLTest Tolerance' 2025-10-27 16:21:51 +05:00
divocat
675a6af89c feat: simplify sb check displaying 2025-10-27 13:16:18 +02:00
divocat
f1a6ff3469 feat: add validations & translations 2025-10-27 13:06:33 +02:00
Andrey Petelin
d4b3377d68 feat: add URLTest check interval, tolerance and testing URL options and wire them into outbound config generation 2025-10-27 15:17:20 +05:00
Kirill Sobakin
d2ef640d76 Merge pull request #233 from itdoginfo/feat/add_display_name
feat: replace outbound code with display name
2025-10-27 12:08:04 +03:00
divocat
47457f2c27 feat: replace outbound code with display name 2025-10-26 16:07:28 +02:00
Kirill Sobakin
8a29e176f2 Merge pull request #232 from itdoginfo/fix/change_json_outbound_validation
small pack of fixes
2025-10-26 16:07:47 +03:00
divocat
9653310208 fix: update locales && possible fix of incorrect outdated 2025-10-26 14:58:09 +02:00
divocat
3540610c78 fix: potential fix of structuredClone for old browsers 2025-10-26 14:52:08 +02:00
divocat
fb54d62a7f feat: actualize json outbound validation 2025-10-26 14:46:39 +02:00
Kirill Sobakin
288b8d4cc2 Merge pull request #230 from itdoginfo/feat/diagnostic-outbound-check
Add outbound check to diagnostic
2025-10-26 09:27:09 +03:00
divocat
e014396ae2 feat: extend selector checks displaying 2025-10-26 01:37:17 +03:00
divocat
694e4ca35a fix: remove extra console log 2025-10-26 01:10:33 +03:00
divocat
788c539e16 feat: add outbounds checks to diagnostics 2025-10-26 01:09:24 +03:00
Kirill Sobakin
743cba8936 Merge pull request #229 from itdoginfo/fix/show-config
Fix masked config
2025-10-25 17:38:45 +03:00
Andrey Petelin
d1d703764c fix: mask outbound_json block and DNS/domain_resolver addresses in podkop config output 2025-10-25 19:27:01 +05:00
Kirill Sobakin
2efd415305 Merge pull request #226 from itdoginfo/fix/excluded_ips
Routing Excluded IPs
2025-10-24 15:34:57 +03:00
Andrey Petelin
407b19b3ed fix: read routing_excluded_ips as non-boolean string with config_get instead of config_get_bool 2025-10-24 17:32:35 +05:00
Kirill Sobakin
c3fac995d5 Merge pull request #224 from itdoginfo/feat/fe-improvements
Some FE improvements
2025-10-23 21:12:03 +03:00
divocat
21ecfbbeca fix: correct types on ru translations 2025-10-23 20:43:46 +03:00
divocat
2918487845 feat: add custom port support to dns 2025-10-23 20:33:18 +03:00
divocat
ac258c53c0 fix: alert displaying 2025-10-23 20:05:55 +03:00
divocat
9a389c47bf fix: actualize locales 2025-10-23 20:02:49 +03:00
divocat
7cd70468c5 feat: add wiki disclaimer to diagnostics 2025-10-23 20:00:55 +03:00
divocat
13d27dab21 feat: add toast when shell exec failed 2025-10-23 19:08:27 +03:00
divocat
9f8f032dce feat: increase shell timeout to 15s 2025-10-23 19:01:06 +03:00
divocat
8301f4c271 feat: update checks displaying 2025-10-23 18:59:23 +03:00
divocat
c4078c8242 feat: update some translations 2025-10-23 18:35:34 +03:00
Kirill Sobakin
e0d149f03a fix 2025-10-23 16:39:41 +03:00
50 changed files with 3403 additions and 1622 deletions

View File

@@ -34,6 +34,7 @@ vless://4d21ce62-8723-4c4d-93e3-d586b107aa40@127.0.0.1:51394?type=ws&encryption=
# gRPC
vless://974b39e3-f7bf-42b9-933c-16699c635e77@127.0.0.1:15633?type=grpc&encryption=none&serviceName=TunService&authority=&security=none#vless-gRPC-none
vless://651e7eca-5152-46f1-baf2-d502e0af7b27@127.0.0.1:28535?type=grpc&encryption=none&serviceName=TunService&authority=authority&security=reality&pbk=nhZ7NiKfcqESa5ZeBFfsq9o18W-OWOAHLln9UmuVXSk&fp=chrome&sni=google.com&sid=11cbaeaa&spx=%2F#vless-gRPC-reality
vless://221ff905-b783-41a0-a6a6-8089eaf3b34b@abc.def.xyz:443?security=reality&type=grpc&headerType=&authority=abc.def.xyz&serviceName=name&mode=gun&sni=abc.def.xyz&fp=chrome&pbk=C3nhDJw02ZU_rjx4GbC54Sp79-ysF5lWIQVWdY4FOnE&sid=#vless-gRPC-reality-mode
vless://af1f8b5f-26c9-4fe8-8ce7-6d6366c5c9ce@127.0.0.1:47904?type=grpc&encryption=none&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-gRPC-tls
vless://95f2c4bb-abcb-47ba-bfad-e181c03e4659@127.0.0.1:34530?type=grpc&encryption=none&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#vless-gRPC-tls-insecure
vless://bd39490f-9a4f-49b2-96b6-824190cf89e9@127.0.0.1:27779?type=grpc&encryption=none&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com&ech=AF3%2BDQBZAAAgACBc%2FiNdo4QkTt9eQCQgkOiJVSfA9G6UWAyipaBFtBD%2FVQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D#vless-gRPC-tls-ech
@@ -81,4 +82,38 @@ trojan://ou8pLSyx9N@127.0.0.1:17737?type=httpupgrade&path=%2Fhttpupgradepath&hos
# XHTTP
trojan://VEetltxLtw@127.0.0.1:59072?type=xhttp&path=%2Fxhttppath&host=google.com&mode=auto&security=none#trojan-xhttp
```
## Hysteria2
hysteria2://
```
# With password
hysteria2://password@example.com:443/#hysteria2-password
hysteria2://password@example.com:443/?insecure=1#hysteria2-password-insecure
# With SNI
hysteria2://password@example.com:443/?sni=example.com#hysteria2-password-sni
# With obfuscation
hysteria2://password@example.com:443/?obfs=salamander&obfs-password=obfspassword#hysteria2-obfs
# All parameters combined
hysteria2://mypassword@example.com:8443/?sni=example.com&obfs=salamander&obfs-password=obfspass&insecure=1#hysteria2-all-params
```
hy2://
```
# With password
hy2://password@example.com:443/#hysteria2-password
hy2://password@example.com:443/?insecure=1#hysteria2-password-insecure
# With SNI
hy2://password@example.com:443/?sni=example.com#hysteria2-password-sni
# With obfuscation
hy2://password@example.com:443/?obfs=salamander&obfs-password=obfspassword#hysteria2-obfs
# All parameters combined
hy2://mypassword@example.com:8443/?sni=example.com&obfs=salamander&obfs-password=obfspass&insecure=1#hysteria2-all-params
```

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-21 23:02+0300\n"
"PO-Revision-Date: 2025-10-21 23:02+0300\n"
"POT-Creation-Date: 2025-12-01 16:30+0200\n"
"PO-Revision-Date: 2025-12-01 16:30+0200\n"
"Last-Translator: divocat\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -35,6 +35,9 @@ msgstr "Активные соединения"
msgid "Additional marking rules found"
msgstr "Найдены дополнительные правила маркировки"
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
msgid "Applicable for SOCKS and Shadowsocks proxy"
msgstr "Применимо для SOCKS и Shadowsocks прокси"
@@ -44,6 +47,9 @@ msgstr "Необходимо указать хотя бы один действ
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы."
msgid "Available actions"
msgstr "Доступные действия"
msgid "Bootsrap DNS"
msgstr "Bootstrap DNS"
@@ -62,26 +68,20 @@ msgstr "Путь к файлу кэша"
msgid "Cache file path cannot be empty"
msgstr "Путь к файлу кэша не может быть пустым"
msgid "Cannot receive DNS checks result"
msgstr "Не удалось получить результаты проверки DNS"
msgid "Cannot receive checks result"
msgstr "Не удалось получить результаты проверки"
msgid "Cannot receive nftables checks result"
msgstr "Не удалось получить результаты проверки nftables"
msgid "Checking, please wait"
msgstr "Проверяем, пожалуйста подождите"
msgid "Cannot receive Sing-box checks result"
msgstr "Не удалось получить результаты проверки Sing-box"
msgid "checks"
msgstr "проверки"
msgid "Checking dns, please wait"
msgstr "Проверка dns, пожалуйста подождите"
msgid "Checks failed"
msgstr "Проверки не выполнены"
msgid "Checking FakeIP, please wait"
msgstr "Проверка FakeIP, пожалуйста подождите"
msgid "Checking nftables, please wait"
msgstr "Проверка nftables, пожалуйста подождите"
msgid "Checking sing-box, please wait"
msgstr "Проверка sing-box, пожалуйста подождите"
msgid "Checks passed"
msgstr "Проверки пройдены"
msgid "CIDR must be between 0 and 32"
msgstr "CIDR должен быть между 0 и 32"
@@ -143,12 +143,6 @@ msgstr "Отключить QUIC протокол для улучшения со
msgid "Disabled"
msgstr "Отключено"
msgid "DNS checks"
msgstr "DNS проверки"
msgid "DNS checks passed"
msgstr "DNS проверки успешно завершены"
msgid "DNS on router"
msgstr "DNS на роутере"
@@ -170,6 +164,9 @@ msgstr "DNS-сервер"
msgid "DNS server address cannot be empty"
msgstr "Адрес DNS-сервера не может быть пустым"
msgid "Do not panic, everything can be fixed, just..."
msgstr "Не паникуйте, всё можно исправить, просто..."
msgid "Domain Resolver"
msgstr "Резолвер доменов"
@@ -188,9 +185,6 @@ msgstr "Скачивать списки через Proxy/VPN"
msgid "Download Lists via specific proxy section"
msgstr "Скачивать списки через выбранную секцию"
msgid "Downloading all lists via main Proxy/VPN"
msgstr "Загрузка всех списков через основной прокси/VPN"
msgid "Downloading all lists via specific Proxy/VPN"
msgstr "Загрузка всех списков через указанный прокси/VPN"
@@ -215,6 +209,9 @@ msgstr "Включить смешанный прокси-сервер, разр
msgid "Enable YACD"
msgstr "Включить YACD"
msgid "Enable YACD WAN Access"
msgstr "Включить доступ YACD WAN"
msgid "Enter complete outbound configuration in JSON format"
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
@@ -227,6 +224,18 @@ msgstr "Введите доменные имена без протоколов,
msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses"
msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса"
msgid "Every 1 minute"
msgstr "Каждую минуту"
msgid "Every 3 minutes"
msgstr "Каждые 3 минуты"
msgid "Every 30 seconds"
msgstr "Каждые 30 секунд"
msgid "Every 5 minutes"
msgstr "Каждые 5 минут"
msgid "Exclude NTP"
msgstr "Исключить NTP"
@@ -236,17 +245,8 @@ msgstr "Исключите трафик протокола NTP из туннел
msgid "Failed to copy!"
msgstr "Не удалось скопировать!"
msgid "FakeIP checks"
msgstr "Проверка FakeIP"
msgid "FakeIP checks failed"
msgstr "Проверки FakeIP не пройдены"
msgid "FakeIP checks partially passed"
msgstr "Проверка FakeIP частично пройдена"
msgid "FakeIP checks passed"
msgstr "Проверки FakeIP пройдены"
msgid "Failed to execute!"
msgstr "Не удалось выполнить!"
msgid "Fastest"
msgstr "Самый быстрый"
@@ -281,6 +281,45 @@ msgstr "Неверный домен"
msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y"
msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y"
msgid "Invalid HY2 URL: insecure must be 0 or 1"
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
msgid "Invalid HY2 URL: invalid port number"
msgstr "Неверный URL Hysteria2: неверный номер порта"
msgid "Invalid HY2 URL: missing credentials/server"
msgstr "Неверный URL Hysteria2: отсутствуют учетные данные/сервер"
msgid "Invalid HY2 URL: missing host"
msgstr "Неверный URL Hysteria2: отсутствует хост"
msgid "Invalid HY2 URL: missing host & port"
msgstr "Неверный URL Hysteria2: отсутствуют хост и порт"
msgid "Invalid HY2 URL: missing password"
msgstr "Неверный URL Hysteria2: отсутствует пароль"
msgid "Invalid HY2 URL: missing port"
msgstr "Неверный URL Hysteria2: отсутствует порт"
msgid "Invalid HY2 URL: must not contain spaces"
msgstr "Неверный URL Hysteria2: не должен содержать пробелов"
msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://"
msgstr "Неверный URL Hysteria2: должен начинаться с hysteria2:// или hy2://"
msgid "Invalid HY2 URL: obfs-password required when obfs is set"
msgstr "Неверный URL Hysteria2: требуется obfs-password, когда установлен obfs"
msgid "Invalid HY2 URL: parsing failed"
msgstr "Неверный URL Hysteria2: ошибка разбора"
msgid "Invalid HY2 URL: sni cannot be empty"
msgstr "Неверный URL Hysteria2: sni не может быть пустым"
msgid "Invalid HY2 URL: unsupported obfs type"
msgstr "Неверный URL Hysteria2: неподдерживаемый тип obfs"
msgid "Invalid IP address"
msgstr "Неверный IP-адрес"
@@ -365,6 +404,9 @@ msgstr "Неверный URL VLESS: ошибка разбора"
msgid "IP address 0.0.0.0 is not allowed"
msgstr "IP-адрес 0.0.0.0 не допускается"
msgid "Issues detected"
msgstr "Обнаружены проблемы"
msgid "Latest"
msgstr "Последняя"
@@ -389,24 +431,21 @@ msgstr "Порт смешанного прокси"
msgid "Monitored Interfaces"
msgstr "Наблюдаемые интерфейсы"
msgid "Must be a number in the range of 50 - 1000"
msgstr "Должно быть числом от 50 до 1000"
msgid "Network Interface"
msgstr "Сетевой интерфейс"
msgid "Nftables checks"
msgstr "Проверки Nftables"
msgid "Nftables checks partially passed"
msgstr "Проверки Nftables частично пройдена"
msgid "Nftables checks passed"
msgstr "Nftables проверки успешно завершены"
msgid "No other marking rules found"
msgstr "Другие правила маркировки не найдены"
msgid "Not implement yet"
msgstr "Ещё не реализовано"
msgid "Not responding"
msgstr "Не отвечает"
msgid "Not running"
msgstr "Не запущено"
@@ -419,9 +458,6 @@ msgstr "Конфигурация Outbound"
msgid "Outbound Configuration"
msgstr "Конфигурация исходящего соединения"
msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields"
msgstr "JSON должен содержать поля \"type\", \"server\" и \"server_port\""
msgid "Outdated"
msgstr "Устаревшая"
@@ -440,6 +476,9 @@ msgstr "Путь должен содержать хотя бы одну дире
msgid "Path must end with cache.db"
msgstr "Путь должен заканчиваться на cache.db"
msgid "Pending"
msgstr "Ожидает запуска"
msgid "Podkop"
msgstr "Podkop"
@@ -458,17 +497,14 @@ msgstr "Прокси-трафик не маршрутизируется чере
msgid "Proxy traffic is routed via FakeIP"
msgstr "Прокси-трафик направляется через FakeIP"
msgid "Queued"
msgstr "В очереди"
msgid "Regional options cannot be used together"
msgstr "Нельзя использовать несколько региональных опций одновременно"
msgid "Remote Domain Lists"
msgstr "Удалённые списки доменов"
msgstr "Внешние списки доменов"
msgid "Remote Subnet Lists"
msgstr "Удалённые списки подсетей"
msgstr "Внешние списки подсетей"
msgid "Restart podkop"
msgstr "Перезапустить Podkop"
@@ -506,6 +542,9 @@ msgstr "Запустить диагностику"
msgid "Russia inside restrictions"
msgstr "Ограничения Russia inside"
msgid "Secret key for authenticating remote access to YACD when WAN access is enabled."
msgstr "Секретный ключ для аутентификации удаленного доступа к YACD при включенном доступе через WAN."
msgid "Sections"
msgstr "Секции"
@@ -569,12 +608,6 @@ msgstr "Sing-box"
msgid "Sing-box autostart disabled"
msgstr "Автостарт sing-box отключен"
msgid "Sing-box checks"
msgstr "Sing-box проверки"
msgid "Sing-box checks passed"
msgstr "Sing-box проверки успешно завершены"
msgid "Sing-box installed"
msgstr "Sing-box установлен"
@@ -587,8 +620,8 @@ msgstr "Процесс sing-box запущен"
msgid "Sing-box service exist"
msgstr "Сервис sing-box существует"
msgid "Sing-box version >= 1.12.4"
msgstr "Версия sing-box >= 1.12.4"
msgid "Sing-box version is compatible (newer than 1.12.4)"
msgstr "Версия Sing-box совместима (новее 1.12.4)"
msgid "Source Network Interface"
msgstr "Сетевой интерфейс источника"
@@ -600,10 +633,10 @@ msgid "Specify local IP addresses or subnets whose traffic will always be routed
msgstr "Укажите локальные IP-адреса или подсети, трафик которых всегда будет направляться через настроенный маршрут."
msgid "Specify remote URLs to download and use domain lists"
msgstr "Укажите удаленные URL-адреса для загрузки и использования списков доменов."
msgstr "Укажите URL-адреса для загрузки и использования списков доменов."
msgid "Specify remote URLs to download and use subnet lists"
msgstr "Укажите удаленные URL-адреса для загрузки и использования списков подсетей."
msgstr "Укажите URL-адреса для загрузки и использования списков подсетей."
msgid "Specify the path to the list file located on the router filesystem"
msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора."
@@ -620,21 +653,30 @@ msgstr "Успешно скопировано!"
msgid "System info"
msgstr "Системная информация"
msgid "System information"
msgstr "Системная информация"
msgid "Table exist"
msgstr "Таблица существует"
msgid "Test latency"
msgstr "Измерить задержки"
msgstr "Тестирование задержки"
msgid "Text List"
msgstr "Текстовый список"
msgid "Text List (comma/space/newline separated)"
msgstr "Текстовый список (через запятую, пробел или новую строку)"
msgid "The DNS server used to look up the IP address of an upstream DNS server"
msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера"
msgid "The interval between connectivity tests"
msgstr "Интервал между тестами подключения"
msgid "The maximum difference in response times (ms) allowed when comparing servers"
msgstr "Максимально допустимая разница во времени отклика (мс) при сравнении серверов"
msgid "The URL used to test server connectivity"
msgstr "URL-адрес, используемый для проверки подключения к серверу"
msgid "Time in seconds for DNS record caching (default: 60)"
msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)"
@@ -644,6 +686,9 @@ msgstr "Трафик"
msgid "Traffic Total"
msgstr "Всего трафика"
msgid "Troubleshooting"
msgstr "Устранение неполадок"
msgid "TTL must be a positive number"
msgstr "TTL должно быть положительным числом"
@@ -665,8 +710,8 @@ msgstr "Неизвестная ошибка"
msgid "Uplink"
msgstr "Исходящий"
msgid "URL must start with vless://, ss://, trojan://, or socks4/5://"
msgstr "URL должен начинаться с vless://, ss://, trojan:// или socks4/5://"
msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://"
msgstr "URL должен начинаться с vless://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://"
msgid "URL must use one of the following protocols:"
msgstr "URL должен использовать один из следующих протоколов:"
@@ -674,9 +719,18 @@ msgstr "URL должен использовать один из следующи
msgid "URLTest"
msgstr "URLTest"
msgid "URLTest Check Interval"
msgstr "Интервал проверки URLTest"
msgid "URLTest Proxy Links"
msgstr "Ссылки прокси для URLTest"
msgid "URLTest Testing URL"
msgstr "URLTest ссылка для проверки"
msgid "URLTest Tolerance"
msgstr "URLTest допустимое отклонение"
msgid "User Domain List Type"
msgstr "Тип пользовательского списка доменов"
@@ -704,11 +758,17 @@ msgstr "Ошибки валидации:"
msgid "View logs"
msgstr "Посмотреть логи"
msgid "Visit Wiki"
msgstr "Перейти в wiki"
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных."
msgid "YACD Secret Key"
msgstr "Секретный ключ YACD"
msgid "You can select Output Network Interface, by default autodetect"
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически."

View File

@@ -15,3 +15,4 @@ export * from './renderCircleCheckBigIcon24';
export * from './renderSquareChartGanttIcon24';
export * from './renderCogIcon24';
export * from './renderSearchIcon24';
export * from './renderBookOpenTextIcon24';

View File

@@ -0,0 +1,28 @@
import { svgEl } from '../helpers';
export function renderBookOpenTextIcon24() {
const NS = 'http://www.w3.org/2000/svg';
return svgEl(
'svg',
{
xmlns: NS,
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
'stroke-width': '2',
'stroke-linecap': 'round',
'stroke-linejoin': 'round',
class: 'lucide lucide-book-open-text-icon lucide-book-open-text',
},
[
svgEl('path', { d: 'M12 7v14' }),
svgEl('path', { d: 'M16 12h2' }),
svgEl('path', { d: 'M16 8h2' }),
svgEl('path', {
d: 'M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z',
}),
svgEl('path', { d: 'M6 12h2' }),
svgEl('path', { d: 'M6 8h2' }),
],
);
}

View File

@@ -4,6 +4,9 @@
'require uci';
'require ui';
if (typeof structuredClone !== 'function')
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
export * from './validators';
export * from './helpers';
export * from './podkop';

View File

@@ -0,0 +1,9 @@
import { getConfigSections } from './getConfigSections';
export async function getClashApiSecret() {
const sections = await getConfigSections();
const settings = sections.find((section) => section['.type'] === 'settings');
return settings?.yacd_secret_key || '';
}

View File

@@ -1,7 +1,9 @@
import { getConfigSections } from './getConfigSections';
import { getDashboardSections } from './getDashboardSections';
import { getClashApiSecret } from './getClashApiSecret';
export const CustomPodkopMethods = {
getConfigSections,
getDashboardSections,
getClashApiSecret,
};

View File

@@ -9,7 +9,7 @@ export async function callBaseMethod<T>(
const response = await executeShellCommand({
command,
args: [method as string, ...args],
timeout: 10000,
timeout: 15000,
});
if (response.stdout) {

View File

@@ -29,15 +29,15 @@ export const PodkopShellMethods = {
Podkop.AvailableClashAPIMethods.GET_PROXIES,
]),
getClashApiProxyLatency: async (tag: string) =>
callBaseMethod<unknown>(Podkop.AvailableMethods.CLASH_API, [
Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY,
tag,
]),
callBaseMethod<Podkop.GetClashApiProxyLatency>(
Podkop.AvailableMethods.CLASH_API,
[Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, '5000'],
),
getClashApiGroupLatency: async (tag: string) =>
callBaseMethod<unknown>(Podkop.AvailableMethods.CLASH_API, [
Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY,
tag,
]),
callBaseMethod<Podkop.GetClashApiGroupLatency>(
Podkop.AvailableMethods.CLASH_API,
[Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, '10000'],
),
setClashApiGroupProxy: async (group: string, proxy: string) =>
callBaseMethod<unknown>(Podkop.AvailableMethods.CLASH_API, [
Podkop.AvailableClashAPIMethods.SET_GROUP_PROXY,

View File

@@ -8,6 +8,7 @@ import { CustomPodkopMethods, PodkopShellMethods } from '../../methods';
import { logger, socket, store, StoreType } from '../../services';
import { renderSections, renderWidget } from './partials';
import { fetchServicesInfo } from '../../fetchers';
import { getClashApiSecret } from '../../methods/custom/getClashApiSecret';
// Fetchers
@@ -38,8 +39,10 @@ async function fetchDashboardSections() {
}
async function connectToClashSockets() {
const clashApiSecret = await getClashApiSecret();
socket.subscribe(
`${getClashWsUrl()}/traffic?token=`,
`${getClashWsUrl()}/traffic?token=${clashApiSecret}`,
(msg) => {
const parsedMsg = JSON.parse(msg);
@@ -68,7 +71,7 @@ async function connectToClashSockets() {
);
socket.subscribe(
`${getClashWsUrl()}/connections?token=`,
`${getClashWsUrl()}/connections?token=${clashApiSecret}`,
(msg) => {
const parsedMsg = JSON.parse(msg);

View File

@@ -1,8 +1,11 @@
import { getCheckTitle } from '../helpers/getCheckTitle';
export enum DIAGNOSTICS_CHECKS {
DNS = 'DNS',
SINGBOX = 'SINGBOX',
NFT = 'NFT',
FAKEIP = 'FAKEIP',
OUTBOUNDS = 'OUTBOUNDS',
}
export const DIAGNOSTICS_CHECKS_MAP: Record<
@@ -11,22 +14,27 @@ export const DIAGNOSTICS_CHECKS_MAP: Record<
> = {
[DIAGNOSTICS_CHECKS.DNS]: {
order: 1,
title: _('DNS checks'),
title: getCheckTitle('DNS'),
code: DIAGNOSTICS_CHECKS.DNS,
},
[DIAGNOSTICS_CHECKS.SINGBOX]: {
order: 2,
title: _('Sing-box checks'),
title: getCheckTitle('Sing-box'),
code: DIAGNOSTICS_CHECKS.SINGBOX,
},
[DIAGNOSTICS_CHECKS.NFT]: {
order: 3,
title: _('Nftables checks'),
title: getCheckTitle('Nftables'),
code: DIAGNOSTICS_CHECKS.NFT,
},
[DIAGNOSTICS_CHECKS.FAKEIP]: {
[DIAGNOSTICS_CHECKS.OUTBOUNDS]: {
order: 4,
title: _('FakeIP checks'),
title: getCheckTitle('Outbounds'),
code: DIAGNOSTICS_CHECKS.OUTBOUNDS,
},
[DIAGNOSTICS_CHECKS.FAKEIP]: {
order: 5,
title: getCheckTitle('FakeIP'),
code: DIAGNOSTICS_CHECKS.FAKEIP,
},
};

View File

@@ -3,6 +3,7 @@ import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { PodkopShellMethods } from '../../../methods';
import { IDiagnosticsChecksItem } from '../../../services';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
export async function runDnsCheck() {
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS;
@@ -11,7 +12,7 @@ export async function runDnsCheck() {
order,
code,
title,
description: _('Checking dns, please wait'),
description: _('Checking, please wait'),
state: 'loading',
items: [],
});
@@ -23,7 +24,7 @@ export async function runDnsCheck() {
order,
code,
title,
description: _('Cannot receive DNS checks result'),
description: _('Cannot receive checks result'),
state: 'error',
items: [],
});
@@ -45,27 +46,19 @@ export async function runDnsCheck() {
Boolean(data.bootstrap_dns_status) ||
Boolean(data.dns_status);
function getStatus() {
if (allGood) {
return 'success';
}
if (atLeastOneGood) {
return 'warning';
}
return 'error';
}
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
title,
description: _('DNS checks passed'),
state: getStatus(),
description,
state,
items: [
...insertIf<IDiagnosticsChecksItem>(
data.dns_type === 'doh' || data.dns_type === 'dot',
data.dns_type === 'doh' ||
data.dns_type === 'dot' ||
!data.bootstrap_dns_status,
[
{
state: data.bootstrap_dns_status ? 'success' : 'error',

View File

@@ -3,6 +3,7 @@ import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { PodkopShellMethods, RemoteFakeIPMethods } from '../../../methods';
import { IDiagnosticsChecksItem } from '../../../services';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
export async function runFakeIPCheck() {
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.FAKEIP;
@@ -11,7 +12,7 @@ export async function runFakeIPCheck() {
order,
code,
title,
description: _('Checking FakeIP, please wait'),
description: _('Checking, please wait'),
state: 'loading',
items: [],
});
@@ -34,31 +35,7 @@ export async function runFakeIPCheck() {
const atLeastOneGood =
checks.router && checks.browserFakeIP && checks.differentIP;
function getMeta(): {
description: string;
state: 'loading' | 'warning' | 'success' | 'error' | 'skipped';
} {
if (allGood) {
return {
state: 'success',
description: _('FakeIP checks passed'),
};
}
if (atLeastOneGood) {
return {
state: 'warning',
description: _('FakeIP checks partially passed'),
};
}
return {
state: 'error',
description: _('FakeIP checks failed'),
};
}
const { state, description } = getMeta();
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,

View File

@@ -1,6 +1,7 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { RemoteFakeIPMethods, PodkopShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
export async function runNftCheck() {
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.NFT;
@@ -9,7 +10,7 @@ export async function runNftCheck() {
order,
code,
title,
description: _('Checking nftables, please wait'),
description: _('Checking, please wait'),
state: 'loading',
items: [],
});
@@ -24,7 +25,7 @@ export async function runNftCheck() {
order,
code,
title,
description: _('Cannot receive nftables checks result'),
description: _('Cannot receive checks result'),
state: 'error',
items: [],
});
@@ -54,26 +55,14 @@ export async function runNftCheck() {
Boolean(data.rules_proxy_counters) ||
!data.rules_other_mark_exist;
function getStatus() {
if (allGood) {
return 'success';
}
if (atLeastOneGood) {
return 'warning';
}
return 'error';
}
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
title,
description: allGood
? _('Nftables checks passed')
: _('Nftables checks partially passed'),
state: getStatus(),
description,
state,
items: [
{
state: data.table_exist ? 'success' : 'error',

View File

@@ -0,0 +1,132 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { PodkopShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
import { getDashboardSections } from '../../../methods/custom/getDashboardSections';
import { IDiagnosticsChecksItem } from '../../../services';
export async function runSectionsCheck() {
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS;
updateCheckStore({
order,
code,
title,
description: _('Checking, please wait'),
state: 'loading',
items: [],
});
const sections = await getDashboardSections();
if (!sections.success) {
updateCheckStore({
order,
code,
title,
description: _('Cannot receive checks result'),
state: 'error',
items: [],
});
throw new Error('Sections checks failed');
}
const items = (await Promise.all(
sections.data.map(async (section) => {
async function getLatency() {
if (section.withTagSelect) {
const latencyGroup = await PodkopShellMethods.getClashApiGroupLatency(
section.code,
);
const selectedOutbound = section.outbounds.find(
(item) => item.selected,
);
const isUrlTest = selectedOutbound?.type === 'URLTest';
const success = latencyGroup.success && !latencyGroup.data.message;
if (success) {
if (isUrlTest) {
const latency = Object.values(latencyGroup.data)
.map((item) => (item ? `${item}ms` : 'n/a'))
.join(' / ');
return {
success: true,
latency: `[${_('Fastest')}] ${latency}`,
};
}
const selectedProxyDelay =
latencyGroup.data?.[selectedOutbound?.code ?? ''];
if (selectedProxyDelay) {
return {
success: true,
latency: `[${selectedOutbound?.displayName ?? ''}] ${selectedProxyDelay}ms`,
};
}
return {
success: false,
latency: `[${selectedOutbound?.displayName ?? ''}] ${_('Not responding')}`,
};
}
return {
success: false,
latency: _('Not responding'),
};
}
const latencyProxy = await PodkopShellMethods.getClashApiProxyLatency(
section.code,
);
const success = latencyProxy.success && !latencyProxy.data.message;
if (success) {
return {
success: true,
latency: `${latencyProxy.data.delay} ms`,
};
}
return {
success: false,
latency: _('Not responding'),
};
}
const { latency, success } = await getLatency();
return {
state: success ? 'success' : 'error',
key: section.displayName,
value: latency,
};
}),
)) as Array<IDiagnosticsChecksItem>;
const allGood = items.every((item) => item.state === 'success');
const atLeastOneGood = items.some((item) => item.state === 'success');
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
title,
description,
state,
items,
});
if (!atLeastOneGood) {
throw new Error('Sections checks failed');
}
}

View File

@@ -1,6 +1,7 @@
import { DIAGNOSTICS_CHECKS_MAP } from './contstants';
import { PodkopShellMethods } from '../../../methods';
import { updateCheckStore } from './updateCheckStore';
import { getMeta } from '../helpers/getMeta';
export async function runSingBoxCheck() {
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.SINGBOX;
@@ -9,7 +10,7 @@ export async function runSingBoxCheck() {
order,
code,
title,
description: _('Checking sing-box, please wait'),
description: _('Checking, please wait'),
state: 'loading',
items: [],
});
@@ -21,7 +22,7 @@ export async function runSingBoxCheck() {
order,
code,
title,
description: _('Cannot receive Sing-box checks result'),
description: _('Cannot receive checks result'),
state: 'error',
items: [],
});
@@ -47,24 +48,14 @@ export async function runSingBoxCheck() {
Boolean(data.sing_box_process_running) ||
Boolean(data.sing_box_ports_listening);
function getStatus() {
if (allGood) {
return 'success';
}
if (atLeastOneGood) {
return 'warning';
}
return 'error';
}
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
title,
description: _('Sing-box checks passed'),
state: getStatus(),
description,
state,
items: [
{
state: data.sing_box_installed ? 'success' : 'error',
@@ -73,7 +64,7 @@ export async function runSingBoxCheck() {
},
{
state: data.sing_box_version_ok ? 'success' : 'error',
key: _('Sing-box version >= 1.12.4'),
key: _('Sing-box version is compatible (newer than 1.12.4)'),
value: '',
},
{

View File

@@ -72,6 +72,14 @@ export const initialDiagnosticStore: Pick<
items: [],
state: 'skipped',
},
{
code: DIAGNOSTICS_CHECKS.OUTBOUNDS,
title: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.title,
order: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.order,
description: _('Not running'),
items: [],
state: 'skipped',
},
{
code: DIAGNOSTICS_CHECKS.FAKEIP,
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
@@ -92,7 +100,7 @@ export const loadingDiagnosticsChecksStore: Pick<
code: DIAGNOSTICS_CHECKS.DNS,
title: DIAGNOSTICS_CHECKS_MAP.DNS.title,
order: DIAGNOSTICS_CHECKS_MAP.DNS.order,
description: _('Queued'),
description: _('Pending'),
items: [],
state: 'skipped',
},
@@ -100,7 +108,7 @@ export const loadingDiagnosticsChecksStore: Pick<
code: DIAGNOSTICS_CHECKS.SINGBOX,
title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title,
order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order,
description: _('Queued'),
description: _('Pending'),
items: [],
state: 'skipped',
},
@@ -108,7 +116,15 @@ export const loadingDiagnosticsChecksStore: Pick<
code: DIAGNOSTICS_CHECKS.NFT,
title: DIAGNOSTICS_CHECKS_MAP.NFT.title,
order: DIAGNOSTICS_CHECKS_MAP.NFT.order,
description: _('Queued'),
description: _('Pending'),
items: [],
state: 'skipped',
},
{
code: DIAGNOSTICS_CHECKS.OUTBOUNDS,
title: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.title,
order: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.order,
description: _('Pending'),
items: [],
state: 'skipped',
},
@@ -116,7 +132,7 @@ export const loadingDiagnosticsChecksStore: Pick<
code: DIAGNOSTICS_CHECKS.FAKEIP,
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order,
description: _('Queued'),
description: _('Pending'),
items: [],
state: 'skipped',
},

View File

@@ -0,0 +1,3 @@
export function getCheckTitle(name: string) {
return `${name} ${_('checks')}`;
}

View File

@@ -0,0 +1,28 @@
interface IGetMetaProps {
allGood: boolean;
atLeastOneGood: boolean;
}
export function getMeta({ allGood, atLeastOneGood }: IGetMetaProps): {
description: string;
state: 'loading' | 'warning' | 'success' | 'error' | 'skipped';
} {
if (allGood) {
return {
state: 'success',
description: _('Checks passed'),
};
}
if (atLeastOneGood) {
return {
state: 'warning',
description: _('Issues detected'),
};
}
return {
state: 'error',
description: _('Checks failed'),
};
}

View File

@@ -16,6 +16,10 @@ import { PodkopShellMethods } from '../../methods';
import { fetchServicesInfo } from '../../fetchers';
import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion';
import { renderModal } from '../../../partials';
import { PODKOP_LUCI_APP_VERSION } from '../../../constants';
import { showToast } from '../../../helpers/showToast';
import { renderWikiDisclaimer } from './partials/renderWikiDisclaimer';
import { runSectionsCheck } from './checks/runSectionsCheck';
async function fetchSystemInfo() {
const systemInfo = await PodkopShellMethods.getSystemInfo();
@@ -218,9 +222,13 @@ async function handleShowGlobalCheck() {
_('Global check'),
renderModal(globalCheck.data as string, 'global_check'),
);
} else {
logger.error('[DIAGNOSTIC]', 'handleShowGlobalCheck - e', globalCheck);
showToast(_('Failed to execute!'), 'error');
}
} catch (e) {
logger.error('[DIAGNOSTIC]', 'handleShowGlobalCheck - e', e);
showToast(_('Failed to execute!'), 'error');
} finally {
store.set({
diagnosticsActions: {
@@ -248,9 +256,13 @@ async function handleViewLogs() {
_('View logs'),
renderModal(viewLogs.data as string, 'view_logs'),
);
} else {
logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', viewLogs);
showToast(_('Failed to execute!'), 'error');
}
} catch (e) {
logger.error('[DIAGNOSTIC]', 'handleViewLogs - e', e);
showToast(_('Failed to execute!'), 'error');
} finally {
store.set({
diagnosticsActions: {
@@ -278,9 +290,17 @@ async function handleShowSingBoxConfig() {
_('Show sing-box config'),
renderModal(showSingBoxConfig.data as string, 'show_sing_box_config'),
);
} else {
logger.error(
'[DIAGNOSTIC]',
'handleShowSingBoxConfig - e',
showSingBoxConfig,
);
showToast(_('Failed to execute!'), 'error');
}
} catch (e) {
logger.error('[DIAGNOSTIC]', 'handleShowSingBoxConfig - e', e);
showToast(_('Failed to execute!'), 'error');
} finally {
store.set({
diagnosticsActions: {
@@ -291,6 +311,30 @@ async function handleShowSingBoxConfig() {
}
}
function renderWikiDisclaimerWidget() {
const diagnosticsChecks = store.get().diagnosticsChecks;
function getWikiKind() {
const allResults = diagnosticsChecks.map((check) => check.state);
if (allResults.includes('error')) {
return 'error';
}
if (allResults.includes('warning')) {
return 'warning';
}
return 'default';
}
const container = document.getElementById('pdk_diagnostic-page-wiki');
return preserveScrollForPage(() => {
container!.replaceChildren(renderWikiDisclaimer(getWikiKind()));
});
}
function renderDiagnosticAvailableActionsWidget() {
const diagnosticsActions = store.get().diagnosticsActions;
const servicesInfoWidget = store.get().servicesInfoWidget;
@@ -371,9 +415,9 @@ function renderDiagnosticSystemInfoWidget() {
function getPodkopVersionRow(): IRenderSystemInfoRow {
const loading = diagnosticsSystemInfo.loading;
const unknown = diagnosticsSystemInfo.podkop_version === _('unknown');
const hasActualVersion = Boolean(
diagnosticsSystemInfo.podkop_latest_version,
);
const hasActualVersion =
Boolean(diagnosticsSystemInfo.podkop_latest_version) &&
diagnosticsSystemInfo.podkop_latest_version !== 'unknown';
const version = normalizeCompiledVersion(
diagnosticsSystemInfo.podkop_version,
);
@@ -387,6 +431,11 @@ function renderDiagnosticSystemInfoWidget() {
}
if (version !== `v${diagnosticsSystemInfo.podkop_latest_version}`) {
logger.debug(
'[DIAGNOSTIC]',
'diagnosticsSystemInfo',
diagnosticsSystemInfo,
);
return {
key: 'Podkop',
value: version,
@@ -412,7 +461,7 @@ function renderDiagnosticSystemInfoWidget() {
getPodkopVersionRow(),
{
key: 'Luci App',
value: normalizeCompiledVersion(diagnosticsSystemInfo.luci_app_version),
value: normalizeCompiledVersion(PODKOP_LUCI_APP_VERSION),
},
{
key: 'Sing-box',
@@ -441,6 +490,7 @@ async function onStoreUpdate(
) {
if (diff.diagnosticsChecks) {
renderDiagnosticsChecks();
renderWikiDisclaimerWidget();
}
if (diff.diagnosticsRunAction) {
@@ -469,6 +519,8 @@ async function runChecks() {
await runNftCheck();
await runSectionsCheck();
await runFakeIPCheck();
} catch (e) {
logger.error('[DIAGNOSTIC]', 'runChecks - e', e);
@@ -496,6 +548,9 @@ function onPageMount() {
// Initial system info render
renderDiagnosticSystemInfoWidget();
// Initial Wiki disclaimer render
renderWikiDisclaimerWidget();
// Initial services info fetch
fetchServicesInfo();

View File

@@ -40,7 +40,7 @@ export function renderAvailableActions({
showSingBoxConfig,
}: IRenderAvailableActionsProps) {
return E('div', { class: 'pdk_diagnostic-page__right-bar__actions' }, [
E('b', {}, 'Available actions'),
E('b', {}, _('Available actions')),
...insertIf(restart.visible, [
renderButton({
classNames: ['cbi-button-apply'],

View File

@@ -18,7 +18,7 @@ export function renderSystemInfo({ items }: IRenderSystemInfoProps) {
E(
'b',
{ class: 'pdk_diagnostic-page__right-bar__system-info__title' },
'System information',
_('System information'),
),
...items.map((item) => {
const tagClass = [

View File

@@ -0,0 +1,40 @@
import { renderBookOpenTextIcon24 } from '../../../../icons';
import { renderButton } from '../../../../partials';
import { insertIf } from '../../../../helpers';
export function renderWikiDisclaimer(kind: 'default' | 'error' | 'warning') {
const iconWrap = E('span', {
class: 'pdk_diagnostic-page__right-bar__wiki__icon',
});
iconWrap.appendChild(renderBookOpenTextIcon24());
const className = [
'pdk_diagnostic-page__right-bar__wiki',
...insertIf(kind === 'error', [
'pdk_diagnostic-page__right-bar__wiki--error',
]),
...insertIf(kind === 'warning', [
'pdk_diagnostic-page__right-bar__wiki--warning',
]),
].join(' ');
return E('div', { class: className }, [
E('div', { class: 'pdk_diagnostic-page__right-bar__wiki__content' }, [
iconWrap,
E('div', { class: 'pdk_diagnostic-page__right-bar__wiki__texts' }, [
E('b', {}, _('Troubleshooting')),
E('div', {}, _('Do not panic, everything can be fixed, just...')),
]),
]),
renderButton({
classNames: ['cbi-button-save'],
text: _('Visit Wiki'),
onClick: () =>
window.open(
'https://podkop.net/docs/troubleshooting/?utm_source=podkop',
'_blank',
'noopener,noreferrer',
),
}),
]);
}

View File

@@ -8,6 +8,7 @@ export function render() {
}),
]),
E('div', { class: 'pdk_diagnostic-page__right-bar' }, [
E('div', { id: 'pdk_diagnostic-page-wiki' }),
E('div', { id: 'pdk_diagnostic-page-actions' }),
E('div', { id: 'pdk_diagnostic-page-system-info' }),
]),

View File

@@ -28,6 +28,31 @@ export const styles = `
grid-row-gap: 10px;
}
.pdk_diagnostic-page__right-bar__wiki {
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
padding: 10px;
display: grid;
grid-template-columns: auto;
grid-row-gap: 10px;
}
.pdk_diagnostic-page__right-bar__wiki--warning {
border: 2px var(--warn-color-medium, orange) solid;
}
.pdk_diagnostic-page__right-bar__wiki--error {
border: 2px var(--error-color-medium, red) solid;
}
.pdk_diagnostic-page__right-bar__wiki__content {
display: grid;
grid-template-columns: 1fr 5fr;
grid-column-gap: 10px;
}
.pdk_diagnostic-page__right-bar__wiki__texts {}
.pdk_diagnostic-page__right-bar__actions {
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;

View File

@@ -126,6 +126,7 @@ export namespace Podkop {
export type ConfigSection = ConfigBaseSection & {
'.name': string;
'.type': 'settings' | 'section';
yacd_secret_key?: string;
};
export interface MethodSuccessResponse<T> {
@@ -196,4 +197,11 @@ export namespace Podkop {
openwrt_version: string;
device_model: string;
}
export interface GetClashApiProxyLatency {
delay: number;
message?: string;
}
export type GetClashApiGroupLatency = Record<string, number>;
}

View File

@@ -3,7 +3,18 @@ import { validateDNS } from '../validateDns.js';
import { invalidIPs, validIPs } from './validateIp.test';
import { invalidDomains, validDomains } from './validateDomain.test';
const validDns = [...validIPs, ...validDomains];
export const additionalValidDns = [
['Google DNS (port 53)', '8.8.8.8:53'],
['Google DNS (port 5353)', '8.8.8.8:5353'],
['Cloudflare DNS (port 853)', '1.1.1.1:853'],
['Cloudflare domain (port 853)', 'cloudflare-dns.com:853'],
['DoH IP', '1.1.1.1/dns-query'],
['DoH IP with port 443', '1.1.1.1:443/dns-query'],
['DoH domain', 'cloudflare-dns.com/dns-query'],
['DoH domain with port 443', 'cloudflare-dns.com:443/dns-query'],
];
const validDns = [...validIPs, ...validDomains, ...additionalValidDns];
const invalidDns = [...invalidIPs, ...invalidDomains];

View File

@@ -0,0 +1,74 @@
import { describe, it, expect } from 'vitest';
import { validateHysteria2Url } from '../validateHysteriaUrl.js';
const validUrls = [
// Basic password-only
['password basic', 'hysteria2://pass@example.com:443/#hy2-basic'],
// insecure=1
[
'insecure allowed',
'hysteria2://pass@example.com:443/?insecure=1#hy2-insecure',
],
// SNI
['SNI param', 'hysteria2://pass@example.com:443/?sni=google.com#hy2-sni'],
// Obfuscation
[
'Obfs + password',
'hysteria2://mypassword@1.1.1.1:8443/?obfs=salamander&obfs-password=abc123#hy2-obfs',
],
// All params
[
'All options combined',
'hysteria2://pw@8.8.8.8:8443/?sni=example.com&obfs=salamander&obfs-password=hello&insecure=1#hy2-full',
],
// Explicit obfs=none (valid)
['obfs none = ok', 'hysteria2://pw@example.com:443/?obfs=none#hy2-none'],
];
const invalidUrls = [
['No prefix', 'pw@example.com:443'],
['Missing password', 'hysteria2://@example.com:443/'],
['Missing host', 'hysteria2://pw@:443/'],
['Missing port', 'hysteria2://pw@example.com/'],
['Non-numeric port', 'hysteria2://pw@example.com:port/'],
['Port out of range', 'hysteria2://pw@example.com:99999/'],
// Obfuscation errors
['Unknown obfs type', 'hysteria2://pw@example.com:443/?obfs=weird'],
[
'obfs without obfs-password',
'hysteria2://pw@example.com:443/?obfs=salamander',
],
// insecure only accepts 0/1
['invalid insecure', 'hysteria2://pw@example.com:443/?insecure=5'],
// SNI empty
['empty sni', 'hysteria2://pw@example.com:443/?sni='],
];
describe('validateHysteria2Url', () => {
describe.each(validUrls)('Valid HY2 URL: %s', (_desc, url) => {
it(`returns valid=true for "${url}"`, () => {
const res = validateHysteria2Url(url);
expect(res.valid).toBe(true);
});
});
describe.each(invalidUrls)('Invalid HY2 URL: %s', (_desc, url) => {
it(`returns valid=false for "${url}"`, () => {
const res = validateHysteria2Url(url);
expect(res.valid).toBe(false);
});
});
it('detects invalid port range', () => {
const res = validateHysteria2Url('hysteria2://pw@example.com:70000/');
expect(res.valid).toBe(false);
});
});

View File

@@ -16,6 +16,7 @@ const invalidUrls = [
['Unsupported protocol (ftp)', 'ftp://example.com'],
['Unsupported protocol (ws)', 'ws://example.com'],
['Empty string', ''],
['Without tld', 'https://google'],
];
describe('validateUrl', () => {

View File

@@ -7,11 +7,14 @@ export function validateDNS(value: string): ValidationResult {
return { valid: false, message: _('DNS server address cannot be empty') };
}
if (validateIPV4(value).valid) {
const cleanedValueWithoutPort = value.replace(/:(\d+)(?=\/|$)/, '');
const cleanedIpWithoutPath = cleanedValueWithoutPort.split('/')[0];
if (validateIPV4(cleanedIpWithoutPath).valid) {
return { valid: true, message: _('Valid') };
}
if (validateDomain(value).valid) {
if (validateDomain(cleanedValueWithoutPort).valid) {
return { valid: true, message: _('Valid') };
}

View File

@@ -0,0 +1,117 @@
import { ValidationResult } from './types';
import { parseQueryString } from '../helpers/parseQueryString';
export function validateHysteria2Url(url: string): ValidationResult {
try {
const isHY2 = url.startsWith('hysteria2://');
const isHY2Short = url.startsWith('hy2://');
if (!isHY2 && !isHY2Short) {
return {
valid: false,
message: _('Invalid HY2 URL: must start with hysteria2:// or hy2://'),
};
}
if (/\s/.test(url)) {
return {
valid: false,
message: _('Invalid HY2 URL: must not contain spaces'),
};
}
const prefix = isHY2 ? 'hysteria2://' : 'hy2://';
const body = url.slice(prefix.length);
const [mainPart] = body.split('#');
const [authHostPort, queryString] = mainPart.split('?');
if (!authHostPort)
return {
valid: false,
message: _('Invalid HY2 URL: missing credentials/server'),
};
const [passwordPart, hostPortPart] = authHostPort.split('@');
if (!passwordPart)
return { valid: false, message: _('Invalid HY2 URL: missing password') };
if (!hostPortPart)
return {
valid: false,
message: _('Invalid HY2 URL: missing host & port'),
};
const [host, port] = hostPortPart.split(':');
if (!host) {
return { valid: false, message: _('Invalid HY2 URL: missing host') };
}
if (!port) {
return { valid: false, message: _('Invalid HY2 URL: missing port') };
}
const cleanedPort = port.replace('/', '');
const portNum = Number(cleanedPort);
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
return {
valid: false,
message: _('Invalid HY2 URL: invalid port number'),
};
}
if (queryString) {
const params = parseQueryString(queryString);
const paramsKeys = Object.keys(params);
if (
paramsKeys.includes('insecure') &&
!['0', '1'].includes(params.insecure)
) {
return {
valid: false,
message: _('Invalid HY2 URL: insecure must be 0 or 1'),
};
}
const validObfsTypes = ['none', 'salamander'];
if (
paramsKeys.includes('obfs') &&
!validObfsTypes.includes(params.obfs)
) {
return {
valid: false,
message: _('Invalid HY2 URL: unsupported obfs type'),
};
}
if (
paramsKeys.includes('obfs') &&
params.obfs !== 'none' &&
!params['obfs-password']
) {
return {
valid: false,
message: _(
'Invalid HY2 URL: obfs-password required when obfs is set',
),
};
}
if (paramsKeys.includes('sni') && !params.sni) {
return {
valid: false,
message: _('Invalid HY2 URL: sni cannot be empty'),
};
}
}
return { valid: true, message: _('Valid') };
} catch (_e) {
return { valid: false, message: _('Invalid HY2 URL: parsing failed') };
}
}

View File

@@ -1,18 +1,8 @@
import { ValidationResult } from './types';
// TODO refactor current validation and add tests
export function validateOutboundJson(value: string): ValidationResult {
try {
const parsed = JSON.parse(value);
if (!parsed.type || !parsed.server || !parsed.server_port) {
return {
valid: false,
message: _(
'Outbound JSON must contain at least "type", "server" and "server_port" fields',
),
};
}
JSON.parse(value);
return { valid: true, message: _('Valid') };
} catch {

View File

@@ -3,29 +3,39 @@ import { validateShadowsocksUrl } from './validateShadowsocksUrl';
import { validateVlessUrl } from './validateVlessUrl';
import { validateTrojanUrl } from './validateTrojanUrl';
import { validateSocksUrl } from './validateSocksUrl';
import { validateHysteria2Url } from './validateHysteriaUrl';
// TODO refactor current validation and add tests
export function validateProxyUrl(url: string): ValidationResult {
if (url.startsWith('ss://')) {
return validateShadowsocksUrl(url);
const trimmedUrl = url.trim();
if (trimmedUrl.startsWith('ss://')) {
return validateShadowsocksUrl(trimmedUrl);
}
if (url.startsWith('vless://')) {
return validateVlessUrl(url);
if (trimmedUrl.startsWith('vless://')) {
return validateVlessUrl(trimmedUrl);
}
if (url.startsWith('trojan://')) {
return validateTrojanUrl(url);
if (trimmedUrl.startsWith('trojan://')) {
return validateTrojanUrl(trimmedUrl);
}
if (/^socks(4|4a|5):\/\//.test(url)) {
return validateSocksUrl(url);
if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) {
return validateSocksUrl(trimmedUrl);
}
if (
trimmedUrl.startsWith('hysteria2://') ||
trimmedUrl.startsWith('hy2://')
) {
return validateHysteria2Url(trimmedUrl);
}
return {
valid: false,
message: _(
'URL must start with vless://, ss://, trojan://, or socks4/5://',
'URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://',
),
};
}

View File

@@ -2,19 +2,31 @@ import { ValidationResult } from './types';
export function validateUrl(
url: string,
protocols: string[] = ['http:', 'https:'],
protocols = ['http:', 'https:'],
): ValidationResult {
try {
const parsedUrl = new URL(url);
if (!protocols.includes(parsedUrl.protocol)) {
return {
valid: false,
message: `${_('URL must use one of the following protocols:')} ${protocols.join(', ')}`,
};
}
return { valid: true, message: _('Valid') };
} catch (_e) {
if (!url.length) {
return { valid: false, message: _('Invalid URL format') };
}
const hasValidProtocol = protocols.some((p) => url.indexOf(p + '//') === 0);
if (!hasValidProtocol)
return {
valid: false,
message:
_('URL must use one of the following protocols:') +
' ' +
protocols.join(', '),
};
const regex = new RegExp(
`^(?:${protocols.map((p) => p.replace(':', '')).join('|')})://` +
`(?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,}(?::\\d+)?(?:/[^\\s]*)?$`,
);
if (regex.test(url)) {
return { valid: true, message: _('Valid') };
}
return { valid: false, message: _('Invalid URL format') };
}

View File

@@ -43,7 +43,8 @@ export function validateVlessUrl(url: string): ValidationResult {
if (!port)
return { valid: false, message: 'Invalid VLESS URL: missing port' };
const portNum = Number(port);
const cleanedPort = port.replace('/', '');
const portNum = Number(cleanedPort);
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
return {
valid: false,

View File

@@ -90,6 +90,7 @@ update_config() {
mv /etc/config/podkop /etc/config/podkop-070
wget -O /etc/config/podkop https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/podkop/files/etc/config/podkop
msg "Podkop config has been reset to default. Your old config saved in /etc/config/podkop-070"
break
;;
*)
msg "Exit"
@@ -108,16 +109,16 @@ main() {
pkg_list_update || { echo "Packages list update failed"; exit 1; }
if [ -f "/etc/init.d/podkop" ]; then
msg "Podkop is already installed. Upgraded..."
msg "Podkop is already installed. Upgrading..."
else
msg "Installed podkop..."
msg "Installing podkop..."
fi
if command -v curl >/dev/null 2>&1; then
check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest")
if echo "$check_response" | grep -q 'API rate limit '; then
msg "You've reached rate limit from GitHub. Repeat in five minutes."
msg "You've reached the GitHub rate limit. Repeat in five minutes."
exit 1
fi
fi
@@ -142,7 +143,7 @@ main() {
break
fi
fi
msg "Download error $filename. Retry..."
msg "Download error for $filename. Retrying..."
rm -f "$filepath"
attempt=$((attempt+1))
done
@@ -167,7 +168,7 @@ main() {
fi
done
if [ -n "$file" ]; then
msg "Installing $file"
msg "Installing $file..."
pkg_install "$DOWNLOAD_DIR/$file"
sleep 3
fi
@@ -182,11 +183,11 @@ main() {
done
if [ -n "$ru" ]; then
if pkg_is_installed luci-i18n-podkop-ru; then
msg "Upgraded ru translation..."
msg "Upgrading Russian translation..."
pkg_remove luci-i18n-podkop*
pkg_install "$DOWNLOAD_DIR/$ru"
else
msg "Русский язык интерфейса ставим? y/n (Need a Russian translation?)"
msg "Русский язык интерфейса ставим? y/n (Install the Russian interface language?)"
while true; do
read -r -p '' RUS
case $RUS in
@@ -235,7 +236,7 @@ check_system() {
fi
if ! nslookup google.com >/dev/null 2>&1; then
msg "DNS not working"
msg "DNS is not working."
exit 1
fi
@@ -269,7 +270,7 @@ check_system() {
fi
if pkg_is_installed https-dns-proxy; then
msg "Сonflicting package detected: https-dns-proxy. Remove?"
msg "Conflicting package detected: https-dns-proxy. Remove?"
while true; do
read -r -p '' DNSPROXY
@@ -299,7 +300,7 @@ sing_box() {
required_version="1.12.4"
if [ "$(printf '%s\n%s\n' "$sing_box_version" "$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
msg "sing-box version $sing_box_version is older than required $required_version"
msg "sing-box version $sing_box_version is older than the required version $required_version."
msg "Removing old version..."
service podkop stop
pkg_remove sing-box

View File

@@ -40,10 +40,12 @@ function validateDNS(value) {
if (!value) {
return { valid: false, message: _("DNS server address cannot be empty") };
}
if (validateIPV4(value).valid) {
const cleanedValueWithoutPort = value.replace(/:(\d+)(?=\/|$)/, "");
const cleanedIpWithoutPath = cleanedValueWithoutPort.split("/")[0];
if (validateIPV4(cleanedIpWithoutPath).valid) {
return { valid: true, message: _("Valid") };
}
if (validateDomain(value).valid) {
if (validateDomain(cleanedValueWithoutPort).valid) {
return { valid: true, message: _("Valid") };
}
return {
@@ -56,18 +58,22 @@ function validateDNS(value) {
// src/validators/validateUrl.ts
function validateUrl(url, protocols = ["http:", "https:"]) {
try {
const parsedUrl = new URL(url);
if (!protocols.includes(parsedUrl.protocol)) {
return {
valid: false,
message: `${_("URL must use one of the following protocols:")} ${protocols.join(", ")}`
};
}
return { valid: true, message: _("Valid") };
} catch (_e) {
if (!url.length) {
return { valid: false, message: _("Invalid URL format") };
}
const hasValidProtocol = protocols.some((p) => url.indexOf(p + "//") === 0);
if (!hasValidProtocol)
return {
valid: false,
message: _("URL must use one of the following protocols:") + " " + protocols.join(", ")
};
const regex = new RegExp(
`^(?:${protocols.map((p) => p.replace(":", "")).join("|")})://(?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,}(?::\\d+)?(?:/[^\\s]*)?$`
);
if (regex.test(url)) {
return { valid: true, message: _("Valid") };
}
return { valid: false, message: _("Invalid URL format") };
}
// src/validators/validatePath.ts
@@ -259,7 +265,8 @@ function validateVlessUrl(url) {
return { valid: false, message: "Invalid VLESS URL: missing hostname" };
if (!port)
return { valid: false, message: "Invalid VLESS URL: missing port" };
const portNum = Number(port);
const cleanedPort = port.replace("/", "");
const portNum = Number(cleanedPort);
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535)
return {
valid: false,
@@ -320,15 +327,7 @@ function validateVlessUrl(url) {
// src/validators/validateOutboundJson.ts
function validateOutboundJson(value) {
try {
const parsed = JSON.parse(value);
if (!parsed.type || !parsed.server || !parsed.server_port) {
return {
valid: false,
message: _(
'Outbound JSON must contain at least "type", "server" and "server_port" fields'
)
};
}
JSON.parse(value);
return { valid: true, message: _("Valid") };
} catch {
return { valid: false, message: _("Invalid JSON format") };
@@ -449,24 +448,114 @@ function validateSocksUrl(url) {
return { valid: true, message: _("Valid") };
}
// src/validators/validateHysteriaUrl.ts
function validateHysteria2Url(url) {
try {
const isHY2 = url.startsWith("hysteria2://");
const isHY2Short = url.startsWith("hy2://");
if (!isHY2 && !isHY2Short) {
return {
valid: false,
message: _("Invalid HY2 URL: must start with hysteria2:// or hy2://")
};
}
if (/\s/.test(url)) {
return {
valid: false,
message: _("Invalid HY2 URL: must not contain spaces")
};
}
const prefix = isHY2 ? "hysteria2://" : "hy2://";
const body = url.slice(prefix.length);
const [mainPart] = body.split("#");
const [authHostPort, queryString] = mainPart.split("?");
if (!authHostPort)
return {
valid: false,
message: _("Invalid HY2 URL: missing credentials/server")
};
const [passwordPart, hostPortPart] = authHostPort.split("@");
if (!passwordPart)
return { valid: false, message: _("Invalid HY2 URL: missing password") };
if (!hostPortPart)
return {
valid: false,
message: _("Invalid HY2 URL: missing host & port")
};
const [host, port] = hostPortPart.split(":");
if (!host) {
return { valid: false, message: _("Invalid HY2 URL: missing host") };
}
if (!port) {
return { valid: false, message: _("Invalid HY2 URL: missing port") };
}
const cleanedPort = port.replace("/", "");
const portNum = Number(cleanedPort);
if (!Number.isInteger(portNum) || portNum < 1 || portNum > 65535) {
return {
valid: false,
message: _("Invalid HY2 URL: invalid port number")
};
}
if (queryString) {
const params = parseQueryString(queryString);
const paramsKeys = Object.keys(params);
if (paramsKeys.includes("insecure") && !["0", "1"].includes(params.insecure)) {
return {
valid: false,
message: _("Invalid HY2 URL: insecure must be 0 or 1")
};
}
const validObfsTypes = ["none", "salamander"];
if (paramsKeys.includes("obfs") && !validObfsTypes.includes(params.obfs)) {
return {
valid: false,
message: _("Invalid HY2 URL: unsupported obfs type")
};
}
if (paramsKeys.includes("obfs") && params.obfs !== "none" && !params["obfs-password"]) {
return {
valid: false,
message: _(
"Invalid HY2 URL: obfs-password required when obfs is set"
)
};
}
if (paramsKeys.includes("sni") && !params.sni) {
return {
valid: false,
message: _("Invalid HY2 URL: sni cannot be empty")
};
}
}
return { valid: true, message: _("Valid") };
} catch (_e) {
return { valid: false, message: _("Invalid HY2 URL: parsing failed") };
}
}
// src/validators/validateProxyUrl.ts
function validateProxyUrl(url) {
if (url.startsWith("ss://")) {
return validateShadowsocksUrl(url);
const trimmedUrl = url.trim();
if (trimmedUrl.startsWith("ss://")) {
return validateShadowsocksUrl(trimmedUrl);
}
if (url.startsWith("vless://")) {
return validateVlessUrl(url);
if (trimmedUrl.startsWith("vless://")) {
return validateVlessUrl(trimmedUrl);
}
if (url.startsWith("trojan://")) {
return validateTrojanUrl(url);
if (trimmedUrl.startsWith("trojan://")) {
return validateTrojanUrl(trimmedUrl);
}
if (/^socks(4|4a|5):\/\//.test(url)) {
return validateSocksUrl(url);
if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) {
return validateSocksUrl(trimmedUrl);
}
if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) {
return validateHysteria2Url(trimmedUrl);
}
return {
valid: false,
message: _(
"URL must start with vless://, ss://, trojan://, or socks4/5://"
"URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://"
)
};
}
@@ -486,7 +575,7 @@ async function callBaseMethod(method, args = [], command = "/usr/bin/podkop") {
const response = await executeShellCommand({
command,
args: [method, ...args],
timeout: 1e4
timeout: 15e3
});
if (response.stdout) {
try {
@@ -559,14 +648,14 @@ var PodkopShellMethods = {
getClashApiProxies: async () => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [
Podkop.AvailableClashAPIMethods.GET_PROXIES
]),
getClashApiProxyLatency: async (tag) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [
Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY,
tag
]),
getClashApiGroupLatency: async (tag) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [
Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY,
tag
]),
getClashApiProxyLatency: async (tag) => callBaseMethod(
Podkop.AvailableMethods.CLASH_API,
[Podkop.AvailableClashAPIMethods.GET_PROXY_LATENCY, tag, "5000"]
),
getClashApiGroupLatency: async (tag) => callBaseMethod(
Podkop.AvailableMethods.CLASH_API,
[Podkop.AvailableClashAPIMethods.GET_GROUP_LATENCY, tag, "10000"]
),
setClashApiGroupProxy: async (group, proxy) => callBaseMethod(Podkop.AvailableMethods.CLASH_API, [
Podkop.AvailableClashAPIMethods.SET_GROUP_PROXY,
group,
@@ -731,10 +820,18 @@ async function getDashboardSections() {
};
}
// src/podkop/methods/custom/getClashApiSecret.ts
async function getClashApiSecret() {
const sections = await getConfigSections();
const settings = sections.find((section) => section[".type"] === "settings");
return settings?.yacd_secret_key || "";
}
// src/podkop/methods/custom/index.ts
var CustomPodkopMethods = {
getConfigSections,
getDashboardSections
getDashboardSections,
getClashApiSecret
};
// src/constants.ts
@@ -978,26 +1075,36 @@ var TabService = class _TabService {
};
var TabServiceInstance = TabService.getInstance();
// src/podkop/tabs/diagnostic/helpers/getCheckTitle.ts
function getCheckTitle(name) {
return `${name} ${_("checks")}`;
}
// src/podkop/tabs/diagnostic/checks/contstants.ts
var DIAGNOSTICS_CHECKS_MAP = {
["DNS" /* DNS */]: {
order: 1,
title: _("DNS checks"),
title: getCheckTitle("DNS"),
code: "DNS" /* DNS */
},
["SINGBOX" /* SINGBOX */]: {
order: 2,
title: _("Sing-box checks"),
title: getCheckTitle("Sing-box"),
code: "SINGBOX" /* SINGBOX */
},
["NFT" /* NFT */]: {
order: 3,
title: _("Nftables checks"),
title: getCheckTitle("Nftables"),
code: "NFT" /* NFT */
},
["FAKEIP" /* FAKEIP */]: {
["OUTBOUNDS" /* OUTBOUNDS */]: {
order: 4,
title: _("FakeIP checks"),
title: getCheckTitle("Outbounds"),
code: "OUTBOUNDS" /* OUTBOUNDS */
},
["FAKEIP" /* FAKEIP */]: {
order: 5,
title: getCheckTitle("FakeIP"),
code: "FAKEIP" /* FAKEIP */
}
};
@@ -1065,6 +1172,14 @@ var initialDiagnosticStore = {
items: [],
state: "skipped"
},
{
code: "OUTBOUNDS" /* OUTBOUNDS */,
title: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.title,
order: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.order,
description: _("Not running"),
items: [],
state: "skipped"
},
{
code: "FAKEIP" /* FAKEIP */,
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
@@ -1081,7 +1196,7 @@ var loadingDiagnosticsChecksStore = {
code: "DNS" /* DNS */,
title: DIAGNOSTICS_CHECKS_MAP.DNS.title,
order: DIAGNOSTICS_CHECKS_MAP.DNS.order,
description: _("Queued"),
description: _("Pending"),
items: [],
state: "skipped"
},
@@ -1089,7 +1204,7 @@ var loadingDiagnosticsChecksStore = {
code: "SINGBOX" /* SINGBOX */,
title: DIAGNOSTICS_CHECKS_MAP.SINGBOX.title,
order: DIAGNOSTICS_CHECKS_MAP.SINGBOX.order,
description: _("Queued"),
description: _("Pending"),
items: [],
state: "skipped"
},
@@ -1097,7 +1212,15 @@ var loadingDiagnosticsChecksStore = {
code: "NFT" /* NFT */,
title: DIAGNOSTICS_CHECKS_MAP.NFT.title,
order: DIAGNOSTICS_CHECKS_MAP.NFT.order,
description: _("Queued"),
description: _("Pending"),
items: [],
state: "skipped"
},
{
code: "OUTBOUNDS" /* OUTBOUNDS */,
title: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.title,
order: DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS.order,
description: _("Pending"),
items: [],
state: "skipped"
},
@@ -1105,7 +1228,7 @@ var loadingDiagnosticsChecksStore = {
code: "FAKEIP" /* FAKEIP */,
title: DIAGNOSTICS_CHECKS_MAP.FAKEIP.title,
order: DIAGNOSTICS_CHECKS_MAP.FAKEIP.order,
description: _("Queued"),
description: _("Pending"),
items: [],
state: "skipped"
}
@@ -1850,8 +1973,9 @@ async function fetchDashboardSections() {
});
}
async function connectToClashSockets() {
const clashApiSecret = await getClashApiSecret();
socket.subscribe(
`${getClashWsUrl()}/traffic?token=`,
`${getClashWsUrl()}/traffic?token=${clashApiSecret}`,
(msg) => {
const parsedMsg = JSON.parse(msg);
store.set({
@@ -1878,7 +2002,7 @@ async function connectToClashSockets() {
}
);
socket.subscribe(
`${getClashWsUrl()}/connections?token=`,
`${getClashWsUrl()}/connections?token=${clashApiSecret}`,
(msg) => {
const parsedMsg = JSON.parse(msg);
store.set({
@@ -2335,6 +2459,7 @@ function render2() {
})
]),
E("div", { class: "pdk_diagnostic-page__right-bar" }, [
E("div", { id: "pdk_diagnostic-page-wiki" }),
E("div", { id: "pdk_diagnostic-page-actions" }),
E("div", { id: "pdk_diagnostic-page-system-info" })
])
@@ -2355,6 +2480,26 @@ function updateCheckStore(check, minified) {
});
}
// src/podkop/tabs/diagnostic/helpers/getMeta.ts
function getMeta({ allGood, atLeastOneGood }) {
if (allGood) {
return {
state: "success",
description: _("Checks passed")
};
}
if (atLeastOneGood) {
return {
state: "warning",
description: _("Issues detected")
};
}
return {
state: "error",
description: _("Checks failed")
};
}
// src/podkop/tabs/diagnostic/checks/runDnsCheck.ts
async function runDnsCheck() {
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.DNS;
@@ -2362,7 +2507,7 @@ async function runDnsCheck() {
order,
code,
title,
description: _("Checking dns, please wait"),
description: _("Checking, please wait"),
state: "loading",
items: []
});
@@ -2372,7 +2517,7 @@ async function runDnsCheck() {
order,
code,
title,
description: _("Cannot receive DNS checks result"),
description: _("Cannot receive checks result"),
state: "error",
items: []
});
@@ -2381,24 +2526,16 @@ async function runDnsCheck() {
const data = dnsChecks.data;
const allGood = Boolean(data.dns_on_router) && Boolean(data.dhcp_config_status) && Boolean(data.bootstrap_dns_status) && Boolean(data.dns_status);
const atLeastOneGood = Boolean(data.dns_on_router) || Boolean(data.dhcp_config_status) || Boolean(data.bootstrap_dns_status) || Boolean(data.dns_status);
function getStatus() {
if (allGood) {
return "success";
}
if (atLeastOneGood) {
return "warning";
}
return "error";
}
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
title,
description: _("DNS checks passed"),
state: getStatus(),
description,
state,
items: [
...insertIf(
data.dns_type === "doh" || data.dns_type === "dot",
data.dns_type === "doh" || data.dns_type === "dot" || !data.bootstrap_dns_status,
[
{
state: data.bootstrap_dns_status ? "success" : "error",
@@ -2436,7 +2573,7 @@ async function runSingBoxCheck() {
order,
code,
title,
description: _("Checking sing-box, please wait"),
description: _("Checking, please wait"),
state: "loading",
items: []
});
@@ -2446,7 +2583,7 @@ async function runSingBoxCheck() {
order,
code,
title,
description: _("Cannot receive Sing-box checks result"),
description: _("Cannot receive checks result"),
state: "error",
items: []
});
@@ -2455,21 +2592,13 @@ async function runSingBoxCheck() {
const data = singBoxChecks.data;
const allGood = Boolean(data.sing_box_installed) && Boolean(data.sing_box_version_ok) && Boolean(data.sing_box_service_exist) && Boolean(data.sing_box_autostart_disabled) && Boolean(data.sing_box_process_running) && Boolean(data.sing_box_ports_listening);
const atLeastOneGood = Boolean(data.sing_box_installed) || Boolean(data.sing_box_version_ok) || Boolean(data.sing_box_service_exist) || Boolean(data.sing_box_autostart_disabled) || Boolean(data.sing_box_process_running) || Boolean(data.sing_box_ports_listening);
function getStatus() {
if (allGood) {
return "success";
}
if (atLeastOneGood) {
return "warning";
}
return "error";
}
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
title,
description: _("Sing-box checks passed"),
state: getStatus(),
description,
state,
items: [
{
state: data.sing_box_installed ? "success" : "error",
@@ -2478,7 +2607,7 @@ async function runSingBoxCheck() {
},
{
state: data.sing_box_version_ok ? "success" : "error",
key: _("Sing-box version >= 1.12.4"),
key: _("Sing-box version is compatible (newer than 1.12.4)"),
value: ""
},
{
@@ -2515,7 +2644,7 @@ async function runNftCheck() {
order,
code,
title,
description: _("Checking nftables, please wait"),
description: _("Checking, please wait"),
state: "loading",
items: []
});
@@ -2527,7 +2656,7 @@ async function runNftCheck() {
order,
code,
title,
description: _("Cannot receive nftables checks result"),
description: _("Cannot receive checks result"),
state: "error",
items: []
});
@@ -2536,21 +2665,13 @@ async function runNftCheck() {
const data = nftablesChecks.data;
const allGood = Boolean(data.table_exist) && Boolean(data.rules_mangle_exist) && Boolean(data.rules_mangle_counters) && Boolean(data.rules_mangle_output_exist) && Boolean(data.rules_mangle_output_counters) && Boolean(data.rules_proxy_exist) && Boolean(data.rules_proxy_counters) && !data.rules_other_mark_exist;
const atLeastOneGood = Boolean(data.table_exist) || Boolean(data.rules_mangle_exist) || Boolean(data.rules_mangle_counters) || Boolean(data.rules_mangle_output_exist) || Boolean(data.rules_mangle_output_counters) || Boolean(data.rules_proxy_exist) || Boolean(data.rules_proxy_counters) || !data.rules_other_mark_exist;
function getStatus() {
if (allGood) {
return "success";
}
if (atLeastOneGood) {
return "warning";
}
return "error";
}
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
title,
description: allGood ? _("Nftables checks passed") : _("Nftables checks partially passed"),
state: getStatus(),
description,
state,
items: [
{
state: data.table_exist ? "success" : "error",
@@ -2606,7 +2727,7 @@ async function runFakeIPCheck() {
order,
code,
title,
description: _("Checking FakeIP, please wait"),
description: _("Checking, please wait"),
state: "loading",
items: []
});
@@ -2620,25 +2741,7 @@ async function runFakeIPCheck() {
};
const allGood = checks.router || checks.browserFakeIP || checks.differentIP;
const atLeastOneGood = checks.router && checks.browserFakeIP && checks.differentIP;
function getMeta() {
if (allGood) {
return {
state: "success",
description: _("FakeIP checks passed")
};
}
if (atLeastOneGood) {
return {
state: "warning",
description: _("FakeIP checks partially passed")
};
}
return {
state: "error",
description: _("FakeIP checks failed")
};
}
const { state, description } = getMeta();
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
@@ -3211,6 +3314,34 @@ function renderSearchIcon24() {
);
}
// src/icons/renderBookOpenTextIcon24.ts
function renderBookOpenTextIcon24() {
const NS = "http://www.w3.org/2000/svg";
return svgEl(
"svg",
{
xmlns: NS,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "lucide lucide-book-open-text-icon lucide-book-open-text"
},
[
svgEl("path", { d: "M12 7v14" }),
svgEl("path", { d: "M16 12h2" }),
svgEl("path", { d: "M16 8h2" }),
svgEl("path", {
d: "M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"
}),
svgEl("path", { d: "M6 12h2" }),
svgEl("path", { d: "M6 8h2" })
]
);
}
// src/partials/button/renderButton.ts
function renderButton({
classNames = [],
@@ -3341,7 +3472,7 @@ function renderAvailableActions({
showSingBoxConfig
}) {
return E("div", { class: "pdk_diagnostic-page__right-bar__actions" }, [
E("b", {}, "Available actions"),
E("b", {}, _("Available actions")),
...insertIf(restart.visible, [
renderButton({
classNames: ["cbi-button-apply"],
@@ -3599,7 +3730,7 @@ function renderSystemInfo({ items }) {
E(
"b",
{ class: "pdk_diagnostic-page__right-bar__system-info__title" },
"System information"
_("System information")
),
...items.map((item) => {
const tagClass = [
@@ -3634,6 +3765,140 @@ function normalizeCompiledVersion(version) {
return version;
}
// src/podkop/tabs/diagnostic/partials/renderWikiDisclaimer.ts
function renderWikiDisclaimer(kind) {
const iconWrap = E("span", {
class: "pdk_diagnostic-page__right-bar__wiki__icon"
});
iconWrap.appendChild(renderBookOpenTextIcon24());
const className = [
"pdk_diagnostic-page__right-bar__wiki",
...insertIf(kind === "error", [
"pdk_diagnostic-page__right-bar__wiki--error"
]),
...insertIf(kind === "warning", [
"pdk_diagnostic-page__right-bar__wiki--warning"
])
].join(" ");
return E("div", { class: className }, [
E("div", { class: "pdk_diagnostic-page__right-bar__wiki__content" }, [
iconWrap,
E("div", { class: "pdk_diagnostic-page__right-bar__wiki__texts" }, [
E("b", {}, _("Troubleshooting")),
E("div", {}, _("Do not panic, everything can be fixed, just..."))
])
]),
renderButton({
classNames: ["cbi-button-save"],
text: _("Visit Wiki"),
onClick: () => window.open(
"https://podkop.net/docs/troubleshooting/?utm_source=podkop",
"_blank",
"noopener,noreferrer"
)
})
]);
}
// src/podkop/tabs/diagnostic/checks/runSectionsCheck.ts
async function runSectionsCheck() {
const { order, title, code } = DIAGNOSTICS_CHECKS_MAP.OUTBOUNDS;
updateCheckStore({
order,
code,
title,
description: _("Checking, please wait"),
state: "loading",
items: []
});
const sections = await getDashboardSections();
if (!sections.success) {
updateCheckStore({
order,
code,
title,
description: _("Cannot receive checks result"),
state: "error",
items: []
});
throw new Error("Sections checks failed");
}
const items = await Promise.all(
sections.data.map(async (section) => {
async function getLatency() {
if (section.withTagSelect) {
const latencyGroup = await PodkopShellMethods.getClashApiGroupLatency(
section.code
);
const selectedOutbound = section.outbounds.find(
(item) => item.selected
);
const isUrlTest = selectedOutbound?.type === "URLTest";
const success3 = latencyGroup.success && !latencyGroup.data.message;
if (success3) {
if (isUrlTest) {
const latency2 = Object.values(latencyGroup.data).map((item) => item ? `${item}ms` : "n/a").join(" / ");
return {
success: true,
latency: `[${_("Fastest")}] ${latency2}`
};
}
const selectedProxyDelay = latencyGroup.data?.[selectedOutbound?.code ?? ""];
if (selectedProxyDelay) {
return {
success: true,
latency: `[${selectedOutbound?.displayName ?? ""}] ${selectedProxyDelay}ms`
};
}
return {
success: false,
latency: `[${selectedOutbound?.displayName ?? ""}] ${_("Not responding")}`
};
}
return {
success: false,
latency: _("Not responding")
};
}
const latencyProxy = await PodkopShellMethods.getClashApiProxyLatency(
section.code
);
const success2 = latencyProxy.success && !latencyProxy.data.message;
if (success2) {
return {
success: true,
latency: `${latencyProxy.data.delay} ms`
};
}
return {
success: false,
latency: _("Not responding")
};
}
const { latency, success } = await getLatency();
return {
state: success ? "success" : "error",
key: section.displayName,
value: latency
};
})
);
const allGood = items.every((item) => item.state === "success");
const atLeastOneGood = items.some((item) => item.state === "success");
const { state, description } = getMeta({ atLeastOneGood, allGood });
updateCheckStore({
order,
code,
title,
description,
state,
items
});
if (!atLeastOneGood) {
throw new Error("Sections checks failed");
}
}
// src/podkop/tabs/diagnostic/initController.ts
async function fetchSystemInfo() {
const systemInfo = await PodkopShellMethods.getSystemInfo();
@@ -3813,9 +4078,13 @@ async function handleShowGlobalCheck() {
_("Global check"),
renderModal(globalCheck.data, "global_check")
);
} else {
logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", globalCheck);
showToast(_("Failed to execute!"), "error");
}
} catch (e) {
logger.error("[DIAGNOSTIC]", "handleShowGlobalCheck - e", e);
showToast(_("Failed to execute!"), "error");
} finally {
store.set({
diagnosticsActions: {
@@ -3840,9 +4109,13 @@ async function handleViewLogs() {
_("View logs"),
renderModal(viewLogs.data, "view_logs")
);
} else {
logger.error("[DIAGNOSTIC]", "handleViewLogs - e", viewLogs);
showToast(_("Failed to execute!"), "error");
}
} catch (e) {
logger.error("[DIAGNOSTIC]", "handleViewLogs - e", e);
showToast(_("Failed to execute!"), "error");
} finally {
store.set({
diagnosticsActions: {
@@ -3867,9 +4140,17 @@ async function handleShowSingBoxConfig() {
_("Show sing-box config"),
renderModal(showSingBoxConfig.data, "show_sing_box_config")
);
} else {
logger.error(
"[DIAGNOSTIC]",
"handleShowSingBoxConfig - e",
showSingBoxConfig
);
showToast(_("Failed to execute!"), "error");
}
} catch (e) {
logger.error("[DIAGNOSTIC]", "handleShowSingBoxConfig - e", e);
showToast(_("Failed to execute!"), "error");
} finally {
store.set({
diagnosticsActions: {
@@ -3879,6 +4160,23 @@ async function handleShowSingBoxConfig() {
});
}
}
function renderWikiDisclaimerWidget() {
const diagnosticsChecks = store.get().diagnosticsChecks;
function getWikiKind() {
const allResults = diagnosticsChecks.map((check) => check.state);
if (allResults.includes("error")) {
return "error";
}
if (allResults.includes("warning")) {
return "warning";
}
return "default";
}
const container = document.getElementById("pdk_diagnostic-page-wiki");
return preserveScrollForPage(() => {
container.replaceChildren(renderWikiDisclaimer(getWikiKind()));
});
}
function renderDiagnosticAvailableActionsWidget() {
const diagnosticsActions = store.get().diagnosticsActions;
const servicesInfoWidget = store.get().servicesInfoWidget;
@@ -3948,9 +4246,7 @@ function renderDiagnosticSystemInfoWidget() {
function getPodkopVersionRow() {
const loading = diagnosticsSystemInfo.loading;
const unknown = diagnosticsSystemInfo.podkop_version === _("unknown");
const hasActualVersion = Boolean(
diagnosticsSystemInfo.podkop_latest_version
);
const hasActualVersion = Boolean(diagnosticsSystemInfo.podkop_latest_version) && diagnosticsSystemInfo.podkop_latest_version !== "unknown";
const version = normalizeCompiledVersion(
diagnosticsSystemInfo.podkop_version
);
@@ -3962,6 +4258,11 @@ function renderDiagnosticSystemInfoWidget() {
};
}
if (version !== `v${diagnosticsSystemInfo.podkop_latest_version}`) {
logger.debug(
"[DIAGNOSTIC]",
"diagnosticsSystemInfo",
diagnosticsSystemInfo
);
return {
key: "Podkop",
value: version,
@@ -3985,7 +4286,7 @@ function renderDiagnosticSystemInfoWidget() {
getPodkopVersionRow(),
{
key: "Luci App",
value: normalizeCompiledVersion(diagnosticsSystemInfo.luci_app_version)
value: normalizeCompiledVersion(PODKOP_LUCI_APP_VERSION)
},
{
key: "Sing-box",
@@ -4008,6 +4309,7 @@ function renderDiagnosticSystemInfoWidget() {
async function onStoreUpdate2(next, prev, diff) {
if (diff.diagnosticsChecks) {
renderDiagnosticsChecks();
renderWikiDisclaimerWidget();
}
if (diff.diagnosticsRunAction) {
renderDiagnosticRunActionWidget();
@@ -4028,6 +4330,7 @@ async function runChecks() {
await runDnsCheck();
await runSingBoxCheck();
await runNftCheck();
await runSectionsCheck();
await runFakeIPCheck();
} catch (e) {
logger.error("[DIAGNOSTIC]", "runChecks - e", e);
@@ -4042,6 +4345,7 @@ function onPageMount2() {
renderDiagnosticRunActionWidget();
renderDiagnosticAvailableActionsWidget();
renderDiagnosticSystemInfoWidget();
renderWikiDisclaimerWidget();
fetchServicesInfo();
fetchSystemInfo();
}
@@ -4120,6 +4424,31 @@ var styles4 = `
grid-row-gap: 10px;
}
.pdk_diagnostic-page__right-bar__wiki {
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
padding: 10px;
display: grid;
grid-template-columns: auto;
grid-row-gap: 10px;
}
.pdk_diagnostic-page__right-bar__wiki--warning {
border: 2px var(--warn-color-medium, orange) solid;
}
.pdk_diagnostic-page__right-bar__wiki--error {
border: 2px var(--error-color-medium, red) solid;
}
.pdk_diagnostic-page__right-bar__wiki__content {
display: grid;
grid-template-columns: 1fr 5fr;
grid-column-gap: 10px;
}
.pdk_diagnostic-page__right-bar__wiki__texts {}
.pdk_diagnostic-page__right-bar__actions {
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
@@ -4508,6 +4837,10 @@ function insertIf(condition, elements) {
function insertIfObj(condition, object) {
return condition ? object : {};
}
// src/main.ts
if (typeof structuredClone !== "function")
globalThis.structuredClone = (obj) => JSON.parse(JSON.stringify(obj));
return baseclass.extend({
ALLOWED_WITH_RUSSIA_INSIDE,
BOOTSTRAP_DNS_SERVER_OPTIONS,

View File

@@ -87,7 +87,7 @@ function createSectionContent(section) {
_("URLTest Proxy Links"),
);
o.depends("proxy_config_type", "urltest");
o.placeholder = "vless://, ss://, trojan://, socks4/5:// links";
o.placeholder = "vless://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links";
o.rmempty = false;
o.validate = function (section_id, value) {
// Optional
@@ -104,6 +104,70 @@ function createSectionContent(section) {
return validation.message;
};
o = section.option(
form.ListValue,
"urltest_check_interval",
_("URLTest Check Interval"),
_("The interval between connectivity tests")
);
o.value("30s", _("Every 30 seconds"));
o.value("1m", _("Every 1 minute"));
o.value("3m", _("Every 3 minutes"));
o.value("5m", _("Every 5 minutes"));
o.default = "3m";
o.depends("proxy_config_type", "urltest");
o = section.option(
form.Value,
"urltest_tolerance",
_("URLTest Tolerance"),
_("The maximum difference in response times (ms) allowed when comparing servers")
);
o.default = "50";
o.rmempty = false;
o.depends("proxy_config_type", "urltest");
o.validate = function (section_id, value) {
if (!value || value.length === 0) {
return true;
}
const parsed = parseFloat(value);
if (/^[0-9]+$/.test(value) && !isNaN(parsed) && isFinite(parsed) && parsed >= 50 && parsed <= 1000) {
return true;
}
return _('Must be a number in the range of 50 - 1000');
};
o = section.option(
form.Value,
"urltest_testing_url",
_("URLTest Testing URL"),
_("The URL used to test server connectivity")
);
o.value("https://www.gstatic.com/generate_204", "https://www.gstatic.com/generate_204 (Google)");
o.value("https://cp.cloudflare.com/generate_204", "https://cp.cloudflare.com/generate_204 (Cloudflare)");
o.value("https://captive.apple.com", "https://captive.apple.com (Apple)");
o.value("https://connectivity-check.ubuntu.com", "https://connectivity-check.ubuntu.com (Ubuntu)")
o.default = "https://www.gstatic.com/generate_204";
o.rmempty = false;
o.depends("proxy_config_type", "urltest");
o.validate = function (section_id, value) {
if (!value || value.length === 0) {
return true;
}
const validation = main.validateUrl(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = section.option(
form.Flag,
"enable_udp_over_tcp",
@@ -241,7 +305,7 @@ function createSectionContent(section) {
(v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v),
);
notifications.push(
E("p", { class: "alert-message warning" }, [
E("p", {}, [
E("strong", {}, _("Regional options cannot be used together")),
E("br"),
_(
@@ -381,7 +445,7 @@ function createSectionContent(section) {
);
o.value("disabled", _("Disabled"));
o.value("dynamic", _("Dynamic List"));
o.value("text", _("Text List (comma/space/newline separated)"));
o.value("text", _("Text List"));
o.default = "disabled";
o.rmempty = false;

View File

@@ -240,6 +240,25 @@ function createSettingsContent(section) {
o.default = "0";
o.rmempty = false;
o = section.option(
form.Flag,
"enable_yacd_wan_access",
_("Enable YACD WAN Access"),
_("Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."),
);
o.depends("enable_yacd", "1");
o.default = "0";
o.rmempty = false;
o = section.option(
form.Value,
"yacd_secret_key",
_("YACD Secret Key"),
_("Secret key for authenticating remote access to YACD when WAN access is enabled."),
);
o.depends("enable_yacd_wan_access", "1");
o.rmempty = false;
o = section.option(
form.Flag,
"disable_quic",
@@ -267,7 +286,7 @@ function createSettingsContent(section) {
form.Flag,
"download_lists_via_proxy",
_("Download Lists via Proxy/VPN"),
_("Downloading all lists via main Proxy/VPN"),
_("Downloading all lists via specific Proxy/VPN"),
);
o.default = "0";
o.rmempty = false;

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PODKOP\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-21 23:02+0300\n"
"PO-Revision-Date: 2025-10-21 23:02+0300\n"
"POT-Creation-Date: 2025-12-01 16:30+0200\n"
"PO-Revision-Date: 2025-12-01 16:30+0200\n"
"Last-Translator: divocat\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -35,6 +35,9 @@ msgstr "Активные соединения"
msgid "Additional marking rules found"
msgstr "Найдены дополнительные правила маркировки"
msgid "Allows access to YACD from the WAN. Make sure to open the appropriate port in your firewall."
msgstr "Обеспечивает доступ к YACD из WAN. Убедитесь, что в брандмауэре открыт соответствующий порт."
msgid "Applicable for SOCKS and Shadowsocks proxy"
msgstr "Применимо для SOCKS и Shadowsocks прокси"
@@ -44,6 +47,9 @@ msgstr "Необходимо указать хотя бы один действ
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы."
msgid "Available actions"
msgstr "Доступные действия"
msgid "Bootsrap DNS"
msgstr "Bootstrap DNS"
@@ -62,26 +68,20 @@ msgstr "Путь к файлу кэша"
msgid "Cache file path cannot be empty"
msgstr "Путь к файлу кэша не может быть пустым"
msgid "Cannot receive DNS checks result"
msgstr "Не удалось получить результаты проверки DNS"
msgid "Cannot receive checks result"
msgstr "Не удалось получить результаты проверки"
msgid "Cannot receive nftables checks result"
msgstr "Не удалось получить результаты проверки nftables"
msgid "Checking, please wait"
msgstr "Проверяем, пожалуйста подождите"
msgid "Cannot receive Sing-box checks result"
msgstr "Не удалось получить результаты проверки Sing-box"
msgid "checks"
msgstr "проверки"
msgid "Checking dns, please wait"
msgstr "Проверка dns, пожалуйста подождите"
msgid "Checks failed"
msgstr "Проверки не выполнены"
msgid "Checking FakeIP, please wait"
msgstr "Проверка FakeIP, пожалуйста подождите"
msgid "Checking nftables, please wait"
msgstr "Проверка nftables, пожалуйста подождите"
msgid "Checking sing-box, please wait"
msgstr "Проверка sing-box, пожалуйста подождите"
msgid "Checks passed"
msgstr "Проверки пройдены"
msgid "CIDR must be between 0 and 32"
msgstr "CIDR должен быть между 0 и 32"
@@ -143,12 +143,6 @@ msgstr "Отключить QUIC протокол для улучшения со
msgid "Disabled"
msgstr "Отключено"
msgid "DNS checks"
msgstr "DNS проверки"
msgid "DNS checks passed"
msgstr "DNS проверки успешно завершены"
msgid "DNS on router"
msgstr "DNS на роутере"
@@ -170,6 +164,9 @@ msgstr "DNS-сервер"
msgid "DNS server address cannot be empty"
msgstr "Адрес DNS-сервера не может быть пустым"
msgid "Do not panic, everything can be fixed, just..."
msgstr "Не паникуйте, всё можно исправить, просто..."
msgid "Domain Resolver"
msgstr "Резолвер доменов"
@@ -188,9 +185,6 @@ msgstr "Скачивать списки через Proxy/VPN"
msgid "Download Lists via specific proxy section"
msgstr "Скачивать списки через выбранную секцию"
msgid "Downloading all lists via main Proxy/VPN"
msgstr "Загрузка всех списков через основной прокси/VPN"
msgid "Downloading all lists via specific Proxy/VPN"
msgstr "Загрузка всех списков через указанный прокси/VPN"
@@ -215,6 +209,9 @@ msgstr "Включить смешанный прокси-сервер, разр
msgid "Enable YACD"
msgstr "Включить YACD"
msgid "Enable YACD WAN Access"
msgstr "Включить доступ YACD WAN"
msgid "Enter complete outbound configuration in JSON format"
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
@@ -227,6 +224,18 @@ msgstr "Введите доменные имена без протоколов,
msgid "Enter subnets in CIDR notation (e.g. 103.21.244.0/22) or single IP addresses"
msgstr "Введите подсети в нотации CIDR (например, 103.21.244.0/22) или отдельные IP-адреса"
msgid "Every 1 minute"
msgstr "Каждую минуту"
msgid "Every 3 minutes"
msgstr "Каждые 3 минуты"
msgid "Every 30 seconds"
msgstr "Каждые 30 секунд"
msgid "Every 5 minutes"
msgstr "Каждые 5 минут"
msgid "Exclude NTP"
msgstr "Исключить NTP"
@@ -236,17 +245,8 @@ msgstr "Исключите трафик протокола NTP из туннел
msgid "Failed to copy!"
msgstr "Не удалось скопировать!"
msgid "FakeIP checks"
msgstr "Проверка FakeIP"
msgid "FakeIP checks failed"
msgstr "Проверки FakeIP не пройдены"
msgid "FakeIP checks partially passed"
msgstr "Проверка FakeIP частично пройдена"
msgid "FakeIP checks passed"
msgstr "Проверки FakeIP пройдены"
msgid "Failed to execute!"
msgstr "Не удалось выполнить!"
msgid "Fastest"
msgstr "Самый быстрый"
@@ -281,6 +281,45 @@ msgstr "Неверный домен"
msgid "Invalid format. Use X.X.X.X or X.X.X.X/Y"
msgstr "Неверный формат. Используйте X.X.X.X или X.X.X.X/Y"
msgid "Invalid HY2 URL: insecure must be 0 or 1"
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
msgid "Invalid HY2 URL: invalid port number"
msgstr "Неверный URL Hysteria2: неверный номер порта"
msgid "Invalid HY2 URL: missing credentials/server"
msgstr "Неверный URL Hysteria2: отсутствуют учетные данные/сервер"
msgid "Invalid HY2 URL: missing host"
msgstr "Неверный URL Hysteria2: отсутствует хост"
msgid "Invalid HY2 URL: missing host & port"
msgstr "Неверный URL Hysteria2: отсутствуют хост и порт"
msgid "Invalid HY2 URL: missing password"
msgstr "Неверный URL Hysteria2: отсутствует пароль"
msgid "Invalid HY2 URL: missing port"
msgstr "Неверный URL Hysteria2: отсутствует порт"
msgid "Invalid HY2 URL: must not contain spaces"
msgstr "Неверный URL Hysteria2: не должен содержать пробелов"
msgid "Invalid HY2 URL: must start with hysteria2:// or hy2://"
msgstr "Неверный URL Hysteria2: должен начинаться с hysteria2:// или hy2://"
msgid "Invalid HY2 URL: obfs-password required when obfs is set"
msgstr "Неверный URL Hysteria2: требуется obfs-password, когда установлен obfs"
msgid "Invalid HY2 URL: parsing failed"
msgstr "Неверный URL Hysteria2: ошибка разбора"
msgid "Invalid HY2 URL: sni cannot be empty"
msgstr "Неверный URL Hysteria2: sni не может быть пустым"
msgid "Invalid HY2 URL: unsupported obfs type"
msgstr "Неверный URL Hysteria2: неподдерживаемый тип obfs"
msgid "Invalid IP address"
msgstr "Неверный IP-адрес"
@@ -365,6 +404,9 @@ msgstr "Неверный URL VLESS: ошибка разбора"
msgid "IP address 0.0.0.0 is not allowed"
msgstr "IP-адрес 0.0.0.0 не допускается"
msgid "Issues detected"
msgstr "Обнаружены проблемы"
msgid "Latest"
msgstr "Последняя"
@@ -389,24 +431,21 @@ msgstr "Порт смешанного прокси"
msgid "Monitored Interfaces"
msgstr "Наблюдаемые интерфейсы"
msgid "Must be a number in the range of 50 - 1000"
msgstr "Должно быть числом от 50 до 1000"
msgid "Network Interface"
msgstr "Сетевой интерфейс"
msgid "Nftables checks"
msgstr "Проверки Nftables"
msgid "Nftables checks partially passed"
msgstr "Проверки Nftables частично пройдена"
msgid "Nftables checks passed"
msgstr "Nftables проверки успешно завершены"
msgid "No other marking rules found"
msgstr "Другие правила маркировки не найдены"
msgid "Not implement yet"
msgstr "Ещё не реализовано"
msgid "Not responding"
msgstr "Не отвечает"
msgid "Not running"
msgstr "Не запущено"
@@ -419,9 +458,6 @@ msgstr "Конфигурация Outbound"
msgid "Outbound Configuration"
msgstr "Конфигурация исходящего соединения"
msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields"
msgstr "JSON должен содержать поля \"type\", \"server\" и \"server_port\""
msgid "Outdated"
msgstr "Устаревшая"
@@ -440,6 +476,9 @@ msgstr "Путь должен содержать хотя бы одну дире
msgid "Path must end with cache.db"
msgstr "Путь должен заканчиваться на cache.db"
msgid "Pending"
msgstr "Ожидает запуска"
msgid "Podkop"
msgstr "Podkop"
@@ -458,17 +497,14 @@ msgstr "Прокси-трафик не маршрутизируется чере
msgid "Proxy traffic is routed via FakeIP"
msgstr "Прокси-трафик направляется через FakeIP"
msgid "Queued"
msgstr "В очереди"
msgid "Regional options cannot be used together"
msgstr "Нельзя использовать несколько региональных опций одновременно"
msgid "Remote Domain Lists"
msgstr "Удалённые списки доменов"
msgstr "Внешние списки доменов"
msgid "Remote Subnet Lists"
msgstr "Удалённые списки подсетей"
msgstr "Внешние списки подсетей"
msgid "Restart podkop"
msgstr "Перезапустить Podkop"
@@ -506,6 +542,9 @@ msgstr "Запустить диагностику"
msgid "Russia inside restrictions"
msgstr "Ограничения Russia inside"
msgid "Secret key for authenticating remote access to YACD when WAN access is enabled."
msgstr "Секретный ключ для аутентификации удаленного доступа к YACD при включенном доступе через WAN."
msgid "Sections"
msgstr "Секции"
@@ -569,12 +608,6 @@ msgstr "Sing-box"
msgid "Sing-box autostart disabled"
msgstr "Автостарт sing-box отключен"
msgid "Sing-box checks"
msgstr "Sing-box проверки"
msgid "Sing-box checks passed"
msgstr "Sing-box проверки успешно завершены"
msgid "Sing-box installed"
msgstr "Sing-box установлен"
@@ -587,8 +620,8 @@ msgstr "Процесс sing-box запущен"
msgid "Sing-box service exist"
msgstr "Сервис sing-box существует"
msgid "Sing-box version >= 1.12.4"
msgstr "Версия sing-box >= 1.12.4"
msgid "Sing-box version is compatible (newer than 1.12.4)"
msgstr "Версия Sing-box совместима (новее 1.12.4)"
msgid "Source Network Interface"
msgstr "Сетевой интерфейс источника"
@@ -600,10 +633,10 @@ msgid "Specify local IP addresses or subnets whose traffic will always be routed
msgstr "Укажите локальные IP-адреса или подсети, трафик которых всегда будет направляться через настроенный маршрут."
msgid "Specify remote URLs to download and use domain lists"
msgstr "Укажите удаленные URL-адреса для загрузки и использования списков доменов."
msgstr "Укажите URL-адреса для загрузки и использования списков доменов."
msgid "Specify remote URLs to download and use subnet lists"
msgstr "Укажите удаленные URL-адреса для загрузки и использования списков подсетей."
msgstr "Укажите URL-адреса для загрузки и использования списков подсетей."
msgid "Specify the path to the list file located on the router filesystem"
msgstr "Укажите путь к файлу списка, расположенному в файловой системе маршрутизатора."
@@ -620,21 +653,30 @@ msgstr "Успешно скопировано!"
msgid "System info"
msgstr "Системная информация"
msgid "System information"
msgstr "Системная информация"
msgid "Table exist"
msgstr "Таблица существует"
msgid "Test latency"
msgstr "Измерить задержки"
msgstr "Тестирование задержки"
msgid "Text List"
msgstr "Текстовый список"
msgid "Text List (comma/space/newline separated)"
msgstr "Текстовый список (через запятую, пробел или новую строку)"
msgid "The DNS server used to look up the IP address of an upstream DNS server"
msgstr "DNS-сервер, используемый для поиска IP-адреса вышестоящего DNS-сервера"
msgid "The interval between connectivity tests"
msgstr "Интервал между тестами подключения"
msgid "The maximum difference in response times (ms) allowed when comparing servers"
msgstr "Максимально допустимая разница во времени отклика (мс) при сравнении серверов"
msgid "The URL used to test server connectivity"
msgstr "URL-адрес, используемый для проверки подключения к серверу"
msgid "Time in seconds for DNS record caching (default: 60)"
msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 60)"
@@ -644,6 +686,9 @@ msgstr "Трафик"
msgid "Traffic Total"
msgstr "Всего трафика"
msgid "Troubleshooting"
msgstr "Устранение неполадок"
msgid "TTL must be a positive number"
msgstr "TTL должно быть положительным числом"
@@ -665,8 +710,8 @@ msgstr "Неизвестная ошибка"
msgid "Uplink"
msgstr "Исходящий"
msgid "URL must start with vless://, ss://, trojan://, or socks4/5://"
msgstr "URL должен начинаться с vless://, ss://, trojan:// или socks4/5://"
msgid "URL must start with vless://, ss://, trojan://, socks4/5://, or hysteria2://hy2://"
msgstr "URL должен начинаться с vless://, ss://, trojan://, socks4/5:// или hysteria2:// hy2://"
msgid "URL must use one of the following protocols:"
msgstr "URL должен использовать один из следующих протоколов:"
@@ -674,9 +719,18 @@ msgstr "URL должен использовать один из следующи
msgid "URLTest"
msgstr "URLTest"
msgid "URLTest Check Interval"
msgstr "Интервал проверки URLTest"
msgid "URLTest Proxy Links"
msgstr "Ссылки прокси для URLTest"
msgid "URLTest Testing URL"
msgstr "URLTest ссылка для проверки"
msgid "URLTest Tolerance"
msgstr "URLTest допустимое отклонение"
msgid "User Domain List Type"
msgstr "Тип пользовательского списка доменов"
@@ -704,11 +758,17 @@ msgstr "Ошибки валидации:"
msgid "View logs"
msgstr "Посмотреть логи"
msgid "Visit Wiki"
msgstr "Перейти в wiki"
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
msgid "Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection."
msgstr "Предупреждение: Russia inside может быть использован только с %s. %s уже есть в Russia inside и будет удален из выбранных."
msgid "YACD Secret Key"
msgstr "Секретный ключ YACD"
msgid "You can select Output Network Interface, by default autodetect"
msgstr "Вы можете выбрать выходной сетевой интерфейс, по умолчанию он определяется автоматически."

File diff suppressed because it is too large Load Diff

View File

@@ -18,6 +18,7 @@ check_required_file "$PODKOP_LIB/helpers.sh"
check_required_file "$PODKOP_LIB/sing_box_config_manager.sh"
check_required_file "$PODKOP_LIB/sing_box_config_facade.sh"
check_required_file "$PODKOP_LIB/logging.sh"
check_required_file "$PODKOP_LIB/rulesets.sh"
. /lib/config/uci.sh
. /lib/functions.sh
. "$PODKOP_LIB/constants.sh"
@@ -26,6 +27,7 @@ check_required_file "$PODKOP_LIB/logging.sh"
. "$PODKOP_LIB/sing_box_config_manager.sh"
. "$PODKOP_LIB/sing_box_config_facade.sh"
. "$PODKOP_LIB/logging.sh"
. "$PODKOP_LIB/rulesets.sh"
config_load "$PODKOP_CONFIG"
@@ -123,36 +125,19 @@ start_main() {
# base
route_table_rule_mark
create_nft_table
sing_box_uci
create_nft_rules
sing_box_configure_service
# sing-box
sing_box_init_config
config_foreach add_cron_job "section"
/etc/init.d/sing-box start
local exclude_ntp
config_get_bool exclude_ntp "settings" "exclude_ntp" "0"
if [ "$exclude_ntp" -eq 1 ]; then
log "NTP traffic exclude for proxy"
nft insert rule inet "$NFT_TABLE_NAME" mangle udp dport 123 return
fi
log "Nice"
list_update &
echo $! > /var/run/podkop_list_update.pid
}
start() {
start_main
config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_add_resolver
fi
uci_set "podkop" "settings" "shutdown_correctly" 0
uci commit "podkop" && config_load "$PODKOP_CONFIG"
}
stop_main() {
log "Stopping the podkop"
@@ -188,13 +173,27 @@ stop_main() {
/etc/init.d/sing-box stop
}
start() {
start_main
config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_configure
fi
uci_set "podkop" "settings" "shutdown_correctly" 0
uci commit "podkop" && config_load "$PODKOP_CONFIG"
}
stop() {
local dont_touch_dhcp
config_get_bool dont_touch_dhcp "settings" "dont_touch_dhcp" 0
if [ "$dont_touch_dhcp" -eq 0 ]; then
dnsmasq_restore
fi
stop_main
uci_set "podkop" "settings" "shutdown_correctly" 1
uci commit "podkop" && config_load "$PODKOP_CONFIG"
}
@@ -279,12 +278,10 @@ nft_init_interfaces_set() {
done
}
create_nft_table() {
create_nft_rules() {
log "Create nft table"
nft_create_table "$NFT_TABLE_NAME"
nft_init_interfaces_set
log "Create localv4 set"
nft_create_ipv4_set "$NFT_TABLE_NAME" "$NFT_LOCALV4_SET_NAME"
nft add element inet "$NFT_TABLE_NAME" localv4 '{
@@ -326,7 +323,14 @@ create_nft_table() {
nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto tcp meta mark set 0x105 counter
nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "@$NFT_COMMON_SET_NAME" meta l4proto udp meta mark set 0x105 counter
nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set 0x105 counter
nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto tcp meta mark set 0x105 counter
nft add rule inet "$NFT_TABLE_NAME" mangle_output ip daddr "$SB_FAKEIP_INET4_RANGE" meta l4proto udp meta mark set 0x105 counter
local exclude_ntp
config_get_bool exclude_ntp "settings" "exclude_ntp" "0"
if [ "$exclude_ntp" -eq 1 ]; then
log "NTP traffic exclude for proxy"
nft insert rule inet "$NFT_TABLE_NAME" mangle udp dport 123 return
fi
}
backup_dnsmasq_config_option() {
@@ -340,7 +344,7 @@ backup_dnsmasq_config_option() {
fi
}
dnsmasq_add_resolver() {
dnsmasq_configure() {
local shutdown_correctly
config_get shutdown_correctly "settings" "shutdown_correctly"
if [ "$shutdown_correctly" -eq 0 ]; then
@@ -472,42 +476,55 @@ remove_cron_job() {
list_update() {
echolog "🔄 Starting lists update..."
local nslookup_timeout=3
local nslookup_attempts=10
local curl_timeout=5
local curl_attempts=10
local curl_max_timeout=10
local delay=3
local i
for i in $(seq 1 60); do
if nslookup -timeout=1 openwrt.org > /dev/null 2>&1; then
# DNS Check
for i in $(seq 1 $nslookup_timeout); do
if nslookup -timeout=$nslookup_timeout openwrt.org > /dev/null 2>&1; then
echolog "✅ DNS check passed"
break
fi
log "DNS is unavailable [$i/60]"
sleep 3
echolog "DNS is unavailable [$i/$nslookup_attempts]"
sleep $delay
done
if [ "$i" -eq 60 ]; then
echolog "❌ DNS check failed after 60 attempts"
if [ "$i" -eq $nslookup_attempts ]; then
echolog "❌ DNS check failed after $nslookup_attempts attempts"
return 1
fi
for i in $(seq 1 60); do
config_get_bool download_lists_via_proxy "settings" "download_lists_via_proxy" "0"
if [ "$download_lists_via_proxy" -eq 1 ]; then
if http_proxy="http://127.0.0.1:4534" https_proxy="http://127.0.0.1:4534" curl -s -m 3 https://github.com > /dev/null; then
# Github Check
for i in $(seq 1 $curl_attempts); do
local service_proxy_address
service_proxy_address="$(get_service_proxy_address)"
if [ -n "$http_proxy_address" ]; then
if curl -s -x "http://$service_proxy_address" -m $curl_timeout https://github.com > /dev/null; then
echolog "✅ GitHub connection check passed (via proxy)"
break
fi
else
if curl -s -m 3 https://github.com > /dev/null; then
if curl -s -m $curl_timeout https://github.com > /dev/null; then
echolog "✅ GitHub connection check passed"
break
fi
fi
echolog "GitHub is unavailable [$i/60]"
sleep 3
echolog "GitHub is unavailable [$i/$curl_attempts] (max-timeout=$curl_timeout)"
if [ "$curl_timeout" -lt $curl_max_timeout ]; then
curl_timeout=$((curl_timeout + 1))
fi
sleep $delay
done
if [ "$i" -eq 60 ]; then
echolog "❌ GitHub connection check failed after 60 attempts"
if [ "$i" -eq $curl_attempts ]; then
echolog "❌ GitHub connection check failed after $curl_attempts attempts"
return 1
fi
@@ -525,30 +542,30 @@ list_update() {
}
# sing-box funcs
sing_box_uci() {
sing_box_configure_service() {
local sing_box_enabled sing_box_user sing_box_config_path sing_box_conffile
sing_box_enabled=$(uci get "sing-box.main.enabled")
sing_box_user=$(uci get "sing-box.main.user")
sing_box_enabled="$(uci_get "sing-box" "main" "enabled")"
sing_box_user="$(uci_get "sing-box" "main" "user")"
if [ "$sing_box_enabled" -ne 1 ]; then
uci set "sing-box.main.enabled=1"
uci commit "sing-box"
uci_set "sing-box" "main" "enabled" 1
uci_commit "sing-box"
log "sing-box service has been enabled"
fi
if [ "$sing_box_user" != "root" ]; then
uci set "sing-box.main.user=root"
uci commit "sing-box"
uci_set "sing-box" "main" "user" "root"
uci_commit "sing-box"
log "sing-box service user has been changed to root"
fi
config_get sing_box_config_path "settings" "config_path"
sing_box_conffile=$(uci get "sing-box.main.conffile")
sing_box_conffile="$(uci_get "sing-box" "main" "conffile")"
log "sing-box config path: $sing_box_config_path" "debug"
log "sing-box service conffile: $sing_box_conffile" "debug"
if [ "$sing_box_conffile" != "$sing_box_config_path" ]; then
uci set "sing-box.main.conffile=$sing_box_config_path"
uci commit "sing-box"
uci_set "sing-box" "main" "conffile" "$sing_box_config_path"
uci_commit "sing-box"
log "Configuration file path has been set to $sing_box_config_path"
fi
@@ -627,9 +644,12 @@ configure_outbound_handler() {
urltest)
log "Detected proxy configuration type: urltest" "debug"
local urltest_proxy_links udp_over_tcp i urltest_tag selector_tag outbound_tag outbound_tags \
urltest_outbounds selector_outbounds
urltest_outbounds selector_outbounds urltest_check_interval urltest_tolerance urltest_testing_url
config_get urltest_proxy_links "$section" "urltest_proxy_links"
config_get udp_over_tcp "$section" "enable_udp_over_tcp"
config_get urltest_check_interval "$section" "urltest_check_interval" "3m"
config_get urltest_tolerance "$section" "urltest_tolerance" 50
config_get urltest_testing_url "$section" "urltest_testing_url" "https://www.gstatic.com/generate_204"
if [ -z "$urltest_proxy_links" ]; then
log "URLTest proxy links is not set. Aborted." "fatal"
@@ -652,7 +672,8 @@ configure_outbound_handler() {
selector_tag="$(get_outbound_tag_by_section "$section")"
urltest_outbounds="$(comma_string_to_json_array "$outbound_tags")"
selector_outbounds="$(comma_string_to_json_array "$outbound_tags,$urltest_tag")"
config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds")"
config="$(sing_box_cm_add_urltest_outbound "$config" "$urltest_tag" "$urltest_outbounds" \
"$urltest_testing_url" "$urltest_check_interval" "$urltest_tolerance")"
config="$(sing_box_cm_add_selector_outbound "$config" "$selector_tag" "$selector_outbounds" "$urltest_tag")"
;;
*)
@@ -767,7 +788,7 @@ sing_box_configure_route() {
configure_common_reject_route_rule
local routing_excluded_ips
config_get_bool routing_excluded_ips "settings" "routing_excluded_ips"
config_get routing_excluded_ips "settings" "routing_excluded_ips"
if [ -n "$routing_excluded_ips" ]; then
rule_tag="$(gen_id)"
config=$(sing_box_cm_add_route_rule "$config" "$rule_tag" "$SB_TPROXY_INBOUND_TAG" "$SB_DIRECT_OUTBOUND_TAG")
@@ -861,66 +882,37 @@ configure_routing_for_section_lists() {
if [ "$user_domain_list_type" != "disabled" ]; then
log "Processing user domains routing rules for '$section' section"
prepare_common_ruleset "$section" "domains" "$route_rule_tag"
configure_user_domain_or_subnets_list "$section" "domains" "$route_rule_tag"
configure_user_domain_list "$section" "$route_rule_tag"
fi
if [ "$user_subnet_list_type" != "disabled" ]; then
log "Processing user subnets routing rules for '$section' section"
prepare_common_ruleset "$section" "subnets" "$route_rule_tag"
configure_user_domain_or_subnets_list "$section" "subnets" "$route_rule_tag"
configure_user_subnet_list "$section" "$route_rule_tag"
fi
if [ -n "$local_domain_lists" ]; then
log "Processing local domains routing rules for '$section' section"
configure_local_domain_or_subnet_lists "$section" "domains" "$route_rule_tag"
configure_local_domain_lists "$section" "$route_rule_tag"
fi
if [ -n "$local_subnet_lists" ]; then
log "Processing local subnets routing rules for '$section' section"
configure_local_domain_or_subnet_lists "$section" "subnets" "$route_rule_tag"
configure_local_subnet_lists "$section" "$route_rule_tag"
fi
if [ -n "$remote_domain_lists" ]; then
log "Processing remote domains routing rules for '$section' section"
prepare_common_ruleset "$section" "domains" "$route_rule_tag"
config_list_foreach "$section" "remote_domain_lists" configure_remote_domain_or_subnet_list_handler \
"domains" "$section" "$route_rule_tag"
fi
if [ -n "$remote_subnet_lists" ]; then
log "Processing remote subnets routing rules for '$section' section"
prepare_common_ruleset "$section" "subnets" "$route_rule_tag"
config_list_foreach "$section" "remote_subnet_lists" configure_remote_domain_or_subnet_list_handler \
"subnets" "$section" "$route_rule_tag"
fi
}
prepare_common_ruleset() {
local section="$1"
local type="$2"
local route_rule_tag="$3"
log "Preparing a common $type ruleset for '$section' section" "debug"
ruleset_tag=$(get_ruleset_tag "$section" "common" "$type")
ruleset_filename="$ruleset_tag.json"
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
if file_exists "$ruleset_filepath"; then
log "Ruleset $ruleset_filepath already exists. Skipping." "debug"
else
sing_box_cm_create_local_source_ruleset "$ruleset_filepath"
config=$(sing_box_cm_add_local_ruleset "$config" "$ruleset_tag" "source" "$ruleset_filepath")
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
case "$type" in
domains)
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
;;
subnets) ;;
*) log "Unsupported remote rule set type: $type" "error" ;;
esac
fi
}
configure_community_list_handler() {
local tag="$1"
local section="$2"
@@ -938,99 +930,113 @@ configure_community_list_handler() {
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
}
configure_user_domain_or_subnets_list() {
prepare_source_ruleset() {
local section="$1"
local type="$2"
local name="$2"
local type="$3"
local route_rule_tag="$4"
local items ruleset_tag ruleset_filename ruleset_filepath json_array
case "$type" in
domains)
local user_domain_list_type
config_get user_domain_list_type "$section" "user_domain_list_type"
case "$user_domain_list_type" in
dynamic) config_get items "$section" "user_domains" ;;
text) config_get items "$section" "user_domains_text" ;;
esac
;;
subnets)
local user_subnet_list_type
config_get user_subnet_list_type "$section" "user_subnet_list_type"
case "$user_subnet_list_type" in
dynamic) config_get items "$section" "user_subnets" ;;
text) config_get items "$section" "user_subnets_text" ;;
log "Preparing a $name $type rule set for '$section' section" "debug"
ruleset_tag=$(get_ruleset_tag "$section" "$name" "$type")
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json"
create_source_rule_set "$ruleset_filepath"
case $? in
0)
config=$(sing_box_cm_add_local_ruleset "$config" "$ruleset_tag" "source" "$ruleset_filepath")
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
case "$type" in
domains)
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
;;
subnets) ;;
*)
log "Unsupported remote rule set type: $type" "error"
return 1
;;
esac
;;
3) log "Source rule set $ruleset_filepath already exists, skipping." "debug" ;;
esac
}
configure_user_domain_list() {
local section="$1"
local route_rule_tag="$2"
prepare_source_ruleset "$section" "user" "domains" "$route_rule_tag"
local user_domain_list_type items json_array
config_get user_domain_list_type "$section" "user_domain_list_type"
case "$user_domain_list_type" in
dynamic) config_get items "$section" "user_domains" ;;
text) config_get items "$section" "user_domains_text" ;;
esac
ruleset_tag=$(get_ruleset_tag "$section" "common" "$type")
ruleset_filename="$ruleset_tag.json"
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
items="$(parse_domain_or_subnet_string_to_commas_string "$items" "$type")"
items="$(parse_domain_or_subnet_string_to_commas_string "$items" "domains")"
json_array="$(comma_string_to_json_array "$items")"
case "$type" in
domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
subnets)
sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
;;
esac
patch_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array"
}
configure_local_domain_or_subnet_lists() {
configure_user_subnet_list() {
local section="$1"
local type="$2"
local route_rule_tag="$3"
local route_rule_tag="$2"
local ruleset_tag ruleset_filename ruleset_filepath
ruleset_tag="$(get_ruleset_tag "$section" "local" "$type")"
ruleset_filename="$ruleset_tag.json"
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
prepare_source_ruleset "$section" "user" "subnets" "$route_rule_tag"
sing_box_cm_create_local_source_ruleset "$ruleset_filepath"
config=$(sing_box_cm_add_local_ruleset "$config" "$ruleset_tag" "source" "$ruleset_filepath")
config=$(sing_box_cm_patch_route_rule "$config" "$route_rule_tag" "rule_set" "$ruleset_tag")
case "$type" in
domains)
config_list_foreach "$section" "local_domain_lists" import_local_domain_or_subnet_list "$type" \
"$section" "$ruleset_filepath"
config=$(sing_box_cm_patch_dns_route_rule "$config" "$SB_FAKEIP_DNS_RULE_TAG" "rule_set" "$ruleset_tag")
;;
subnets)
config_list_foreach "$section" "local_subnet_lists" import_local_domain_or_subnet_list "$type" \
"$section" "$ruleset_filepath"
;;
*) log "Unsupported local rule set type: $type" "error" ;;
local user_subnet_list_type items json_array
config_get user_subnet_list_type "$section" "user_subnet_list_type"
case "$user_subnet_list_type" in
dynamic) config_get items "$section" "user_subnets" ;;
text) config_get items "$section" "user_subnets_text" ;;
esac
items="$(parse_domain_or_subnet_string_to_commas_string "$items" "subnets")"
json_array="$(comma_string_to_json_array "$items")"
patch_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
}
import_local_domain_or_subnet_list() {
local filepath="$1"
local type="$2"
local section="$3"
local ruleset_filepath="$4"
configure_local_domain_lists() {
local section="$1"
local route_rule_tag="$2"
if ! file_exists "$filepath"; then
log "File $filepath not found" "error"
prepare_source_ruleset "$section" "local" "domains" "$route_rule_tag"
config_list_foreach "$section" "local_domain_lists" import_local_domain_list_handler "$ruleset_filepath"
}
import_local_domain_list_handler() {
local local_domain_list_filepath="$1"
local ruleset_filepath="$2"
if ! file_exists "$local_domain_list_filepath"; then
log "Local domain list file $local_domain_list_filepath not found" "error"
return 1
fi
local items json_array
items="$(parse_domain_or_subnet_file_to_comma_string "$filepath" "$type")"
import_plain_domain_list_to_local_source_ruleset_chunked "$local_domain_list_filepath" "$ruleset_filepath"
}
if [ -z "$items" ]; then
log "No valid $type found in $filepath" "warn"
return 0
configure_local_subnet_lists() {
local section="$1"
local route_rule_tag="$2"
prepare_source_ruleset "$section" "local" "subnets" "$route_rule_tag"
config_list_foreach "$section" "local_subnet_lists" import_local_subnets_list_handler "$ruleset_filepath"
}
import_local_subnets_list_handler() {
local local_subnet_list_filepath="$1"
local ruleset_filepath="$2"
if ! file_exists "$local_subnet_list_filepath"; then
log "Local subnet list file $local_subnet_list_filepath not found" "error"
return 1
fi
json_array="$(comma_string_to_json_array "$items")"
case "$type" in
domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
subnets)
sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
;;
esac
import_plain_subnet_list_to_local_source_ruleset_chunked "$local_subnet_list_filepath" "$ruleset_filepath"
nft_add_set_elements_from_file_chunked "$local_subnet_list_filepath" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
}
configure_remote_domain_or_subnet_list_handler() {
@@ -1041,9 +1047,10 @@ configure_remote_domain_or_subnet_list_handler() {
local file_extension
file_extension=$(url_get_file_extension "$url")
log "Detected file extension: '$file_extension'" "debug"
case "$file_extension" in
json | srs)
log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
log "Creating a remote $type ruleset from the source URL" "info"
local basename ruleset_tag format detour update_interval
basename=$(url_get_basename "$url")
ruleset_tag=$(get_ruleset_tag "$section" "$basename" "remote-$type")
@@ -1062,7 +1069,7 @@ configure_remote_domain_or_subnet_list_handler() {
esac
;;
*)
log "Detected file extension: '$file_extension' → no processing needed, managed on list_update" "debug"
prepare_source_ruleset "$section" "remote" "$type" "$route_rule_tag"
;;
esac
}
@@ -1075,16 +1082,39 @@ sing_box_configure_experimental() {
config_get cache_file "settings" "cache_path" "/tmp/sing-box/cache.db"
config=$(sing_box_cm_configure_cache_file "$config" true "$cache_file" true)
local enable_yacd external_controller_ui
config_get_bool enable_yacd "settings" "enable_yacd" 0
log "Configuring Clash API"
local enable_yacd enable_yacd_wan_access clash_api_controller_address
config_get_bool enable_yacd "settings" "enable_yacd" 0
config_get_bool enable_yacd_wan_access "settings" "enable_yacd_wan_access" 0
if [ "$enable_yacd" -eq 1 ] && [ "$enable_yacd_wan_access" -eq 1 ]; then
clash_api_controller_address="0.0.0.0"
else
clash_api_controller_address="$(get_service_listen_address)"
if [ -z "$clash_api_controller_address" ]; then
log "Could not determine the listening IP address for the Clash API controller. It will run only on localhost." "warn"
clash_api_controller_address="127.0.0.1"
fi
fi
if [ "$enable_yacd" -eq 1 ]; then
log "YACD is enabled, enabling Clash API with downloadable YACD" "debug"
local external_controller_ui="ui"
config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER" "$external_controller_ui")
local yacd_secret_key external_controller_ui
config_get yacd_secret_key "settings" "yacd_secret_key"
external_controller_ui="ui"
config=$(
sing_box_cm_configure_clash_api \
"$config" \
"$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT" \
"$external_controller_ui" \
"$yacd_secret_key"
)
else
log "YACD is disabled, enabling Clash API in online mode" "debug"
config=$(sing_box_cm_configure_clash_api "$config" "$SB_CLASH_API_CONTROLLER")
config=$(
sing_box_cm_configure_clash_api "$config" "$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT"
)
fi
}
@@ -1113,8 +1143,13 @@ sing_box_additional_inbounds() {
configure_section_mixed_proxy() {
local section="$1"
local mixed_inbound_enabled mixed_proxy_port mixed_inbound_tag mixed_outbound_tag
local mixed_inbound_enabled mixed_proxy_port mixed_inbound_tag mixed_outbound_tag mixed_proxy_address
config_get_bool mixed_inbound_enabled "$section" "mixed_proxy_enabled" 0
mixed_proxy_address="$(get_service_listen_address)"
if [ -z "$mixed_proxy_address" ]; then
log "Could not determine the listening IP address for the Mixed Proxy. The proxy will not be created." "warn"
return 1
fi
config_get mixed_proxy_port "$section" "mixed_proxy_port"
if [ "$mixed_inbound_enabled" -eq 1 ]; then
mixed_inbound_tag="$(get_inbound_tag_by_section "$section-mixed")"
@@ -1123,7 +1158,7 @@ configure_section_mixed_proxy() {
sing_box_cf_add_mixed_inbound_and_route_rule \
"$config" \
"$mixed_inbound_tag" \
"$SB_MIXED_INBOUND_ADDRESS" \
"$mixed_proxy_address" \
"$mixed_proxy_port" \
"$mixed_outbound_tag"
)
@@ -1209,7 +1244,7 @@ import_community_service_subnet_list_handler() {
*) return 0 ;;
esac
local tmpfile http_proxy_address subnets
local tmpfile http_proxy_address
tmpfile=$(mktemp)
http_proxy_address="$(get_service_proxy_address)"
@@ -1220,14 +1255,13 @@ import_community_service_subnet_list_handler() {
return 1
fi
subnets="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "subnets")"
rm -f "$tmpfile"
if [ "$service" = "discord" ]; then
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME" "$subnets"
nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_DISCORD_SET_NAME"
else
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
fi
rm -f "$tmpfile"
}
import_domains_from_remote_domain_lists() {
@@ -1248,17 +1282,41 @@ import_domains_from_remote_domain_list_handler() {
local file_extension
file_extension=$(url_get_file_extension "$url")
log "Detected file extension: '$file_extension'" "debug"
case "$file_extension" in
json | srs)
log "Detected file extension: '$file_extension' → no update needed, sing-box manages updates" "debug"
log "No update needed - sing-box manages updates automatically."
;;
*)
log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
import_domains_or_subnets_from_remote_file "$url" "$section" "domains"
log "Import domains from a remote plain-text list"
import_domains_from_remote_plain_file "$url" "$section"
;;
esac
}
import_domains_from_remote_plain_file() {
local url="$1"
local section="$2"
local tmpfile http_proxy_address items json_array
tmpfile=$(mktemp)
http_proxy_address="$(get_service_proxy_address)"
download_to_file "$url" "$tmpfile" "$http_proxy_address"
if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
log "Download $url list failed" "error"
return 1
fi
convert_crlf_to_lf "$tmpfile"
ruleset_tag=$(get_ruleset_tag "$section" "remote" "domains")
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json"
import_plain_domain_list_to_local_source_ruleset_chunked "$tmpfile" "$ruleset_filepath"
rm -f "$tmpfile"
}
import_subnets_from_remote_subnet_lists() {
local section="$1"
local remote_subnet_lists
@@ -1277,81 +1335,46 @@ import_subnets_from_remote_subnet_list_handler() {
local file_extension
file_extension="$(url_get_file_extension "$url")"
log "Detected file extension: '$file_extension'" "debug"
case "$file_extension" in
json)
log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
log "Import subnets from a remote JSON list" "info"
import_subnets_from_remote_json_file "$url"
;;
srs)
log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
log "Import subnets from a remote SRS list" "info"
import_subnets_from_remote_srs_file "$url"
;;
*)
log "Detected file extension: '$file_extension' → proceeding with processing" "debug"
import_domains_or_subnets_from_remote_file "$url" "$section" "subnets"
;;
esac
}
import_domains_or_subnets_from_remote_file() {
local url="$1"
local section="$2"
local type="$3"
local tmpfile http_proxy_address items json_array
tmpfile=$(mktemp)
http_proxy_address="$(get_service_proxy_address)"
download_to_file "$url" "$tmpfile" "$http_proxy_address"
if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
log "Download $url list failed" "error"
return 1
fi
items="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "$type")"
rm -f "$tmpfile"
if [ -z "$items" ]; then
log "No valid $type found in $url" "warn"
return 0
fi
ruleset_tag=$(get_ruleset_tag "$section" "common" "$type")
ruleset_filename="$ruleset_tag.json"
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_filename"
json_array="$(comma_string_to_json_array "$items")"
case "$type" in
domains) sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array" ;;
subnets)
sing_box_cm_patch_local_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$items"
log "Import subnets from a remote plain-text list" "info"
import_subnets_from_remote_plain_file "$url" "$section"
;;
esac
}
import_subnets_from_remote_json_file() {
local url="$1"
local tmpfile subnets http_proxy_address
tmpfile="$(mktemp)"
local json_tmpfile subnets_tmpfile http_proxy_address
json_tmpfile="$(mktemp)"
subnets_tmpfile="$(mktemp)"
http_proxy_address="$(get_service_proxy_address)"
download_to_stream "$url" "$http_proxy_address" | jq -r '.rules[].ip_cidr[]?' > "$tmpfile"
download_to_file "$url" "$json_tmpfile" "$http_proxy_address"
if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
if [ $? -ne 0 ] || [ ! -s "$json_tmpfile" ]; then
log "Download $url list failed" "error"
return 1
fi
subnets="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "subnets")"
rm -f "$tmpfile"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
extract_ip_cidr_from_json_ruleset_to_file "$json_tmpfile" "$subnets_tmpfile"
nft_add_set_elements_from_file_chunked "$subnets_tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
rm -f "$json_tmpfile" "$subnets_tmpfile"
}
import_subnets_from_remote_srs_file() {
local url="$1"
local binary_tmpfile json_tmpfile subnets_tmpfile subnets http_proxy_address
local binary_tmpfile json_tmpfile subnets_tmpfile http_proxy_address
binary_tmpfile="$(mktemp)"
json_tmpfile="$(mktemp)"
subnets_tmpfile="$(mktemp)"
@@ -1364,15 +1387,39 @@ import_subnets_from_remote_srs_file() {
return 1
fi
if ! decompile_srs_file "$binary_tmpfile" "$json_tmpfile"; then
log "Failed to decompile SRS file" "error"
if ! decompile_binary_ruleset "$binary_tmpfile" "$json_tmpfile"; then
log "Failed to decompile binary rule set file" "error"
return 1
fi
jq -r '.rules[].ip_cidr[]' "$json_tmpfile" > "$subnets_tmpfile"
subnets="$(parse_domain_or_subnet_file_to_comma_string "$subnets_tmpfile" "subnets")"
extract_ip_cidr_from_json_ruleset_to_file "$json_tmpfile" "$subnets_tmpfile"
nft_add_set_elements_from_file_chunked "$subnets_tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
rm -f "$binary_tmpfile" "$json_tmpfile" "$subnets_tmpfile"
nft_add_set_elements "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME" "$subnets"
}
import_subnets_from_remote_plain_file() {
local url="$1"
local section="$2"
local tmpfile http_proxy_address items json_array
tmpfile=$(mktemp)
http_proxy_address="$(get_service_proxy_address)"
download_to_file "$url" "$tmpfile" "$http_proxy_address"
if [ $? -ne 0 ] || [ ! -s "$tmpfile" ]; then
log "Download $url list failed" "error"
return 1
fi
convert_crlf_to_lf "$tmpfile"
ruleset_tag=$(get_ruleset_tag "$section" "remote" "subnets")
ruleset_filepath="$TMP_RULESET_FOLDER/$ruleset_tag.json"
import_plain_subnet_list_to_local_source_ruleset_chunked "$tmpfile" "$ruleset_filepath"
nft_add_set_elements_from_file_chunked "$tmpfile" "$NFT_TABLE_NAME" "$NFT_COMMON_SET_NAME"
rm -f "$tmpfile"
}
## Support functions
@@ -1455,6 +1502,26 @@ section_has_enabled_lists() {
fi
}
get_service_listen_address() {
local service_listen_address
config_get service_listen_address "settings" "service_listen_address"
if [ -n "$service_listen_address" ]; then
log "Attention! The service_listen_address option is being used, overriding the automatic detection of the listening IP address!" "warn"
echo "$service_listen_address"
return 0
fi
service_listen_address="$(uci_get "network" "lan" "ipaddr" | awk '{print $1}' | cut -d'/' -f1)"
if [ -z "$service_listen_address" ]; then
log "Failed to determine the listening IP address. Please open an issue to report this problem: https://github.com/itdoginfo/podkop/issues" "error"
return 1
fi
echo "$service_listen_address"
}
## nftables
nft_list_all_traffic_from_ip() {
local ip="$1"
@@ -1626,7 +1693,7 @@ check_logs() {
nolog "Logs not found"
return 1
fi
ы
# Find the last occurrence of "Starting podkop"
local start_line
start_line=$(echo "$logs" | grep -n "podkop.*Starting podkop" | tail -n 1 | cut -d: -f1)
@@ -1684,16 +1751,11 @@ show_config() {
tmp_config=$(mktemp)
sed -e 's/\(option proxy_string\).*/\1 '\''MASKED'\''/g' \
-e 's/\(option outbound_json\).*/\1 '\''MASKED'\''/g' \
-e 's/\(option second_proxy_string\).*/\1 '\''MASKED'\''/g' \
-e 's/\(option second_outbound_json\).*/\1 '\''MASKED'\''/g' \
-e 's/\(vless:\/\/[^@]*@\)/vless:\/\/MASKED@/g' \
-e 's/\(ss:\/\/[^@]*@\)/ss:\/\/MASKED@/g' \
-e 's/\(pbk=[^&]*\)/pbk=MASKED/g' \
-e 's/\(sid=[^&]*\)/sid=MASKED/g' \
-e 's/\(option dns_server '\''[^'\'']*\.dns\.nextdns\.io'\''\)/option dns_server '\''MASKED.dns.nextdns.io'\''/g' \
-e "s|\(option dns_server 'dns\.nextdns\.io\)/[^']*|\1/MASKED|" \
-e '/option outbound_json/,/^}/c\ option outbound_json '\''MASKED'\''' \
-e 's/\(list urltest_proxy_links\).*/\1 '\''MASKED'\''/g' \
-e "s@\\(option dns_server '[^/]*\\)/[^']*'@\\1/MASKED'@g" \
-e "s@\\(option domain_resolver_dns_server '[^/]*\\)/[^']*'@\\1/MASKED'@g" \
-e 's/\(option yacd_secret_key\).*/\1 '\''MASKED'\''/g' \
"$PODKOP_CONFIG" > "$tmp_config"
cat "$tmp_config"
@@ -2073,13 +2135,28 @@ check_fakeip() {
#######################################
clash_api() {
local CLASH_URL="127.0.0.1:9090"
local TEST_URL="https://www.gstatic.com/generate_204"
local action="$1"
local clash_api_controller_address CLASH_URL TEST_URL
clash_api_controller_address="$(get_service_listen_address)"
if [ -z "$clash_api_controller_address" ]; then
clash_api_controller_address="127.0.0.1"
fi
CLASH_URL="$clash_api_controller_address:$SB_CLASH_API_CONTROLLER_PORT"
TEST_URL="https://www.gstatic.com/generate_204"
local enable_yacd_wan_access yacd_secret_key auth_header
config_get_bool enable_yacd_wan_access "settings" "enable_yacd_wan_access" 0
config_get yacd_secret_key "settings" "yacd_secret_key"
if [ "$enable_yacd_wan_access" -eq 1 ]; then
auth_header="Authorization: Bearer $yacd_secret_key"
else
auth_header=""
fi
case "$action" in
get_proxies)
curl -s "$CLASH_URL/proxies" | jq .
curl -s --header "$auth_header" "$CLASH_URL/proxies" | jq .
;;
get_proxy_latency)
@@ -2092,6 +2169,7 @@ clash_api() {
fi
curl -G -s "$CLASH_URL/proxies/$proxy_tag/delay" \
--header "$auth_header" \
--data-urlencode "url=$TEST_URL" \
--data-urlencode "timeout=$timeout" | jq .
;;
@@ -2106,6 +2184,7 @@ clash_api() {
fi
curl -G -s "$CLASH_URL/group/$group_tag/delay" \
--header "$auth_header" \
--data-urlencode "url=$TEST_URL" \
--data-urlencode "timeout=$timeout" | jq .
;;
@@ -2120,8 +2199,11 @@ clash_api() {
fi
local response
response=$(curl -X PUT -s -w "\n%{http_code}" "$CLASH_URL/proxies/$group_tag" \
--data-raw "{\"name\":\"$proxy_tag\"}")
response=$(
curl -X PUT -s -w "\n%{http_code}" "$CLASH_URL/proxies/$group_tag" \
--header "$auth_header" \
--data-raw "{\"name\":\"$proxy_tag\"}"
)
local http_code
local body
@@ -2279,9 +2361,9 @@ global_check() {
fi
if [ "$sing_box_version_ok" -eq 1 ]; then
print_global "✅ Sing-box version >= 1.12.4"
print_global "✅ Sing-box version is compatible (newer than 1.12.4)"
else
print_global "❌ Sing-box version >= 1.12.4"
print_global "❌ Sing-box version is not compatible (older than 1.12.4)"
fi
if [ "$sing_box_service_exist" -eq 1 ]; then

View File

@@ -38,7 +38,6 @@ SB_TPROXY_INBOUND_PORT=1602
SB_DNS_INBOUND_TAG="dns-in"
SB_DNS_INBOUND_ADDRESS="127.0.0.42"
SB_DNS_INBOUND_PORT=53
SB_MIXED_INBOUND_ADDRESS="0.0.0.0" # TODO(ampetelin): maybe to determine address?
SB_SERVICE_MIXED_INBOUND_TAG="service-mixed-in"
SB_SERVICE_MIXED_INBOUND_ADDRESS="127.0.0.1"
SB_SERVICE_MIXED_INBOUND_PORT=4534
@@ -47,7 +46,7 @@ SB_DIRECT_OUTBOUND_TAG="direct-out"
# Route
SB_REJECT_RULE_TAG="reject-rule-tag"
# Experimental
SB_CLASH_API_CONTROLLER="0.0.0.0:9090"
SB_CLASH_API_CONTROLLER_PORT=9090
## Lists
GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"

View File

@@ -105,37 +105,6 @@ get_domain_resolver_tag() {
echo "$section-$postfix"
}
# Constructs and returns a ruleset tag using section, name, optional type, and a fixed postfix
get_ruleset_tag() {
local section="$1"
local name="$2"
local type="$3"
local postfix="ruleset"
if [ -n "$type" ]; then
echo "$section-$name-$type-$postfix"
else
echo "$section-$name-$postfix"
fi
}
# Determines the ruleset format based on the file extension (json → source, srs → binary)
get_ruleset_format_by_file_extension() {
local file_extension="$1"
local format
case "$file_extension" in
json) format="source" ;;
srs) format="binary" ;;
*)
log "Unsupported file extension: .$file_extension" "error"
return 1
;;
esac
echo "$format"
}
# Converts a comma-separated string into a JSON array string
comma_string_to_json_array() {
local input="$1"
@@ -156,6 +125,12 @@ url_decode() {
printf '%b' "$(echo "$encoded" | sed 's/+/ /g; s/%/\\x/g')"
}
# Returns the scheme (protocol) part of a URL
url_get_scheme() {
local url="$1"
echo "${url%%://*}"
}
# Extracts the userinfo (username[:password]) part from a URL
url_get_userinfo() {
local url="$1"
@@ -165,13 +140,23 @@ url_get_userinfo() {
# Extracts the host part from a URL
url_get_host() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#\([:/].*\|$\)##p'
url="${url#*://}"
url="${url#*@}"
url="${url%%[/?#]*}"
echo "${url%%:*}"
}
# Extracts the port number from a URL
url_get_port() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#^[^/]*:\([0-9][0-9]*\).*#\1#p'
url="${url#*://}"
url="${url#*@}"
url="${url%%[/?#]*}"
[[ "$url" == *:* ]] && echo "${url#*:}" || echo ""
}
# Extracts the path from a URL (without query or fragment; returns "/" if empty)
@@ -268,25 +253,6 @@ migration_rename_config_key() {
fi
}
# Download URL content directly
download_to_stream() {
local url="$1"
local http_proxy_address="$2"
local retries="${3:-3}"
local wait="${4:-2}"
for attempt in $(seq 1 "$retries"); do
if [ -n "$http_proxy_address" ]; then
http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -qO- "$url" | sed 's/\r$//' && break
else
wget -qO- "$url" | sed 's/\r$//' && break
fi
log "Attempt $attempt/$retries to download $url failed" "warn"
sleep "$wait"
done
}
# Download URL to file
download_to_file() {
local url="$1"
@@ -305,29 +271,17 @@ download_to_file() {
log "Attempt $attempt/$retries to download $url failed" "warn"
sleep "$wait"
done
if grep -q $'\r' "$filepath"; then
log "Downloaded file has Windows line endings (CRLF). Converting to Unix (LF)"
sed -i 's/\r$//' "$filepath"
fi
}
# Decompiles a sing-box SRS binary file into a JSON ruleset file
decompile_srs_file() {
local binary_filepath="$1"
local output_filepath="$2"
# Converts Windows-style line endings (CRLF) to Unix-style (LF)
convert_crlf_to_lf() {
local filepath="$1"
log "Decompiling $binary_filepath to $output_filepath" "debug"
if ! file_exists "$binary_filepath"; then
log "File $binary_filepath not found" "error"
return 1
fi
sing-box rule-set decompile "$binary_filepath" -o "$output_filepath"
if [[ $? -ne 0 ]]; then
log "Decompilation command failed for $binary_filepath" "error"
return 1
if grep -q $'\r' "$filepath"; then
log "File '$filepath' contains CRLF line endings. Converting to LF..." "debug"
local tmpfile
tmpfile=$(mktemp)
tr -d '\r' < "$filepath" > "$tmpfile" && mv "$tmpfile" "$filepath" || rm -f "$tmpfile"
fi
}
@@ -399,4 +353,4 @@ parse_domain_or_subnet_file_to_comma_string() {
done < "$filepath"
echo "$result"
}
}

View File

@@ -27,4 +27,44 @@ nft_add_set_elements() {
local elements="$3"
nft add element inet "$table" "$set" "{ $elements }"
}
nft_add_set_elements_from_file_chunked() {
local filepath="$1"
local nft_table_name="$2"
local nft_set_name="$3"
local chunk_size="${4:-5000}"
local array count
count=0
while IFS= read -r line; do
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[ -z "$line" ] && continue
if ! is_ipv4 "$line" && ! is_ipv4_cidr "$line"; then
log "'$line' is not IPv4 or IPv4 CIDR" "debug"
continue
fi
if [ -z "$array" ]; then
array="$line"
else
array="$array,$line"
fi
count=$((count + 1))
if [ "$count" = "$chunk_size" ]; then
log "Adding $count elements to nft set $nft_set_name" "debug"
nft_add_set_elements "$nft_table_name" "$nft_set_name" "$array"
array=""
count=0
fi
done < "$filepath"
if [ -n "$array" ]; then
log "Adding $count elements to nft set $nft_set_name" "debug"
nft_add_set_elements "$nft_table_name" "$nft_set_name" "$array"
fi
}

View File

@@ -0,0 +1,180 @@
# Constructs and returns a ruleset tag using section, name, optional type, and a fixed postfix
get_ruleset_tag() {
local section="$1"
local name="$2"
local type="$3"
local postfix="ruleset"
if [ -n "$type" ]; then
echo "$section-$name-$type-$postfix"
else
echo "$section-$name-$postfix"
fi
}
# Creates a new ruleset JSON file if it doesn't already exist
create_source_rule_set() {
local ruleset_filepath="$1"
if file_exists "$ruleset_filepath"; then
return 3
fi
jq -n '{version: 3, rules: []}' > "$ruleset_filepath"
}
#######################################
# Patch a source ruleset JSON file for sing-box by appending a new ruleset object containing the provided key
# and value.
# Arguments:
# filepath: path to the JSON file to patch
# key: the ruleset key to insert (e.g., "ip_cidr")
# value: a JSON array of values to assign to the key
# Example:
# patch_source_ruleset_rules "/tmp/sing-box/ruleset.json" "ip_cidr" '["1.1.1.1","2.2.2.2"]'
#######################################
patch_source_ruleset_rules() {
local filepath="$1"
local key="$2"
local value="$3"
local tmpfile=$(mktemp)
jq --arg key "$key" --argjson value "$value" \
'( .rules | map(has($key)) | index(true) ) as $idx |
if $idx != null then
.rules[$idx][$key] = (.rules[$idx][$key] + $value | unique)
else
.rules += [{ ($key): $value }]
end' "$filepath" > "$tmpfile"
if [ $? -ne 0 ]; then
rm -f "$tmpfile"
return 1
fi
mv "$tmpfile" "$filepath"
}
# Imports a plain domain list into a ruleset in chunks, validating domains and appending them as domain_suffix rules
import_plain_domain_list_to_local_source_ruleset_chunked() {
local plain_list_filepath="$1"
local ruleset_filepath="$2"
local chunk_size="${3:-5000}"
local array count json_array
count=0
while IFS= read -r line; do
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[ -z "$line" ] && continue
if ! is_domain_suffix "$line"; then
log "'$line' is not a valid domain" "debug"
continue
fi
if [ -z "$array" ]; then
array="$line"
else
array="$array,$line"
fi
count=$((count + 1))
if [ "$count" = "$chunk_size" ]; then
log "Adding $count elements to rule set at $ruleset_filepath" "debug"
json_array="$(comma_string_to_json_array "$array")"
patch_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array"
array=""
count=0
fi
done < "$plain_list_filepath"
if [ -n "$array" ]; then
log "Adding $count elements to rule set at $ruleset_filepath" "debug"
json_array="$(comma_string_to_json_array "$array")"
patch_source_ruleset_rules "$ruleset_filepath" "domain_suffix" "$json_array"
fi
}
# Imports a plain IPv4/CIDR list into a ruleset in chunks, validating entries and appending them as ip_cidr rules
import_plain_subnet_list_to_local_source_ruleset_chunked() {
local plain_list_filepath="$1"
local ruleset_filepath="$2"
local chunk_size="${3:-5000}"
local array count json_array
count=0
while IFS= read -r line; do
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[ -z "$line" ] && continue
if ! is_ipv4 "$line" && ! is_ipv4_cidr "$line"; then
log "'$line' is not IPv4 or IPv4 CIDR" "debug"
continue
fi
if [ -z "$array" ]; then
array="$line"
else
array="$array,$line"
fi
count=$((count + 1))
if [ "$count" = "$chunk_size" ]; then
log "Adding $count elements to ruleset at $ruleset_filepath" "debug"
json_array="$(comma_string_to_json_array "$array")"
patch_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
array=""
count=0
fi
done < "$plain_list_filepath"
if [ -n "$array" ]; then
log "Adding $count elements to ruleset at $ruleset_filepath" "debug"
json_array="$(comma_string_to_json_array "$array")"
patch_source_ruleset_rules "$ruleset_filepath" "ip_cidr" "$json_array"
fi
}
# Determines the ruleset format based on the file extension (json → source, srs → binary)
get_ruleset_format_by_file_extension() {
local file_extension="$1"
local format
case "$file_extension" in
json) format="source" ;;
srs) format="binary" ;;
*)
log "Unsupported file extension: .$file_extension" "error"
return 1
;;
esac
echo "$format"
}
# Decompiles a sing-box SRS binary file into a JSON ruleset file
decompile_binary_ruleset() {
local binary_filepath="$1"
local output_filepath="$2"
log "Decompiling $binary_filepath to $output_filepath" "debug"
sing-box rule-set decompile "$binary_filepath" -o "$output_filepath"
if [[ $? -ne 0 ]]; then
log "Decompilation command failed for $binary_filepath" "error"
return 1
fi
}
# Extracts all ip_cidr entries from a JSON ruleset file and writes them to an output file.
extract_ip_cidr_from_json_ruleset_to_file() {
local json_file="$1"
local output_file="$2"
log "Extracting ip_cidr entries from $json_file to $output_file" "debug"
jq -r '.rules[].ip_cidr[]' "$json_file" > "$output_file"
}

View File

@@ -64,7 +64,8 @@ sing_box_cf_add_proxy_outbound() {
url=$(url_decode "$url")
url=$(url_strip_fragment "$url")
local scheme="${url%%://*}"
local scheme
scheme="$(url_get_scheme "$url")"
case "$scheme" in
socks4 | socks4a | socks5)
local tag host port version userinfo username password udp_over_tcp
@@ -146,6 +147,21 @@ sing_box_cf_add_proxy_outbound() {
config=$(_add_outbound_security "$config" "$tag" "$url")
config=$(_add_outbound_transport "$config" "$tag" "$url")
;;
hysteria2 | hy2)
local tag host port password obfuscator_type obfuscator_password upload_mbps download_mbps
tag=$(get_outbound_tag_by_section "$section")
host=$(url_get_host "$url")
port="$(url_get_port "$url")"
password=$(url_get_userinfo "$url")
obfuscator_type=$(url_get_query_param "$url" "obfs")
obfuscator_password=$(url_get_query_param "$url" "obfs-password")
upload_mbps=$(url_get_query_param "$url" "upmbps")
download_mbps=$(url_get_query_param "$url" "downmbps")
config=$(sing_box_cm_add_hysteria2_outbound "$config" "$tag" "$host" "$port" "$password" "$obfuscator_type" \
"$obfuscator_password" "$upload_mbps" "$download_mbps")
config=$(_add_outbound_security "$config" "$tag" "$url")
;;
*)
log "Unsupported proxy $scheme type. Aborted." "fatal"
exit 1
@@ -160,13 +176,20 @@ _add_outbound_security() {
local outbound_tag="$2"
local url="$3"
local security
local security scheme
security=$(url_get_query_param "$url" "security")
if [ -z "$security" ]; then
scheme="$(url_get_scheme "$url")"
if [ "$scheme" = "hysteria2" ] || [ "$scheme" = "hy2" ]; then
security="tls"
fi
fi
case "$security" in
tls | reality)
local sni insecure alpn fingerprint public_key short_id
sni=$(url_get_query_param "$url" "sni")
insecure=$(url_get_query_param "$url" "allowInsecure")
insecure=$(_get_insecure_query_param_from_url "$url")
alpn=$(comma_string_to_json_array "$(url_get_query_param "$url" "alpn")")
fingerprint=$(url_get_query_param "$url" "fp")
public_key=$(url_get_query_param "$url" "pbk")
@@ -193,6 +216,18 @@ _add_outbound_security() {
echo "$config"
}
_get_insecure_query_param_from_url() {
local url="$1"
local insecure
insecure=$(url_get_query_param "$url" "allowInsecure")
if [ -z "$insecure" ]; then
insecure=$(url_get_query_param "$url" "insecure")
fi
echo "$insecure"
}
_add_outbound_transport() {
local config="$1"
local outbound_tag="$2"
@@ -214,7 +249,12 @@ _add_outbound_transport() {
;;
grpc)
# TODO(ampetelin): Add handling of optional gRPC parameters; example links are needed.
config=$(sing_box_cm_set_grpc_transport_for_outbound "$config" "$outbound_tag")
local grpc_service_name
grpc_service_name=$(url_get_query_param "$url" "serviceName")
config=$(
sing_box_cm_set_grpc_transport_for_outbound "$config" "$outbound_tag" "$grpc_service_name"
)
;;
*)
log "Unknown transport '$transport' detected." "error"

View File

@@ -21,9 +21,9 @@ SERVICE_TAG="__service_tag"
#######################################
# Configure the logging section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string, JSON configuration
# disabled: boolean, true to disable logging
# level: string, e.g., "info", "debug", "warn"
# level: string, log level. One of: trace debug info warn error fatal panic.
# timestamp: boolean, true to include timestamps
# Outputs:
# Writes updated JSON configuration to stdout
@@ -50,7 +50,7 @@ sing_box_cm_configure_log() {
#######################################
# Configure the DNS section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# final: string, default dns server tag
# strategy: string, default domain strategy for resolving the domain names
# independent_cache: boolean, whether to use an independent DNS cache
@@ -82,12 +82,12 @@ sing_box_cm_configure_dns() {
#######################################
# Add a UDP DNS server to the DNS section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the DNS server
# server_address: string, IP address or hostname of the DNS server
# server_port: string or number, port of the DNS server
# domain_resolver: string, domain resolver to use for resolving domain names
# detour: string, tag of the upstream outbound
# server_port: string or integer, port of the DNS server
# domain_resolver: string, domain resolver to use for resolving domain names (optional)
# detour: string, tag of the upstream outbound (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -122,12 +122,12 @@ sing_box_cm_add_udp_dns_server() {
#######################################
# Add a TLS DNS server to the DNS section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the DNS server
# server_address: string, IP address or hostname of the DNS server
# server_port: string or number, port of the DNS server
# domain_resolver: string, domain resolver to use for resolving domain names
# detour: string, tag of the upstream outbound
# server_port: string or integer, port of the DNS server
# domain_resolver: string, domain resolver to use for resolving domain names (optional)
# detour: string, tag of the upstream outbound (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -162,14 +162,14 @@ sing_box_cm_add_tls_dns_server() {
#######################################
# Add an HTTPS DNS server to the DNS section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the DNS server
# server_address: string, IP address or hostname of the DNS server
# server_port: string or number, port of the DNS server
# path: string, optional URL path for HTTPS DNS requests
# headers: string, optional additional headers for HTTPS DNS requests
# domain_resolver: string, domain resolver to use for resolving domain names
# detour: string, tag of the upstream outbound
# server_port: string or integer, port of the DNS server
# path: string, URL path for HTTPS DNS requests (optional)
# headers: string, additional headers for HTTPS DNS requests (optional)
# domain_resolver: string, domain resolver to use for resolving domain names (optional)
# detour: string, tag of the upstream outbound (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -210,7 +210,7 @@ sing_box_cm_add_https_dns_server() {
#######################################
# Add a FakeIP DNS server to the DNS section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the DNS server
# inet4_range: string, IPv4 range used for fake IP mapping
# Outputs:
@@ -236,7 +236,7 @@ sing_box_cm_add_fakeip_dns_server() {
#######################################
# Add a DNS routing rule to the DNS section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# server: string, target DNS server for the rule
# tag: string, identifier for the route rule
# Outputs:
@@ -263,10 +263,10 @@ sing_box_cm_add_dns_route_rule() {
#######################################
# Patch a DNS routing rule in the DNS section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier of the rule to patch
# key: string, the key in the rule to update or add
# value: JSON value to assign to the key
# value: string, JSON value to assign to the key
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -304,9 +304,9 @@ sing_box_cm_patch_dns_route_rule() {
#######################################
# Add a DNS reject rule to the DNS section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# key: string, the key to set for the reject rule
# value: JSON value to assign to the key
# value: string, JSON value to assign to the key
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -331,10 +331,10 @@ sing_box_cm_add_dns_reject_rule() {
#######################################
# Add a TProxy inbound to the inbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the inbound
# listen_address: string, IP address to listen on
# listen_port: number, port to listen on
# listen_port: integer, port to listen on
# tcp_fast_open: boolean, enable or disable TCP Fast Open
# udp_fragment: boolean, enable or disable UDP fragmentation
# Outputs:
@@ -369,10 +369,10 @@ sing_box_cm_add_tproxy_inbound() {
#######################################
# Add a Direct inbound to the inbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the inbound
# listen_address: string, IP address to listen on
# listen_port: number, port to listen on
# listen_port: integer, port to listen on
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -399,10 +399,10 @@ sing_box_cm_add_direct_inbound() {
#######################################
# Add a Mixed inbound to the inbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the inbound
# listen_address: string, IP address to listen on
# listen_port: number, port to listen on
# listen_port: integer, port to listen on
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -429,7 +429,7 @@ sing_box_cm_add_mixed_inbound() {
#######################################
# Add a Direct outbound to the outbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the outbound
# Outputs:
# Writes updated JSON configuration to stdout
@@ -451,15 +451,15 @@ sing_box_cm_add_direct_outbound() {
#######################################
# Add a SOCKS outbound to the outbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the outbound
# server_address: string, IP address or hostname of the SOCKS server
# server_port: number, port of the SOCKS server
# version: string, optional SOCKS version
# username: string, optional username for authentication
# password: string, optional password for authentication
# network: string, optional network type (e.g., "tcp")
# udp_over_tcp: number, optional version for UDP over TCP
# server_port: integer, port of the SOCKS server
# version: string, SOCKS version (optional)
# username: string, username for authentication (optional)
# password: string, password for authentication (optional)
# network: string, network type (e.g., "tcp") (optional)
# udp_over_tcp: integer, version for UDP over TCP (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -509,16 +509,16 @@ sing_box_cm_add_socks_outbound() {
#######################################
# Add a Shadowsocks outbound to the outbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the outbound
# server_address: string, IP address or hostname of the Shadowsocks server
# server_port: number, port of the Shadowsocks server
# server_port: integer, port of the Shadowsocks server
# method: string, encryption method
# password: string, password for encryption
# network: string, optional network type (e.g., "tcp")
# udp_over_tcp: number, optional version for UDP over TCP
# plugin: string, optional plugin name
# plugin_opts: string, optional plugin options
# network: string, network type (e.g., "tcp") (optional)
# udp_over_tcp: integer, version for UDP over TCP (optional)
# plugin: string, plugin name (optional)
# plugin_opts: string, plugin options (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -573,14 +573,14 @@ sing_box_cm_add_shadowsocks_outbound() {
#######################################
# Add a VLESS outbound to the outbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the outbound
# server_address: string, IP address or hostname of the VLESS server
# server_port: number, port of the VLESS server
# server_port: integer, port of the VLESS server
# uuid: string, user UUID
# flow: string, optional flow setting
# network: string, optional network type (e.g., "tcp")
# packet_encoding: string, optional packet encoding method
# flow: string, flow setting (optional)
# network: string, network type (e.g., "tcp") (optional)
# packet_encoding: string, packet encoding method (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -624,12 +624,12 @@ sing_box_cm_add_vless_outbound() {
#######################################
# Add a Trojan outbound to the outbounds section of a sing-box JSON configuration.
# Arguments:
# config: string, JSON configuration
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the outbound
# server_address: string, IP address or hostname of the Trojan server
# server_port: number, port of the Trojan server
# server_port: integer, port of the Trojan server
# password: string, password for authentication
# network: string, optional network type (e.g., "tcp")
# network: string, network type (e.g., "tcp") (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -661,15 +661,76 @@ sing_box_cm_add_trojan_outbound() {
)]'
}
#######################################
# Add a Hysteria2 outbound to the outbounds section of a sing-box JSON configuration.
# Arguments:
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the outbound
# server_address: string, IP address or hostname of the Hysteria2 server
# server_port: integer, port of the Hysteria2 server
# password: string, password for authentication
# obfuscator_type: string, obfuscation type (optional)
# obfuscator_password: string, obfuscation password (optional)
# upload_mbps: integer, upload bandwidth limit in Mbps (optional)
# download_mbps: integer, download bandwidth limit in Mbps (optional)
# network: string, network type (e.g., "udp") (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
# CONFIG=$(sing_box_cm_add_hysteria2_outbound "$CONFIG" "hysteria2-out" "example.com" 443 "supersecret" \
# "salamander" "obfs-pass" "50" "200" "udp")
#######################################
sing_box_cm_add_hysteria2_outbound() {
local config="$1"
local tag="$2"
local server_address="$3"
local server_port="$4"
local password="$5"
local obfuscator_type="$6"
local obfuscator_password="$7"
local upload_mbps="$8"
local download_mbps="$9"
local network="${10}"
echo "$config" | jq \
--arg tag "$tag" \
--arg server_address "$server_address" \
--arg server_port "$server_port" \
--arg password "$password" \
--arg obfuscator_type "$obfuscator_type" \
--arg obfuscator_password "$obfuscator_password" \
--arg upload_mbps "$upload_mbps" \
--arg download_mbps "$download_mbps" \
--arg network "$network" \
'.outbounds += [(
{
type: "hysteria2",
tag: $tag,
server: $server_address,
server_port: ($server_port | tonumber),
password: $password
}
+ (if $obfuscator_type != "" and $obfuscator_password != "" then {
obfs: {
type: $obfuscator_type,
password: $obfuscator_password
}
} else {} end)
+ (if $upload_mbps != "" then {up_mbps: ($upload_mbps | tonumber)} else {} end)
+ (if $download_mbps != "" then {down_mbps: ($download_mbps | tonumber)} else {} end)
+ (if $network != "" then {network: $network} else {} end)
)]'
}
#######################################
# Set gRPC transport settings for an outbound in a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier of the outbound to modify
# service_name: string, optional gRPC service name
# idle_timeout: string or number, optional idle timeout
# ping_timeout: string or number, optional ping timeout
# permit_without_stream: boolean, optional flag for permitting without stream
# service_name: string, gRPC service name (optional)
# idle_timeout: string or integer, idle timeout (optional)
# ping_timeout: string or integer, ping timeout (optional)
# permit_without_stream: boolean, flag for permitting without stream (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -709,12 +770,12 @@ sing_box_cm_set_grpc_transport_for_outbound() {
#######################################
# Set WebSocket transport settings for an outbound in a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier of the outbound to modify
# path: string, WebSocket path
# host: string, optional Host header for WebSocket
# max_early_data: number, optional maximum early data
# early_data_header_name: string, optional header name for early data
# host: string, Host header for WebSocket (optional)
# max_early_data: integer, maximum early data (optional)
# early_data_header_name: string, header name for early data (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -759,14 +820,14 @@ sing_box_cm_set_ws_transport_for_outbound() {
#######################################
# Set TLS settings for an outbound in a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier of the outbound to modify
# server_name: string, optional, used to verify the hostname on the returned certificates
# insecure: boolean, accept any server certificate
# alpn: JSON value or null, optional supported application level protocols
# utls_fingerprint: string, optional uTLS fingerprint
# reality_public_key: string, optional Reality public key
# reality_short_id: string, optional Reality short ID
# server_name: string, used to verify the hostname on the returned certificates (optional)
# insecure: boolean, accept any server certificate (optional)
# alpn: string, JSON value, supported application level protocols (optional)
# utls_fingerprint: string, uTLS fingerprint (optional)
# reality_public_key: string, Reality public key (optional)
# reality_short_id: string, Reality short ID (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -825,7 +886,7 @@ sing_box_cm_set_tls_for_outbound() {
#######################################
# Add a Direct outbound with a specific network interface to a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the outbound
# interface: string, network interface to bind the outbound
# domain_resolver: string, tag of the domain resolver to be used for this outbound (optional)
@@ -857,9 +918,9 @@ sing_box_cm_add_interface_outbound() {
#######################################
# Add a raw outbound JSON object to the outbounds section of a sing-box configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the outbound
# raw_outbound: JSON object, the raw outbound configuration to add
# raw_outbound: string, JSON object, the raw outbound configuration to add
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -881,14 +942,14 @@ sing_box_cm_add_raw_outbound() {
#######################################
# Add a URLTest outbound to the outbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the URLTest outbound
# outbounds: JSON array of outbound tags to test
# url: URL to probe (optional)
# interval: test interval (e.g., "10s") (optional)
# tolerance: max latency difference tolerated (optional)
# idle_timeout: idle timeout duration (optional)
# interrupt_exist_connections: flag to interrupt existing connections ("true"/"false") (optional)
# outbounds: string, JSON array of outbound tags to test
# url: string, URL to probe (optional)
# interval: string, test interval (e.g., "10s") (optional)
# tolerance: string or integer, max latency difference tolerated (optional)
# idle_timeout: string or integer, idle timeout duration (optional)
# interrupt_exist_connections: boolean, flag to interrupt existing connections ("true"/"false") (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -929,11 +990,11 @@ sing_box_cm_add_urltest_outbound() {
#######################################
# Add a Selector outbound to the outbounds section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the Selector outbound
# outbounds: JSON array of outbound tags to choose from
# default: default outbound tag if none selected (optional)
# interrupt_exist_connections: flag to interrupt existing connections ("true"/"false") (optional)
# outbounds: string (JSON), array of outbound tags to choose from
# default: string, default outbound tag if none selected
# interrupt_exist_connections: boolean, flag to interrupt existing connections ("true"/"false") (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -965,11 +1026,11 @@ sing_box_cm_add_selector_outbound() {
#######################################
# Configure the route section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# final: string, final outbound tag for unmatched traffic
# auto_detect_interface: boolean, enable or disable automatic interface detection
# default_domain_resolver: string, default DNS resolver for domain-based routing
# default_interface: string, default network interface to use when auto detection is disabled
# default_interface: string, default network interface to use when auto detection is disabled (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -1001,7 +1062,7 @@ sing_box_cm_configure_route() {
#######################################
# Add a routing rule to the route section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the route rule
# inbound: string, inbound tag to match
# outbound: string, outbound tag to route matched traffic to
@@ -1032,10 +1093,10 @@ sing_box_cm_add_route_rule() {
#######################################
# Patch a routing rule in the route section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier of the route rule to patch
# key: string, the key in the rule to update or add
# value: JSON value to assign to the key
# value: string (JSON), value to assign to the key
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -1071,9 +1132,9 @@ sing_box_cm_patch_route_rule() {
#######################################
# Add a reject rule to the route section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# key: string, the key to set for the reject rule
# value: JSON value to assign to the key
# value: string (JSON), value to assign to the key
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -1098,9 +1159,9 @@ sing_box_cm_add_reject_route_rule() {
#######################################
# Add a hijack-dns rule to the route section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# key: string, the key to set for the hijack-dns rule
# value: JSON value to assign to the key
# value: string (JSON), value to assign to the key
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -1125,7 +1186,7 @@ sing_box_cm_add_hijack_dns_route_rule() {
#######################################
# Add a route-options rule to the route section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the route-options rule
# Outputs:
# Writes updated JSON configuration to stdout
@@ -1148,9 +1209,9 @@ sing_box_cm_add_options_route_rule() {
#######################################
# Add a sniff rule to the route section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# key: string, the key to set for the sniff rule
# value: JSON value to assign to the key
# value: string (JSON), value to assign to the key
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -1176,7 +1237,7 @@ sing_box_cm_sniff_route_rule() {
#######################################
# Add an inline ruleset to the route.rule_set section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the inline ruleset
# Outputs:
# Writes updated JSON configuration to stdout
@@ -1198,10 +1259,10 @@ sing_box_cm_add_inline_ruleset() {
#######################################
# Add or update a rule in an inline ruleset within the route.rule_set section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier of the inline ruleset
# key: string, the key in the ruleset to update or add
# value: JSON value to assign to the key
# value: string (JSON), value to assign to the key
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -1238,7 +1299,7 @@ sing_box_cm_add_inline_ruleset_rule() {
#######################################
# Add a local ruleset to the route.rule_set section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the local ruleset
# format: string, format of the local ruleset ("source" or "binary")
# path: string, file path to the local ruleset
@@ -1269,12 +1330,12 @@ sing_box_cm_add_local_ruleset() {
#######################################
# Add a remote ruleset to the route.rule_set section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# tag: string, identifier for the remote ruleset
# format: string, format of the remote ruleset ("source" or "binary")
# url: string, URL to download the ruleset from
# download_detour: string, optional detour tag for downloading
# update_interval: string, optional update interval for the ruleset
# download_detour: string, detour tag for downloading (optional)
# update_interval: string, update interval for the ruleset (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -1310,7 +1371,7 @@ sing_box_cm_add_remote_ruleset() {
#######################################
# Configure the experimental cache_file section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# enabled: boolean, enable or disable file caching
# path: string, file path for cache storage
# store_fakeip: boolean, whether to store fake IPs in the cache
@@ -1339,9 +1400,10 @@ sing_box_cm_configure_cache_file() {
#######################################
# Configure the experimental clash_api section of a sing-box JSON configuration.
# Arguments:
# config: JSON configuration (string)
# external_controller: API listening address; Clash API will be disabled if empty
# external_ui: Optional path to static web resources to serve at http://{{external-controller}}/ui
# config: string (JSON), sing-box configuration to modify
# external_controller: string, API listening address; Clash API will be disabled if empty
# external_ui: string, path to static web resources to serve at http://{{external-controller}}/ui (optional)
# secret: string, secret for the RESTful API Authenticate by specifying HTTP header (optional)
# Outputs:
# Writes updated JSON configuration to stdout
# Example:
@@ -1351,65 +1413,23 @@ sing_box_cm_configure_clash_api() {
local config="$1"
local external_controller="$2"
local external_ui="$3"
local secret="$4"
echo "$config" | jq \
--arg external_controller "$external_controller" \
--arg external_ui "$external_ui" \
--arg secret "$secret" \
'.experimental.clash_api = {
external_controller: $external_controller,
}
+ (if $external_ui != "" then { external_ui: $external_ui } else {} end)'
}
#######################################
# Create a local source ruleset JSON file for sing-box.
# Arguments:
# filepath: path to the JSON file to create
# Example:
# sing_box_cm_create_local_source_ruleset "/tmp/sing-box/ruleset.json"
#######################################
sing_box_cm_create_local_source_ruleset() {
local filepath="$1"
jq -n '{version: 3, rules: []}' > "$filepath"
}
#######################################
# Patch a local source ruleset JSON file for sing-box by adding unique! values to a given key.
# Arguments:
# filepath: path to the JSON file to patch
# key: the ruleset key to update (e.g., "ip_cidr")
# value: a JSON array of values to add to the key
# Example:
# sing_box_cm_patch_local_source_ruleset_rules "/tmp/sing-box/ruleset.json" "ip_cidr" '["1.1.1.1","2.2.2.2"]'
#######################################
sing_box_cm_patch_local_source_ruleset_rules() {
local filepath="$1"
local key="$2"
local value="$3"
value=$(_normalize_arg "$value")
local content
content="$(cat "$filepath")"
echo "$content" | jq \
--arg key "$key" \
--argjson value "$value" '
([.rules[]?[$key][]] | unique) as $existing
| ($value - $existing) as $value
| if ($value | length) > 0 then
.rules += [{($key): $value}]
else
.
end
' > "$filepath"
+ (if $external_ui != "" then { external_ui: $external_ui } else {} end)
+ (if $secret != "" then { secret: $secret } else {} end)'
}
#######################################
# Save a sing-box JSON configuration to a file, removing service-specific tags.
# Arguments:
# config: JSON configuration (string)
# config: string (JSON), sing-box configuration to modify
# file_path: string, path to save the configuration file
# Outputs:
# Writes the cleaned JSON configuration to the specified file