Compare commits

..

521 Commits

Author SHA1 Message Date
Kirill Sobakin
2ba1c2f740 Merge pull request #188 from itdoginfo/feat/fe-app-podkop
feat: Introduce new fe modular format with minimal scope of refactoring
2025-10-07 17:38:19 +03:00
divocat
5d0f8ce5bf fix: resolve copilot suggestions 2025-10-07 17:23:26 +03:00
divocat
ddad137fc1 Merge branch 'feat/yacd-exp' into feat/fe-app-podkop 2025-10-07 17:16:36 +03:00
divocat
7b2e5d2838 feat: add missing locales 2025-10-07 17:14:28 +03:00
divocat
9a72785fa7 feat: migrate to _ locales handler 2025-10-07 16:55:50 +03:00
divocat
e0874c3775 refactor: make dashboard widgets reactive 2025-10-07 16:26:06 +03:00
divocat
1e6c827f2b fix: cleanup global styles 2025-10-07 01:07:48 +03:00
divocat
c8c0025470 feat: set clash delay timeout to 5s 2025-10-07 01:07:48 +03:00
divocat
c78f97d64f fix: run prettier & remove unused fragments 2025-10-07 01:07:48 +03:00
divocat
7cb43ffb65 feat: implement dashboard tab 2025-10-07 01:07:48 +03:00
divocat
1e4cda9400 feat: add loaders to test latency buttons 2025-10-07 01:07:48 +03:00
divocat
caf82b096f feat: add test latency & select tag functionality 2025-10-07 01:07:48 +03:00
divocat
6117b0ef9b feat: colorize status ans latency 2025-10-07 01:07:48 +03:00
Andrey Petelin
5418187dd3 feat: enable Clash API with YACD or online mode in podkop configuration 2025-10-07 01:07:48 +03:00
Andrey Petelin
31b09cc3d2 feat: conditionally include external_ui in clash_api config if external_ui path is provided 2025-10-07 01:07:48 +03:00
divocat
b2a473573b feat: add vpn section outbound displaying 2025-10-06 15:13:55 +03:00
divocat
aad6d8c002 feat: implement dashboard prototype 2025-10-06 03:43:55 +03:00
divocat
c75dd3e78b feat: add base clash api methods 2025-10-05 18:36:39 +03:00
divocat
341f260fcf refactor: change vless validation logic 2025-10-05 18:13:19 +03:00
divocat
c5e19a0f2d fix: remove unused params for url test string 2025-10-05 16:59:02 +03:00
divocat
d50b6dbab6 fix: correct output format for test 2025-10-05 16:37:56 +03:00
divocat
99c8ead148 fix: correct output format for test
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-05 16:17:35 +03:00
divocat
d605094a9d Merge remote-tracking branch 'origin/feat/fe-app-podkop' into feat/fe-app-podkop 2025-10-05 16:13:08 +03:00
divocat
eb60e6edec fix: run prettier for luci app js assets 2025-10-05 16:12:56 +03:00
itdoginfo
08f5b31d58 CI: Add great succces to summary 2025-10-04 13:19:03 +03:00
itdoginfo
f69e3478c8 CI: Add frontend workflow 2025-10-04 12:47:39 +03:00
divocat
d9a4f50f62 feat: finalize first modular pack 2025-10-04 01:15:12 +03:00
divocat
eb52d52eb4 feat: implement validateProxyUrl validation 2025-10-03 21:44:42 +03:00
divocat
3f4a0cf094 feat: make URLTest Proxy Links options textarea 2025-10-03 21:16:38 +03:00
Kirill Sobakin
b0a8526c90 Merge pull request #187 from itdoginfo/hotfix
Fix DNS server address validation
2025-10-03 16:17:29 +03:00
Andrey Petelin
e9d5b18816 fix: resolve domain resolver DNS server address before IPv4 validation in VPN and DNS configuration sections 2025-10-03 18:07:24 +05:00
divocat
7b06f422af feat: add trojan link support to Proxy Configuration URL validation 2025-10-03 14:32:17 +03:00
divocat
96bcc36cf1 refactor: remove unused variables 2025-10-03 14:12:08 +03:00
divocat
db8e8e8298 refactor: migrate global styles to injectGlobalStyles 2025-10-03 14:12:08 +03:00
divocat
eb0617eef1 fix: corrent naming for User Domains List validation 2025-10-03 14:12:08 +03:00
divocat
8f9bff9a64 refactor: migrate Outbound Configuration validation to modular 2025-10-03 14:12:08 +03:00
divocat
65d3a9253f refactor: migrate Proxy Configuration URL validation to modular 2025-10-03 14:12:08 +03:00
divocat
b99116fbf3 feat: implement ss/vless validations 2025-10-03 14:12:08 +03:00
divocat
8f19f31e7a refactor: migrate User Domains List validation to modular 2025-10-03 14:12:08 +03:00
divocat
327c3d2b68 feat: implement parseValueList helper 2025-10-03 14:12:08 +03:00
divocat
260b7b9558 refactor: migrate User Subnets List validation to modular 2025-10-03 14:12:08 +03:00
divocat
df9dba9742 feat: implement bulk validate 2025-10-03 14:12:08 +03:00
divocat
547feb0e06 feat: implement validateSubnet 2025-10-03 14:12:08 +03:00
divocat
77e141b305 feat: add soft wrap to Proxy Configuration URL textarea 2025-10-03 14:12:08 +03:00
divocat
cfc5d995a8 refactor: change Network Interface filter logic 2025-10-03 14:12:08 +03:00
divocat
e84233a10c refactor: change Interface for monitoring filter logic 2025-10-03 14:12:08 +03:00
divocat
b71c7b379d refactor: change Source Network Interface filter logic 2025-10-03 14:12:08 +03:00
divocat
3988588c9f feat: migrate yacd url to dynamic 2025-10-03 14:12:08 +03:00
divocat
cd133838cb feat: add BOOTSTRAP_DNS_SERVER_OPTIONS to constants 2025-10-03 14:12:08 +03:00
divocat
f58472a53d feat: migrate validatePath to modular 2025-10-03 14:12:08 +03:00
divocat
5e95148492 feat: migrate constants to modular 2025-10-03 14:12:08 +03:00
divocat
df9400514b feat: migrate some validation places of additional tab to modular 2025-10-03 14:12:08 +03:00
divocat
14eec8e600 feat: migrate some validation places of config sections to modular 2025-10-03 14:12:08 +03:00
divocat
294cb21e91 feat: Introduce fe modular build system 2025-10-03 14:12:08 +03:00
Kirill Sobakin
4ef15f7340 Merge pull request #186 from itdoginfo/trojan
Trojan
2025-10-03 14:10:33 +03:00
Andrey Petelin
41563a5828 fix: correct Russian translation for "works on router" in podkop.po file 2025-10-03 16:08:25 +05:00
Andrey Petelin
2e99ee3a17 fix: pass outbound tag to security and transport functions for accurate config updates 2025-10-03 16:03:36 +05:00
Andrey Petelin
a8db33dd28 feat: add Trojan proxy support (#172) 2025-10-03 15:40:16 +05:00
Andrey Petelin
1295e0dcb2 feat: add function to append Trojan outbound to sing-box JSON configuration 2025-10-03 14:19:45 +05:00
Andrey Petelin
b6bec0fc51 refactor: rename VLESS-specific functions to generic outbound transport and TLS setters 2025-10-03 13:45:00 +05:00
Andrey Petelin
769d263be2 chore: update String-example.md with detailed Shadowsocks, VLESS, and Trojan protocol examples and configurations 2025-10-03 13:35:18 +05:00
Kirill Sobakin
470f11699c Merge pull request #184 from itdoginfo/split_dns
Replacing Split DNS with Domain Resolver and Bootstrap DNS
2025-10-02 18:18:28 +03:00
Andrey Petelin
852b6c043a i18n: update Russian translations and template with new DNS and domain resolver entries 2025-10-02 19:52:21 +05:00
Andrey Petelin
f5cafd5573 chore: add --no-location option to msgmerge and msginit to omit source code references in PO files 2025-10-02 19:44:39 +05:00
Andrey Petelin
3562b913a2 chore: update DNS protocol and server field labels 2025-10-02 19:35:40 +05:00
Andrey Petelin
f4ac9dcc77 feat: add domain resolver support to VPN mode 2025-10-02 17:49:23 +05:00
Andrey Petelin
f5a629afcf feat: add optional domain_resolver parameter to interface outbound config function 2025-10-02 17:48:08 +05:00
Andrey Petelin
aea201bf24 fix: replace non-working split DNS with bootstrap DNS for upstream DNS resolution 2025-10-02 15:58:26 +05:00
Kirill Sobakin
1313c3b26f Merge pull request #182 from itdoginfo/translation
Translation
2025-10-02 10:54:26 +03:00
Andrey Petelin
a3f4e942c3 chore: update Russian translation file encoding to UTF-8 and reformat multiline strings for better readability 2025-10-02 11:17:33 +05:00
Andrey Petelin
4d8e4c1c13 chore: set width variable to 120 for consistent msgmerge and xgettext formatting in localization scripts 2025-10-02 11:16:50 +05:00
itdoginfo
0cb5c2daae Stop podkop before update sing-box 2025-10-01 14:38:32 +03:00
Kirill Sobakin
19fbfff555 Merge pull request #180 from itdoginfo/translation
Translation
2025-10-01 10:33:57 +03:00
Kirill Sobakin
75a2ed1e29 Merge pull request #179 from itdoginfo/fix
refactor: Add version checks and service existence validation
2025-09-30 19:26:45 +03:00
Andrey Petelin
759b6748c6 refactor: Replace opkg version checks with direct command execution 2025-09-30 19:55:25 +05:00
Andrey Petelin
0a27784f85 chore: Update translation template and Russian translations 2025-09-30 19:30:23 +05:00
Andrey Petelin
3b95ac2bc3 chore: Add width option and package name to xgettext and msgmerge scripts 2025-09-30 19:29:46 +05:00
Andrey Petelin
5c51d99d73 chore: Improve NTP exclusion option description for clarity 2025-09-30 13:25:12 +05:00
Andrey Petelin
904b90e012 fix: Remove empty string translations from UI labels 2025-09-30 13:06:52 +05:00
Andrey Petelin
5fb8343cf8 fix: Remove translation function from Yacd link in additional settings tab 2025-09-30 13:05:56 +05:00
Andrey Petelin
014f0f4bdf feat: Add scripts for generating and updating translation templates 2025-09-30 13:04:44 +05:00
Andrey Petelin
dd44e0156e fix: restore default cachesize and noresolv values in dnsmasq configuration if unset 2025-09-27 12:22:50 +05:00
Andrey Petelin
927b8a53b0 fix: restore default resolvfile in DNS settings if backup servers are missing to prevent resolution issues 2025-09-27 11:47:01 +05:00
itdoginfo
7ba20905d5 Fix sing-box remove 2025-09-26 13:21:53 +03:00
Andrey Petelin
5b15a56502 fix: Add local declaration for lowest variable and improve opkg status error redirection spacing 2025-09-25 11:43:03 +05:00
Andrey Petelin
c31df68bec refactor: Add version checks and service existence validation for required packages before starting podkop 2025-09-25 11:11:41 +05:00
Kirill Sobakin
0a5229f4f6 Merge pull request #173 from itdoginfo/fix
fix: Remove URL fragment before parsing VLESS links
2025-09-18 11:13:50 +03:00
Andrey Petelin
5ecb6ef997 fix: Remove URL fragment before parsing VLESS links 2025-09-18 12:59:17 +05:00
Kirill Sobakin
340c2b3505 Merge pull request #171 from itdoginfo/fix
Fix
2025-09-17 19:17:58 +03:00
Andrey Petelin
515c0be38b fix: revert changes from issue #148 2025-09-17 21:14:57 +05:00
Andrey Petelin
59c59bcb17 fix: Improve shadowsocks userinfo decoding with format validation and error handling` 2025-09-17 21:09:03 +05:00
Kirill Sobakin
e5eff41a0f Merge pull request #170 from itdoginfo/fix
Fix: Mask urltest_proxy_links and move sing-box config check to init config function
2025-09-17 13:04:32 +03:00
Andrey Petelin
bb1c06951c fix: Exclusion of ruleset subnets from dns rules (#148) 2025-09-17 13:31:00 +05:00
Andrey Petelin
4999840340 fix: Support comments in user domain/subnet parsing 2025-09-17 11:58:55 +05:00
Andrey Petelin
6c5a271105 fix: Move sing-box config check to after temp file creation 2025-09-16 20:11:13 +05:00
Andrey Petelin
e336bb831c fix: Mask urltest_proxy_links in config output 2025-09-16 19:45:32 +05:00
Kirill Sobakin
00db99723c Merge pull request #169 from itdoginfo/urltest
fix: Correct boolean value for interrupt_exist_connections in JSON
2025-09-16 15:14:43 +03:00
Andrey Petelin
5439504de7 fix: Correct boolean value for interrupt_exist_connections in JSON generation 2025-09-16 17:12:19 +05:00
Kirill Sobakin
c3072162de Merge pull request #168 from itdoginfo/urltest
feat: Add URLTest proxy configuration type with dynamic list support
2025-09-16 15:10:37 +03:00
Andrey Petelin
d021636f85 chore: Fix placeholder text typo in proxy links field 2025-09-16 17:09:37 +05:00
Andrey Petelin
a06aac0613 feat: Add URLTest proxy configuration type with dynamic list support 2025-09-16 16:58:39 +05:00
Kirill Sobakin
29159243ea Merge pull request #167 from itdoginfo/fix
Fix
2025-09-16 11:44:27 +03:00
Andrey Petelin
269123600a fix: Correct variable usage in domain/subnet parsing function (#165) 2025-09-16 13:39:40 +05:00
Andrey Petelin
49add27f81 fix: Improve domain validation to support suffix matching (#166) 2025-09-16 13:19:02 +05:00
itdoginfo
c929c74da5 DeepWiki 2025-09-15 23:29:04 +03:00
itdoginfo
bb91144a91 Update 2025-09-15 22:22:10 +03:00
itdoginfo
2291d9fb9d Check 23.05 2025-09-15 22:15:10 +03:00
Kirill Sobakin
f722a513d0 Merge pull request #163 from itdoginfo/fix
fix: Use correct variable for detour service address
2025-09-15 17:27:15 +03:00
Andrey Petelin
a71707f174 fix: Use correct variable for detour service address 2025-09-15 19:22:52 +05:00
Kirill Sobakin
983f05345b Merge pull request #161 from itdoginfo/refactoring
Refactoring
2025-09-15 15:52:22 +03:00
Andrey Petelin
ee246895de fix: Redirect base64 decode errors to /dev/null 2025-09-15 17:41:17 +05:00
Andrey Petelin
27719f90ee feat: Add support for DoH URLs with paths and UDP port specification 2025-09-14 17:51:20 +05:00
Andrey Petelin
4a17cf66a3 refactor: Add file existence checks and improve startup reliability 2025-09-14 09:26:26 +05:00
Andrey Petelin
db956452d1 refactor: Move logging functions to library file 2025-09-14 09:10:42 +05:00
Andrey Petelin
4897d3d292 refactor: Split nftables rules for TCP and UDP protocols separately 2025-09-14 08:55:34 +05:00
itdoginfo
0aa0a4a9c8 Add /usr/lib/podkop path 2025-09-13 19:23:09 +03:00
Andrey Petelin
7d082c5def chore: Update README task status from done to pending 2025-09-12 14:05:11 +05:00
Andrey Petelin
8845749517 chore: Update README.md to mark completed tasks 2025-09-12 14:03:32 +05:00
Andrey Petelin
054ed355cf fix: Add packet_encoding support for VLESS outbound configuration 2025-09-11 17:52:47 +05:00
Andrey Petelin
304c57edfa fix: Fix translation for "Local Subnet List Paths" 2025-09-11 17:04:22 +05:00
Andrey Petelin
8dd33cdde2 fix: Remove non-existent filename variable 2025-09-11 17:01:39 +05:00
Andrey Petelin
3d3fbe3bfb fix: Fix variable name in HTTP proxy check for wget command 2025-09-11 16:58:40 +05:00
Andrey Petelin
427ea3bc9a fix: Remove unused server_address variable from DNS configuration 2025-09-11 16:56:30 +05:00
Andrey Petelin
a7f6a993ac chore: shfmt formatting 2025-09-11 16:40:06 +05:00
Andrey Petelin
074c1a9349 chore: Remove TODO comments from user domains and subnets text input fields 2025-09-11 15:32:39 +05:00
Andrey Petelin
b6a6db71a8 refactor: Implement user domain and subnet list handling 2025-09-11 15:31:21 +05:00
Andrey Petelin
38fcb59ed7 fix: Reload config after commit to ensure runtime consistency 2025-09-11 13:37:49 +05:00
Andrey Petelin
5a2ffcfd38 refactor: Add graceful shutdown handling for dnsmasq reconfiguration 2025-09-11 11:43:58 +05:00
Andrey Petelin
49f12b212d chore: Update required sing-box version to 1.12.0 2025-09-10 19:44:31 +05:00
Andrey Petelin
489c61baa2 refactor: Move constants to constants.sh 2025-09-10 19:40:12 +05:00
Andrey Petelin
d4b5431db4 refactor: Refactor nft rules to use named sets for interfaces and localv4 2025-09-10 17:52:14 +05:00
Andrey Petelin
d0ea39abd0 refactor: Rename TEST_DOMAIN variable to FAKEIP_TEST_DOMAIN 2025-09-10 15:39:23 +05:00
Andrey Petelin
d4e754d2eb refactor: Rename FAKEIP constant to SB_FAKEIP_INET4_RANG 2025-09-10 14:14:15 +05:00
Andrey Petelin
82f9ae4c6a refactor: Remove redundant FakeIP config verification 2025-09-10 14:10:17 +05:00
Andrey Petelin
775b0073d3 refactor: Fixed nft rule for routing tagged traffic to localhost tproxy 2025-09-10 13:33:07 +05:00
Andrey Petelin
b477a8abc0 fix: Assign domain resolver tag correctly to DNS servers 2025-09-09 16:30:11 +05:00
Andrey Petelin
81e0c86060 refactor: Add block section support 2025-09-09 15:51:09 +05:00
Andrey Petelin
191522f396 chore: Rename download_to_tempfile to download_to_file for clarity 2025-09-09 11:48:03 +05:00
Andrey Petelin
79cea7a31a chore: remove .shellcheckrc file 2025-09-09 11:20:19 +05:00
Andrey Petelin
6c094aceae fix: Ensure unique values when patching local source ruleset 2025-09-09 11:15:29 +05:00
Andrey Petelin
1e8c2b50f7 chore: Rename NFT_GENERAL_SET_NAME to NFT_COMMON_SET_NAME 2025-09-08 23:10:53 +05:00
Andrey Petelin
27d2366208 chore: Remove list_update refactor TODO 2025-09-08 23:03:28 +05:00
Andrey Petelin
c1133827a2 fix: Pass HTTP proxy address to download functions for remote subnet imports 2025-09-08 23:00:26 +05:00
Andrey Petelin
a187192a88 chore: Move and rename _get_download_detour_tag to get_download_detour_tag 2025-09-08 22:55:42 +05:00
Andrey Petelin
fe30cf9e55 refactor: Refactoring list_update function 2025-09-08 22:43:27 +05:00
Andrey Petelin
9496a88774 refactor: Add ip addresses to nft set for local ruleset handling 2025-09-08 10:46:29 +05:00
Andrey Petelin
f54e92cd7a fix: Update config key from cache_file to cache_path in cache file configuration 2025-09-08 10:38:49 +05:00
Andrey Petelin
d70a04b144 refactor: Improve dnsmasq configuration logic for DNS handling 2025-09-07 18:11:46 +05:00
Andrey Petelin
e5be9c3fd1 refactor: Avoid unnecessary sing-box config writes by comparing hashes before saving (#128) 2025-09-07 12:45:27 +05:00
Andrey Petelin
9762b9cca4 refactor: Remove unused functions 2025-09-07 12:14:02 +05:00
Andrey Petelin
9d861cf3e0 feat: Add sing-box config path option (#128) 2025-09-07 12:12:08 +05:00
Andrey Petelin
49836e4adc feat: Add local subnet lists support with UI and backend integration (#156) 2025-09-05 21:23:55 +05:00
Andrey Petelin
5273935d25 chore: fix my perfect English 2025-09-05 17:01:36 +05:00
Andrey Petelin
d03167f49d chore: fix my perfect English 2025-09-05 16:49:05 +05:00
Andrey Petelin
da89c5c7df refactor: rename parameters for migration 2025-09-05 15:02:24 +05:00
Andrey Petelin
acfc95e86d refactor: Refactoring configuration of local domain lists 2025-09-05 14:50:43 +05:00
Andrey Petelin
17c1d09aa8 refactor: Enable cron job scheduling 2025-09-04 20:11:16 +05:00
Andrey Petelin
c7e21010bd refactor: Add download helper functions to helpers.sh and remove from main script 2025-09-04 20:09:44 +05:00
Andrey Petelin
f70e2ac557 refactor: Simplify cron job logic and renaming variable 2025-09-04 20:02:43 +05:00
Andrey Petelin
cb4e3036be chore: Remove module logging from log function 2025-09-04 18:00:28 +05:00
Andrey Petelin
12fc6bd9ac chore: undo renaming of taboption classarg 2025-09-04 16:43:39 +05:00
Andrey Petelin
2794cad533 fix: Remove direct outbound from DNS server configuration to prevent invalid detour 2025-09-04 12:35:53 +05:00
Andrey Petelin
9b182a3045 fix: fix detour parameter for remote ruleset 2025-09-04 12:28:38 +05:00
Andrey Petelin
f07d90a524 refactor: intermediate refactoring commit 2025-09-04 12:10:05 +05:00
Andrey Petelin
75fc377c22 Merge branch 'main' into refactoring 2025-09-04 11:52:52 +05:00
itdoginfo
33ecb771f9 Fix UDP over TCP for extra section 2025-09-02 14:44:14 +03:00
itdoginfo
86038e2756 Delay setting option 2025-09-02 14:09:35 +03:00
Andrey Petelin
db91c628c8 Merge remote-tracking branch 'origin/refactoring' into refactoring
# Conflicts:
#	podkop/files/usr/lib/sing_box_config_manager.sh
2025-08-31 20:25:16 +05:00
Andrey Petelin
41ce41945c feat: Add domain_resolver and detour parameters to DNS server configurations in sing-box manager script 2025-08-31 20:22:57 +05:00
Andrey Petelin
2753a44440 feat: Add domain_resolver parameter to DNS server configurations in sing-box manager script 2025-08-31 19:43:21 +05:00
Andrey Petelin
cd1a4e2a8e chore: update example links with valid values 2025-08-31 13:19:04 +05:00
Andrey Petelin
7e041da8c6 feat: add sing-box configuration manager script and jq helpers 2025-08-31 13:09:55 +05:00
itdoginfo
f3f5bca555 Add HODCA, CloudFront, DO 2025-08-29 15:02:43 +03:00
itdoginfo
174f16bc76 Update 2025-08-28 19:11:37 +03:00
itdoginfo
7c63a35faa Old value #131 2025-08-28 18:40:53 +03:00
itdoginfo
86a86df982 Fix template 2025-08-28 18:12:35 +03:00
itdoginfo
ac445bc227 Fix template config 2025-08-28 18:08:10 +03:00
itdoginfo
4398e6885b Add template for issues and PR 2025-08-28 18:06:08 +03:00
itdoginfo
9974b42cc2 NFT: output chain for traffic from the router 2025-08-27 00:31:56 +03:00
itdoginfo
8cd990f8a3 Fix extension for list 2025-08-26 14:25:28 +03:00
itdoginfo
c509fd38c7 Fix version for docker 2025-08-26 14:16:08 +03:00
itdoginfo
38991a803a Fix 2025-08-25 19:44:16 +03:00
itdoginfo
29c34e31db Fix 2025-08-25 19:41:04 +03:00
itdoginfo
a77e8fae7d Disable tag check 2025-08-25 19:13:01 +03:00
itdoginfo
6d83737336 Auto version for make 2025-08-25 19:11:12 +03:00
itdoginfo
84115e2f3b v0.4.7 2025-08-25 17:02:46 +03:00
itdoginfo
2dbdb9d2c1 Global check: WG Route 2025-08-24 16:13:41 +03:00
itdoginfo
88c6717152 Disable delay 2025-08-24 13:44:49 +03:00
itdoginfo
b3986308ce Cut WRP prefix 2025-08-24 13:44:39 +03:00
itdoginfo
a15c3cf171 Update #147 2025-08-24 11:59:40 +03:00
itdoginfo
4c91223f85 Merge pull request #136 from SaltyMonkey:main
User Subnet validation for glob ip, init.d/zapret proper check, passwall in conflicts
2025-08-24 11:05:55 +03:00
itdoginfo
7cf7b1f626 Merge branch 'main' into main 2025-08-24 11:04:39 +03:00
SaltyMonkey
a2536534f8 Remove opkg list-installed checks for packages from conflicts 2025-08-24 10:00:13 +03:00
itdoginfo
c49354fe38 Merge pull request #149 from ampetelin:json_srs_lists
Added support for JSON and SRS
2025-08-23 18:53:37 +03:00
Andrey Petelin
6e01e036eb handle missing ip_cidr in rulesets 2025-08-23 20:42:43 +05:00
Andrey Petelin
7484d0c203 Fixed logging of custom ruleset preparation 2025-08-23 19:36:34 +05:00
Andrey Petelin
0eb4ca4ea9 Removed filepath from wget when downloading via proxy 2025-08-23 19:33:47 +05:00
Andrey Petelin
c2d95162b7 Added support for JSON and SRS rulesets 2025-08-20 21:42:46 +05:00
SaltyMonkey
1fc2947fbc Log messages for nextdns 2025-07-25 09:41:58 +03:00
SaltyMonkey
ea931d8463 added nextdns package in conflicts 2025-07-25 09:38:48 +03:00
SaltyMonkey
e2f36c35d4 Log messages for luci-app-passwall and luci-app-passwall in binary 2025-07-12 01:08:39 +03:00
SaltyMonkey
e8f8dcc5e7 added passwall and passwall2(paid version?) in conflicts 2025-07-12 01:00:00 +03:00
SaltyMonkey
1e2174bb80 User Subnets validation: 0.0.0.0 is not allowed 2025-07-12 00:40:51 +03:00
SaltyMonkey
85e515ef15 Fix /etc/init.d/zapret check in global_check, cleanup useless whitespaces 2025-07-12 00:29:33 +03:00
itdoginfo
418cdc4366 rm iptables-mod-extra check 2025-06-30 16:56:39 +03:00
itdoginfo
25b0dcaad5 v0.4.6 2025-06-30 16:27:44 +03:00
itdoginfo
cc59e756dd br_netfilter. Cache size unset. Mixed & source_ip_cidr 2025-06-30 16:26:31 +03:00
itdoginfo
210714c499 unnecessary check 2025-06-30 16:24:33 +03:00
itdoginfo
8b6c336584 Change to 1.1.1.1 2025-06-27 23:54:52 +03:00
itdoginfo
5c543c1608 Change to procd_add_interface_trigger. Added PROCD_RELOAD_DELAY 2025-06-27 23:54:31 +03:00
itdoginfo
ac274d8796 v0.4.5 2025-06-25 23:38:55 +03:00
itdoginfo
ce1f86ceb7 Added split dns. Func for build sing-box config 2025-06-25 23:34:39 +03:00
itdoginfo
1fd67eefb3 Fix eng phrase 2025-06-25 23:32:33 +03:00
itdoginfo
e7b726d27c Merge pull request #127 from procudin/fix/extra-configs-visibility
fix: hide extra configs for non-basic tabs
2025-06-23 13:58:25 +03:00
Artem Prokudin
adb16e7f74 fix: hide extra configs for non-basic tabs 2025-06-15 10:57:16 +03:00
itdoginfo
51da8c22fd Update 2025-06-03 15:47:13 +03:00
itdoginfo
41351dafd2 Removed the installation awg/wg/ovpn/oc. Refactoring 2025-06-03 15:45:27 +03:00
itdoginfo
2aee77b9a2 v0.4.4 Added independent_cache 2025-06-02 15:40:14 +03:00
itdoginfo
2a1a220dc8 v0.4.3 2025-05-22 12:07:08 +03:00
itdoginfo
608caba090 Merge pull request #115 from itdoginfo/fix/comments
♻️ refactor(podkop): update command scheduling and priority handling
2025-05-22 12:01:53 +03:00
Ivan K
04af8c9649 ♻️ refactor(podkop): update command scheduling and priority handling 2025-05-22 11:09:02 +03:00
itdoginfo
88d108e5ab Fix i18n version 2025-05-21 19:43:27 +03:00
itdoginfo
8ce6790355 v0.4.2 2025-05-21 19:18:07 +03:00
itdoginfo
8e7b40cf56 Ready image 2025-05-21 19:17:55 +03:00
itdoginfo
21fa017443 Merge pull request #114 from itdoginfo/fix/comments
Fix/comments
2025-05-21 15:43:58 +03:00
Ivan K
f1954df83b ♻️ refactor(diagnosticTab): move command execution helpers to utils.js 2025-05-21 15:09:42 +03:00
Ivan K
8573bd99b5 ♻️ refactor(diagnosticTab): remove unused debug 2025-05-21 14:21:37 +03:00
Ivan K
c3f44bd124 fix(diagnosticTab): add error polling and notification system 2025-05-21 14:18:23 +03:00
itdoginfo
59e394c4f2 v0.4.1 JS refactoring 2025-05-21 14:16:16 +03:00
itdoginfo
c897c90371 Merge pull request #110 from itdoginfo/fix/comments
♻️ refactor(podkop): modularize configuration and diagnostics sections
2025-05-21 13:48:30 +03:00
Ivan K
bcab66f88c ♻️ refactor(podkop): enhance check_nft function for domain-specific set statistics 2025-05-21 09:48:53 +03:00
Ivan K
05a551e5e3 💄 style(podkop): remove extra newline in NFT check completed message 2025-05-20 19:28:40 +03:00
Ivan K
1f81ec8403 🔧 chore(podkop): use check_nft in global_check 2025-05-20 17:32:04 +03:00
Ivan K
9748178562 ♻️ refactor(podkop): enhance nft set statistics and chain configurations 2025-05-20 17:28:00 +03:00
Ivan K
1411e7d403 ♻️ refactor(diagnosticTab): improve command execution and UI updates 2025-05-19 20:32:08 +03:00
Ivan K
d81a90bd28 ♻️ refactor(diagnosticTab): improve status updates and caching 2025-05-19 19:59:29 +03:00
Ivan K
82f4720326 ♻️ refactor(podkop): rename sections into correct files 2025-05-18 15:58:59 +03:00
Ivan K
10f246ea61 ♻️ refactor(podkop): move URL validation to config.js 2025-05-16 23:30:23 +03:00
Ivan K
c0571320f1 ♻️ refactor(networkUtils): remove custom network functions 2025-05-16 22:22:07 +03:00
Ivan K
a658ca5518 💄 style(podkop): remove unused networkUtils import 2025-05-16 18:40:29 +03:00
Ivan K
08709c93c7 ♻️ refactor(podkop): rename section variables for clarity 2025-05-16 18:28:06 +03:00
Ivan K
cf5b2216be ♻️ refactor(podkop): reorganize sections into subdirectory 2025-05-16 18:23:16 +03:00
Ivan K
682913ade0 ♻️ refactor(podkop): remove unused parameter from createAdditionalSection 2025-05-16 18:08:29 +03:00
Ivan K
3b2cbd0332 ♻️ refactor(podkop): modularize configuration and diagnostics sections 2025-05-16 18:04:33 +03:00
itdoginfo
8f9dcf2c55 Merge pull request #109 from itdoginfo/fix/comments
♻️ refactor(podkop): refactor domain list and API endpoints
2025-05-16 14:50:59 +03:00
Ivan K
91d027b5fe ♻️ refactor(podkop): refactor domain list and API endpoints 2025-05-16 14:46:06 +03:00
itdoginfo
f90ab7f468 v0.4.0 beta 2025-05-15 13:26:57 +03:00
itdoginfo
e4bfd447ce Merge pull request #108 from itdoginfo/fix/comments
fix(podkop): add dont touch my dhcp logic to fake IP check functions
2025-05-15 12:07:08 +03:00
Ivan K
fbdd759b83 ♻️ refactor(podkop): remove redundant DNS/sing-box checks in fakeip status 2025-05-15 11:53:30 +03:00
Ivan K
2488bc30b1 fix(podkop): add dont touch my dhcp logic to fake IP check functions 2025-05-15 11:31:33 +03:00
itdoginfo
dcc12cf920 Merge pull request #107 from itdoginfo/fix/comments
💄 style(podkop): update modal button titles and clipboard content
2025-05-14 14:33:53 +03:00
Ivan K
c99cef9f27 💄 style(podkop): update modal button titles and clipboard content 2025-05-14 14:30:52 +03:00
itdoginfo
8a68f3fcc2 Update 2025-05-13 14:26:50 +03:00
itdoginfo
ed2994be3a v0.3.50 Adde Google AI, Play, HTZ, OVH of list 2025-05-12 23:31:06 +03:00
itdoginfo
77ff5ab781 Merge pull request #105 from itdoginfo/fix/comments
️ perf(dns): improve DNS query performance and error handling
2025-05-12 18:17:50 +03:00
Ivan K
1c80bc5a5e ️ perf(dns): improve DNS query performance and error handling 2025-05-12 17:50:50 +03:00
itdoginfo
f688d74c32 v0.3.49 Improved diagnostics 2025-05-12 17:32:51 +03:00
itdoginfo
7bc50d58d3 Merge pull request #104 from itdoginfo/fix/comments
Fix/comments
2025-05-12 17:25:59 +03:00
Ivan K
77ce0c380b 🐛 fix(dns): improve DNS availability check logic 2025-05-12 17:21:38 +03:00
Ivan K
47d1b349c7 ♻️ refactor(podkop): add local var 2025-05-12 16:51:59 +03:00
Ivan K
e9face1f4a ♻️ refactor(podkop): simplify logging function 2025-05-12 16:48:59 +03:00
itdoginfo
e5bf7d9bed Merge pull request #103 from itdoginfo/fix/comments
♻️ refactor(podkop): improve diagnostics and error handling
2025-05-12 16:48:15 +03:00
Ivan K
dd4722f3e1 ♻️ refactor(podkop): simplify logging functions 2025-05-12 16:40:44 +03:00
Ivan K
1e945dafe7 feat(logging): add colored logging to stdout and syslog 2025-05-12 15:54:12 +03:00
itdoginfo
b080521a58 Update 2025-05-12 00:57:52 +03:00
itdoginfo
6a96a85773 Update 2025-05-12 00:49:19 +03:00
itdoginfo
6fb3a36974 Update 2025-05-12 00:46:30 +03:00
Ivan K
b3dbee1dbe 💄 style(podkop): adjust margin styles in status panel 2025-05-11 20:30:16 +03:00
Ivan K
916321578d ️ feat(dns): add random DNS query ID generation 2025-05-11 19:33:08 +03:00
Ivan K
c74d733717 ♻️ refactor(podkop): improve diagnostics and error handling 2025-05-11 19:26:29 +03:00
itdoginfo
433724f762 v0.3.48 Custom URL CRLF 2025-05-10 18:55:35 +03:00
itdoginfo
6378aa9910 Update 2025-05-10 16:15:53 +03:00
itdoginfo
68f5f123ca v0.3.47 Fix noresolv 1 2025-05-10 12:50:01 +03:00
itdoginfo
fae43d0471 v0.3.46 2025-05-08 19:24:08 +03:00
itdoginfo
9d6dc45fdb #99 Block mode 2025-05-08 19:23:45 +03:00
itdoginfo
9aa5a2d242 Fix site. #100 added ntpd 2025-05-08 10:14:58 +03:00
itdoginfo
63dc86fca4 v0.3.45 Update checker domain 2025-05-07 22:39:23 +03:00
itdoginfo
4d9cedaf4c Return upgrade command 2025-05-07 17:58:19 +03:00
itdoginfo
14e7cbae01 v0.3.44 2025-05-07 17:26:57 +03:00
itdoginfo
c9f610bb1e Change to ip.podkop.net. Fix log. Added restart 2025-05-07 17:24:32 +03:00
itdoginfo
19671c7f67 Stop btn. Change to ip.podkop.net 2025-05-07 17:23:20 +03:00
itdoginfo
6d1e4091e5 Detour 2025-05-07 00:22:04 +03:00
itdoginfo
96d661c49f Fixed default values ttl in comment 2025-05-03 18:52:57 +03:00
itdoginfo
da8dd06b34 Move doc to wiki 2025-05-03 18:12:00 +03:00
itdoginfo
2c1bcffb6d fix iptables 2025-05-03 18:11:49 +03:00
itdoginfo
3040ce7286 v0.3.43 2025-05-02 14:53:11 +03:00
itdoginfo
e025271a14 Added to global check: DNS check and proxy check. From VizzleTF 2025-05-02 14:50:06 +03:00
itdoginfo
2b8208186d Fix global check text 2025-05-02 13:55:07 +03:00
itdoginfo
17fb11baf0 Fixed diagnostics from VizzleTF 2025-05-02 13:34:11 +03:00
itdoginfo
3c1b041b52 Edited text from #96 2025-05-01 22:52:41 +03:00
itdoginfo
38acac1a31 Merge pull request #96 from itdoginfo/chore/sing-box-status
Issue #91 , Issue #94
2025-05-01 22:16:38 +03:00
Ivan K
2939229df3 back to the future 2025-05-01 19:30:05 +03:00
Ivan K
26c3d0bc7e ♻️ refactor(podkop): simplify DoH URL determination logic 2025-05-01 19:26:21 +03:00
Ivan K
b364363b1b feat(dns): add DoH URL resolution function 2025-05-01 19:20:36 +03:00
itdoginfo
d85caf0c0c Fix https-dns-proxy i18 2025-05-01 19:10:18 +03:00
Ivan K
65f72e1e04 ♻️ refactor(podkop): update WARP detection logic 2025-05-01 18:29:42 +03:00
Ivan K
e59ef6dd6f 💄 style(podkop): remove unnecessary sed commands in global_check 2025-05-01 17:57:51 +03:00
Ivan K
05272de650 💄 style(podkop): update formatting and messages 2025-05-01 17:48:25 +03:00
Ivan K
48716e7156 Enhance Podkop functionality with global check feature and improved diagnostics. Added support for FakeIP tests in both browser and router contexts. Updated UI elements for better status reporting and added localization for new messages. 2025-05-01 17:18:07 +03:00
itdoginfo
f29b97e495 v0.3.42 2025-05-01 14:17:18 +03:00
itdoginfo
41c21cebcd Fixed validation for ws 2025-04-30 23:43:36 +03:00
itdoginfo
238e99a547 Update 2025-04-30 19:02:31 +03:00
itdoginfo
4f44fcfe99 Update 2025-04-30 14:48:12 +03:00
itdoginfo
9fd2fb9b6e Update 2025-04-30 00:19:42 +03:00
itdoginfo
c0591b25b9 Fix 2025-04-30 00:16:09 +03:00
itdoginfo
97fd392334 Fixed read. Added upgrade flag 2025-04-30 00:11:55 +03:00
itdoginfo
848c784cc0 Fix 2025-04-29 23:49:28 +03:00
itdoginfo
ab971dcd36 Update 2025-04-29 23:48:49 +03:00
itdoginfo
b8d96f28cd Added CF. Fixed https-dns-proxy warning. Masked for static wan 2025-04-29 18:54:50 +03:00
itdoginfo
f2268fd494 v0.3.41. Improved Diagnotics: WAN, WARP, versions, etc 2025-04-29 12:53:29 +03:00
itdoginfo
19897afcdd v0.3.40. Improved Diagnotics 2025-04-28 00:33:07 +03:00
itdoginfo
0e2ea60f01 v0.3.39. Added global check button 2025-04-27 19:29:34 +03:00
itdoginfo
2dc5944961 Fix https-dns-proxy --force-depends 2025-04-27 18:07:58 +03:00
itdoginfo
f65de36804 Detect https-dns-proxy 2025-04-27 15:50:37 +03:00
itdoginfo
19541f8bb3 v0.3.38. fix reload config luci 2025-04-26 22:35:11 +03:00
itdoginfo
aa42c707fe v0.3.37 2025-04-26 17:49:28 +03:00
itdoginfo
bf96f93987 Fix kill stderr. Return if 127.0.0.42 exists 2025-04-26 17:49:04 +03:00
itdoginfo
ff9aad8947 Option enable iface mon 2025-04-26 17:47:52 +03:00
itdoginfo
d9718617bd Option enable iface mon 2025-04-26 17:47:42 +03:00
itdoginfo
e865c9f324 Validate raw network. Path for DoH. Bool for iface monitoring 2025-04-26 17:47:08 +03:00
itdoginfo
7df8bb5826 rmempty proxy url string 2025-04-25 19:29:31 +03:00
itdoginfo
f960358eb6 0.3.36 2025-04-25 10:57:59 +03:00
itdoginfo
ba44966c02 Interface trigger. Disable sing-box autostart. dont touch dhcp. reload without dnsmasq restart 2025-04-24 19:25:08 +03:00
itdoginfo
615241aa37 Merge pull request #88 from Davoyan/patch-1
Update localisation
2025-04-22 11:36:38 +03:00
Davoyan
9a3220d226 Update localisation 2025-04-22 11:24:54 +03:00
itdoginfo
ec8d28857e #82 and #83 2025-04-15 00:42:16 +03:00
itdoginfo
26b49f5bbb Check fix 2025-04-15 00:15:28 +03:00
itdoginfo
0a7efb3169 Fix 2025-04-03 17:53:21 +03:00
itdoginfo
468e51ee8e v0.3.35 2025-04-03 17:42:45 +03:00
itdoginfo
3b93a914de v0.3.34 2025-04-03 17:27:35 +03:00
itdoginfo
76c5baf1e2 Fix tailscale smartdns in resolve.conf 2025-04-03 17:27:13 +03:00
itdoginfo
c752c46abf Fix resolv_conf value 2025-04-03 17:24:57 +03:00
itdoginfo
1df1defa5e Check curl 2025-04-03 17:24:31 +03:00
itdoginfo
3cb4be6427 v0.3.33 2025-04-03 16:47:47 +03:00
itdoginfo
25bfdce5ce Added critical log. Rm friendlywrt check. Added iptables check 2025-04-03 16:47:26 +03:00
itdoginfo
6d0f097a07 Merge pull request #75 from itdoginfo/feature/error-notification (#47)
Feature/error notification
2025-04-03 14:52:00 +03:00
Ivan K
5f780955eb 💄 style(podkop): update error log filtering criteria 2025-04-03 13:42:55 +03:00
Ivan K
389def9056 ♻️ refactor(podkop): remove unused createErrorModal function 2025-04-03 13:40:41 +03:00
Ivan K
e816da5133 feat(podkop): add error logging and notification system 2025-04-03 13:36:22 +03:00
Ivan K
e57adbe042 🔒 refactor(config): Mask NextDNS server address in config output 2025-03-30 20:36:49 +03:00
itdoginfo
d78c51360d Merge pull request #73 from itdoginfo/feature/no-more-cache
🐛 fix(doh): Improve DoH server compatibility detection for quad9
2025-03-30 18:44:26 +03:00
Ivan K
c2357337fc 🐛 fix(dns): improve DoH server compatibility and error handling 2025-03-30 17:20:06 +03:00
Ivan K
bc6490b56e 🐛 fix(doh): Improve DoH server compatibility detection for quad9 2025-03-30 17:04:30 +03:00
itdoginfo
2f645d9151 v0.3.32 2025-03-30 16:03:49 +03:00
itdoginfo
94cc65001b Merge pull request #72 from itdoginfo/feature/no-more-cache
🐛 fix(podkop): Handle DNS check errors and timeouts properly
2025-03-30 16:02:44 +03:00
Ivan K
87caa70e97 feat(dns): Mask NextDNS ID in DNS availability check output 2025-03-30 14:53:01 +03:00
Ivan K
90d7c60fcb 🐛 fix(podkop): Handle DNS check errors and timeouts properly 2025-03-30 14:46:03 +03:00
itdoginfo
3f114b4710 v0.3.31 2025-03-30 12:41:40 +03:00
itdoginfo
b821abe82c Merge pull request #67 from itdoginfo/feature/no-more-cache
Feature/add DNS and bypass status checks to diagnostics
2025-03-30 12:37:58 +03:00
Ivan K
732cab2ef3 🐛 fix(podkop): fix typo in translation and dns check timeout 2025-03-30 09:54:31 +03:00
Ivan K
3b4ce9e7a3 feat: add visibility change event listener for diagnostics updates 2025-03-21 15:06:08 +03:00
Ivan K
69c4445c85 refactor: move buttons for NFT and DNSMasq checks 2025-03-21 14:54:52 +03:00
Ivan K
dcebc3d67d docs: update Russian translations for proxy configuration and add new translation strings 2025-03-21 14:53:24 +03:00
Ivan K
1be31eaf59 feat: add local DNS availability check and display in UI 2025-03-21 14:47:20 +03:00
Ivan K
023210e0f0 style: update text for Bypass Status to Main config 2025-03-21 14:21:35 +03:00
Ivan K
5ff832533e feat: add DNS and bypass status checks to diagnostics 2025-03-21 13:03:29 +03:00
Ivan K
5d2163515e refactor: improve caching prevention logic 2025-03-20 21:47:55 +03:00
Ivan K
5865706d0c feat: add timestamp to URL to prevent caching 2025-03-20 21:45:08 +03:00
itdoginfo
aabe1c53dc Update 2025-03-18 00:32:06 +03:00
itdoginfo
8e91b582ad #36 2025-03-18 00:31:56 +03:00
itdoginfo
62ce1f5acc v0.3.30 2025-03-17 14:44:07 +03:00
itdoginfo
93727ddeb5 Processing empty values 2025-03-17 14:43:33 +03:00
itdoginfo
98797d93b1 v0.3.29 2025-03-17 13:16:38 +03:00
itdoginfo
66c6e998a2 #38 #46 2025-03-17 13:14:37 +03:00
itdoginfo
3d9f82b571 Merge pull request #65 from itdoginfo/chore/fakeip-method
feat: add diagnostics functionality only in tab
2025-03-14 12:33:52 +03:00
Ivan K
38d082e236 feat: add diagnostics functionality only in tab 2025-03-14 09:50:28 +03:00
itdoginfo
9f5abcae6d v0.3.28 2025-03-13 19:34:52 +03:00
itdoginfo
7836d2c6ec Fix 2025-03-13 19:32:57 +03:00
itdoginfo
f46c934c59 Test 2025-03-13 19:30:17 +03:00
itdoginfo
23ed10d393 Added check version in Makefile 2025-03-13 19:28:31 +03:00
itdoginfo
26488baad3 Merge pull request #64 from itdoginfo/chore/fakeip-method
fix: fix enable/disable functionality to podkop service
2025-03-13 19:05:49 +03:00
Ivan K
c79016e456 feat: add createInitActionButton function to ButtonFactory 2025-03-13 10:35:48 +03:00
Ivan K
884bbfee42 fix: remove unused button creation code 2025-03-13 10:32:34 +03:00
Ivan K
1263b9b1b8 fix: fix enable/disable functionality to podkop service 2025-03-13 10:00:37 +03:00
Ivan K
23203fd7a1 feat: add createSystemButton function to ButtonFactory and flush cache button 2025-03-13 00:10:18 +03:00
itdoginfo
25c887a952 v0.3.27 2025-03-12 17:20:35 +03:00
itdoginfo
e7a3c7adf1 Merge pull request #63 from itdoginfo/chore/fakeip-method
feat: update DNS checks and improve FakeIP status reporting
2025-03-12 17:02:18 +03:00
Ivan K
3e96b9a1af feat: update DNS checks and improve FakeIP status reporting 2025-03-12 16:20:59 +03:00
itdoginfo
251f94cb88 v0.3.26 2025-03-12 14:59:03 +03:00
itdoginfo
44936c698e Merge pull request #62 from itdoginfo/chore/fakeip-method
feat: add CLI check for FakeIP functionality and update status display
2025-03-12 14:57:41 +03:00
Ivan K
0faaca12fc сhore: remove tabs 2025-03-12 14:54:56 +03:00
Ivan K
c6d1f05916 feat: add CLI check for FakeIP functionality and update status display 2025-03-11 19:14:21 +03:00
itdoginfo
57554d518b v0.3.25 2025-03-11 18:39:30 +03:00
itdoginfo
09d761956c Some fixes 2025-03-11 18:39:18 +03:00
itdoginfo
ada807fec3 v0.3.24 2025-03-07 14:46:45 +03:00
itdoginfo
b28a5f1293 New default TTL=60, DOH=8.8.8.8 2025-03-07 14:46:22 +03:00
itdoginfo
2332eae5ff Added dns and github checker. JSON file for custom URL lists 2025-03-07 14:45:36 +03:00
itdoginfo
a755b6661d Merge pull request #59 from itdoginfo/feat/multiple-mixed-inbounds
Add support for multiple mixed inbounds with unique ports
2025-03-07 13:10:32 +03:00
Nikita Skryabin
567ce52253 feat: add support for multiple mixed inbounds with unique ports 2025-03-06 22:54:25 +03:00
Nikita Skryabin
b736360b66 fix: ensure routing rule for mixed-in is always applied 2025-03-06 21:55:40 +03:00
itdoginfo
3b2a7ba8af Create /usr/bin/podkop 2025-03-05 01:08:30 +03:00
itdoginfo
c96de62d96 v0.3.22 2025-03-04 13:36:43 +03:00
itdoginfo
14b7fbe4f7 Fix cidr for all_traffic+exclude 2025-03-04 13:36:20 +03:00
itdoginfo
3d05fe8be4 0.3.21 2025-03-03 21:28:21 +03:00
itdoginfo
6ddf9d3b24 Fix section for all_traffic_ip 2025-03-03 21:28:12 +03:00
itdoginfo
b401243f74 0.3.20 2025-03-03 18:26:19 +03:00
itdoginfo
407ef404ac Fix ip_cidr+fakeip, all_traffic_from_ip_enabled list 2025-03-03 18:26:02 +03:00
itdoginfo
f2e45bbbb9 Fix default value 2025-03-03 11:21:49 +03:00
itdoginfo
c2b37a14f4 v0.3.19 2025-02-26 18:24:40 +03:00
itdoginfo
3d029edaea Update 2025-02-26 18:23:02 +03:00
itdoginfo
b86d6d6294 Merge pull request #52 from itdoginfo/fix/increase-timeout-safeexec
feat: add support for comments in proxy and domain/subnet configuration
2025-02-26 18:18:43 +03:00
Ivan K
5c48ead9e4 feat: add support for comments in proxy and domain/subnet configuration 2025-02-24 23:02:23 +03:00
Ivan K
53475b5e8a fix: increase timeout for safeExec function 2025-02-24 20:07:47 +03:00
Ivan K
59e1d75870 refactor: increase timeout for safeExec function 2025-02-24 19:37:59 +03:00
itdoginfo
3ec6cc4d84 0.3.18 2025-02-24 18:07:15 +03:00
itdoginfo
3413af9f94 Merge pull request #51 from itdoginfo/fix/vpn-devices
feat: add section_id parameter to getNetworkInterfaces function
2025-02-24 17:42:30 +03:00
Ivan K
76b5ceae5c feat: add section_id parameter to getNetworkInterfaces function 2025-02-24 17:39:56 +03:00
itdoginfo
99ccd9fbb3 0.3.17 2025-02-24 16:42:35 +03:00
itdoginfo
b82c6eb718 Merge pull request #50 from itdoginfo/fix/many-sni-support
feat: update network interface loading in podkop.js
2025-02-24 16:24:53 +03:00
Ivan K
ccc87d9aa0 feat: update network interface loading in podkop.js 2025-02-24 16:23:05 +03:00
itdoginfo
8bcdee87f5 0.3.16 2025-02-24 15:39:02 +03:00
itdoginfo
f77ef5626b default dns options 2025-02-24 15:38:50 +03:00
itdoginfo
b50a21ded7 rm wget_github 2025-02-24 15:38:24 +03:00
itdoginfo
a831054e5e Merge pull request #48 from itdoginfo/fix/many-sni-support
feat: add status panels and utility functions for better diagnostics UI
2025-02-24 10:05:05 +03:00
Ivan K
a8dbff816c fix: correct logic for checking fakeipStatus state 2025-02-24 10:00:51 +03:00
Ivan K
171381fa18 refactor: improve error handling and code readability in podkop.js and update init.d script to check sing-box status 2025-02-23 22:56:01 +03:00
Ivan K
b806586a5a fix: проверки диагностики только при активной вкладке 2025-02-23 18:13:41 +03:00
Ivan K
9e2b192181 feat: add status panels and utility functions for better diagnostics UI 2025-02-23 13:35:30 +03:00
itdoginfo
c5be041664 Update 2025-02-23 00:11:52 +03:00
itdoginfo
445ad6d3d2 Update todo 2025-02-22 17:31:17 +03:00
itdoginfo
9203315107 Merge pull request #42 from itdoginfo/fix/many-sni-support
fix: add missing URL decoding for semicolon
2025-02-22 14:52:03 +03:00
Ivan K
d8d8d79d68 feat: add support for outbound JSON configuration in sing-box 2025-02-22 14:15:27 +03:00
Ivan K
615928db4e Merge remote-tracking branch 'origin/main' into fix/many-sni-support 2025-02-22 12:49:59 +03:00
Ivan K
7697754a73 refactor: replace fs.exec with safeExec for command execution with timeout 2025-02-22 12:45:25 +03:00
Ivan K
25107a0481 refactor: simplify label fetching and decoding in podkop.js 2025-02-22 09:52:04 +03:00
itdoginfo
5f5b1cbe1f Warnning for friendlywrt, http-dns-proxy. Validation domains in local file 2025-02-22 00:04:24 +03:00
Ivan K
a278918e77 feat: add timeout and chunking to proxy label fetching 2025-02-21 17:55:39 +03:00
itdoginfo
2074ccecce 0.3.15 2025-02-21 17:41:35 +03:00
itdoginfo
06f9bee038 #42 2025-02-21 17:40:52 +03:00
Ivan K
febb69d0be fix: add enable/disable button for Podkop service 2025-02-21 17:38:57 +03:00
Ivan K
1a6ee45612 fix: add function to dynamically fetch network interfaces for VPN configuration 2025-02-21 17:34:31 +03:00
itdoginfo
891b8f713d Fix 2025-02-21 16:07:27 +03:00
itdoginfo
b96552fb49 Fix #41 2025-02-21 16:06:17 +03:00
itdoginfo
ce9a7cdc45 Fix \n 2025-02-21 15:40:22 +03:00
itdoginfo
6071a96e9c 0.3.14 2025-02-21 15:37:43 +03:00
Ivan K
000d2f8e18 fix: add missing URL decoding for semicolon 2025-02-21 15:37:30 +03:00
itdoginfo
e17422a0cf Fix #37 #41 2025-02-21 15:37:20 +03:00
itdoginfo
2e78b2b4b8 Merge pull request #41 from itdoginfo/refactor/deduplicate-sections
Refactor/deduplicate sections
2025-02-21 14:55:03 +03:00
Ivan K
b84f3b6782 feat: add get_proxy_label function to podkop init script 2025-02-21 12:01:46 +03:00
itdoginfo
0f66305e50 Fix 2025-02-21 11:53:08 +03:00
Ivan K
a32a5c600b fix: update domain validation regex to allow single-level domains 2025-02-21 11:47:14 +03:00
Ivan K
89737efcbc refactor: refactor checkFakeIP to return a promise and update updateDiagnostics to use async/await 2025-02-21 11:22:37 +03:00
Ivan K
4608bc31cd refactor: update podkop.js to modularize configuration sections and improve validation logic 2025-02-21 11:09:47 +03:00
itdoginfo
d9e9f2dfe4 Update 2025-02-21 00:50:53 +03:00
Nikita Skryabin
bb9318e96f Merge pull request #37 from vernette/feature/fakeip-cache-path-and-ttl
feat(podkop): add configurable cache file path and dns rewrite_ttl options
2025-02-21 00:20:15 +03:00
Nikita Skryabin
7ff49c3e4e chore(init.d/podkop): remove unused cache file path and constant 2025-02-21 00:17:43 +03:00
Nikita Skryabin
134a79cb3b refactor(podkop.js): remove redundant path validation logic 2025-02-20 23:56:20 +03:00
Nikita Skryabin
560dda8604 feat(podkop): add translations for cache file and rewrite ttl options 2025-02-20 23:49:41 +03:00
Nikita Skryabin
255c08a6de feat(podkop.js): add validation for cache file path to ensure it meets specific criteria 2025-02-20 23:44:32 +03:00
Nikita Skryabin
1f3a65347e feat(podkop): add DNS Rewrite TTL configuration option 2025-02-20 23:27:50 +03:00
Nikita Skryabin
ec936e2369 feat(podkop): add configurable cache file path support 2025-02-20 22:49:58 +03:00
itdoginfo
cee934d139 Merge pull request #34 from itdoginfo/feature/fakeip-updater
feat: enhance FakeIP status check with periodic updates
2025-02-20 21:59:04 +03:00
Ivan K
a25c6b8013 feat: enhance FakeIP status check with periodic updates 2025-02-20 20:28:51 +03:00
itdoginfo
ec3a281cef v0.3.13 2025-02-20 17:22:15 +03:00
itdoginfo
86947e7dee Fix dns_server value 2025-02-20 17:22:03 +03:00
itdoginfo
ff5d017acc Update and rm install 0.2.5 2025-02-20 16:50:06 +03:00
itdoginfo
22d919657c Merge remote-tracking branch 'origin/main' 2025-02-20 16:48:20 +03:00
itdoginfo
3271f23ae0 Fix noresolv bakup 2025-02-20 16:45:22 +03:00
itdoginfo
35ea1a14cf Merge pull request #33 from vernette/feature/dns-server-selection
feat(podkop): add DNS server and protocol selection options
2025-02-20 16:39:05 +03:00
unknown
51a9cc5934 feat(podkop.po): add translations for DNS server address validation messages 2025-02-20 16:34:55 +03:00
unknown
e1df26e62b feat(podkop.js): add DNS server validation for IP and domain formats 2025-02-20 16:33:23 +03:00
unknown
75b8bef0e0 fix(podkop.js): update DNS protocol type and server labels to use translation function 2025-02-20 16:07:38 +03:00
unknown
1a6b0cac46 chore(init.d/podkop): remove redundant comments 2025-02-20 16:03:15 +03:00
unknown
e49bd91109 feat(podkop.po): add translations for DNS protocol and server options 2025-02-20 16:01:23 +03:00
unknown
85642a2585 feat(podkop.pot): add new DNS protocol and server options for translation 2025-02-20 16:01:12 +03:00
unknown
c31785d20e feat(init.d/podkop): add DNS resolver discovery and dynamic configuration 2025-02-20 15:57:52 +03:00
unknown
a0af04037a feat(podkop.js): add DNS protocol type and server options to configuration 2025-02-20 15:57:18 +03:00
itdoginfo
51fb10e30e fix 2025-02-20 00:43:39 +03:00
itdoginfo
069ea41ef8 Hide don't touch my dhcp 2025-02-20 00:25:24 +03:00
itdoginfo
7ee92123bc Fix use-application-dns.net 2025-02-19 23:08:38 +03:00
itdoginfo
5fd0e23cf9 Added backup dhcp and don't touch dhcp. Firefox disable doh FQDN moved to sing-box 2025-02-19 22:40:17 +03:00
itdoginfo
9b25669c8f Merge #30 and #31 2025-02-19 19:48:36 +03:00
itdoginfo
4b020671cc Merge pull request #30 from itdoginfo/feature/web-versions-view
feat: add version information tab to diagnostics
2025-02-19 19:42:40 +03:00
Ivan K
6222221847 docs: update Russian translations and add new strings for FakeIP status check 2025-02-19 19:18:22 +03:00
itdoginfo
6fa215e343 Merge pull request #31 from vernette/feature/dns-check
feat(podkop): add secure DNS probe domain configuration
2025-02-19 18:23:34 +03:00
Nikita Skryabin
a33835415f feat(init.d/podkop): add secure DNS probe domain configuration 2025-02-19 13:11:22 +03:00
Ivan K
f76c657bd7 style: remove unused CSS and JavaScript for tooltips 2025-02-18 22:11:58 +03:00
Ivan K
cceedd6c17 docs: update Russian translations for error messages and UI strings 2025-02-18 21:27:56 +03:00
Ivan K
8fa1986961 docs: update Russian translations for luci-app-podkop 2025-02-18 21:20:15 +03:00
Ivan K
8dec59d118 docs: update Russian translations for new UI elements in luci-app-podkop 2025-02-18 21:17:02 +03:00
Ivan K
c1fac487c7 style: add tooltip functionality and adjust CSS for better UI 2025-02-18 21:05:02 +03:00
Ivan K
d934bcc5e9 refactor: add async to diagnostics section UI 2025-02-18 18:56:34 +03:00
Ivan K
fc99bd7aaa feat: add spacing and line break to diagnostic tools section 2025-02-18 18:28:52 +03:00
Ivan K
b6cf73b974 feat: add service status and diagnostic tools to podkop UI 2025-02-18 18:23:29 +03:00
Ivan K
6df7c8abf8 feat: add URL validation for Shadowsocks and VLESS configurations from examples 2025-02-18 17:18:34 +03:00
Ivan K
8eb97a8023 Merge remote-tracking branch 'origin/main' into feature/web-versions-view 2025-02-18 14:05:22 +03:00
Ivan K
cd43449585 feat: add version information tab to diagnostics 2025-02-18 13:59:04 +03:00
itdoginfo
16c174d624 v0.3.10 2025-02-18 13:21:52 +03:00
itdoginfo
1c02a2208b Stop service before rm 2025-02-18 13:21:31 +03:00
itdoginfo
2c93e98755 Merge pull request #29 from itdoginfo/feature/web-versions-view
feat: add validation and warning messages for regional lists
2025-02-18 13:16:04 +03:00
Ivan K
66b179f282 fix: add extra configurations section to podkop.js 2025-02-18 13:01:15 +03:00
itdoginfo
4bbaae776c Merge pull request #28 from vernette/main
fix(install): resolve update failure due to improper cleanup
2025-02-18 12:52:30 +03:00
Ivan K
e31f313819 feat: add validation and warning messages for regional options in podkop.js 2025-02-18 12:49:24 +03:00
unknown
bd0e33781f fix(install): correct continue logic for existing package files 2025-02-18 12:06:33 +03:00
Nikita Skryabin
ade2b844ec fix(init.d/podkop): change rm command to remove only *.lst files in /tmp/podkop directory 2025-02-18 10:05:34 +03:00
Nikita Skryabin
6f997a6e73 refactor(install.sh): improve download retry logic 2025-02-18 09:59:01 +03:00
Nikita Skryabin
744de6aec2 chore(install.sh): replace rm command with find 2025-02-18 09:45:49 +03:00
itdoginfo
ae06de8189 v0.3.9 2025-02-17 23:36:53 +03:00
itdoginfo
1663f6665f Fix #27, added copy and div 2025-02-17 23:36:37 +03:00
itdoginfo
b005cbe50e Fix rule for section custom_download 2025-02-17 19:42:39 +03:00
itdoginfo
6c752d59ce Merge pull request #27 from VizzleTF/main
Поправил диагностику
2025-02-17 19:41:26 +03:00
itdoginfo
dbdd0560bf Added CODEOWNERS 2025-02-17 19:21:07 +03:00
Ivan K
aeacd9d8fd docs: update README.md with installation instructions 2025-02-17 19:09:52 +03:00
Ivan K
ded0bff23a chore: update build workflow to simplify install script generation 2025-02-17 19:09:09 +03:00
Ivan K
80ab7caee9 chore: update build workflow to use git commit -am 2025-02-17 18:49:29 +03:00
Ivan K
516063310a refactor: update install script generation to use current version tag 2025-02-17 18:40:54 +03:00
Ivan K
c6d72aa781 docs: update README with installation instructions for specific version 2025-02-17 18:28:53 +03:00
Ivan K
91fa2a2859 Merge branch 'itdoginfo:main' into main 2025-02-17 18:08:37 +03:00
Ivan K
13e84afcf0 feat: add new diagnostic checks and update install script 2025-02-17 18:08:13 +03:00
itdoginfo
88c160d3f8 Fix 2025-02-17 17:22:45 +03:00
itdoginfo
ebd185f633 Added install for 0.2.5 2025-02-17 16:34:27 +03:00
itdoginfo
e86bffb720 v0.3.8 2025-02-17 16:04:34 +03:00
itdoginfo
fb65b63639 Merge pull request #25 from VizzleTF/main
docs(ru): add new translations for podkop configuration
2025-02-17 15:51:19 +03:00
itdoginfo
daf7e30ed1 dnsmasq add 8.8.8.8. Validate domain_list 2025-02-17 15:22:55 +03:00
itdoginfo
dd62ecfbeb Check sing-box 2025-02-17 13:20:28 +03:00
Ivan K
41cb8cd650 Merge branch 'itdoginfo:main' into main 2025-02-17 13:08:35 +03:00
Ivan K
b7ad256986 docs(ru): add new translations for podkop configuration 2025-02-17 13:07:11 +03:00
itdoginfo
f88ffa1893 Fix install logic 2025-02-17 12:44:48 +03:00
itdoginfo
6f604ca765 Update 2025-02-16 17:53:14 +03:00
itdoginfo
52c6eeae12 Fix version 2025-02-16 17:52:57 +03:00
itdoginfo
778f2897bc Fix check iptables 2025-02-16 17:41:58 +03:00
111 changed files with 16069 additions and 3611 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @itdoginfo

74
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
---
name: 🐛 Сообщение об ошибке
description: Создавайте только, если проблема точно не на вашей стороне.
title: "[BUG] "
labels: ["bug"]
assignees: []
body:
- type: markdown
attributes:
value: |
Спасибо за создание отчета об ошибке!
Перед отправкой, пожалуйста:
- Проверьте [существующие issues](https://github.com/itdoginfo/podkop/issues)
- Просмотрите [документацию](https://podkop.net)
- type: textarea
id: description
attributes:
label: 📝 Описание проблемы
description: Четкое и краткое описание того, что не работает
placeholder: Опишите проблему
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Шаги для воспроизведения
description: Шаги для воспроизведения проблемы. Если вы настраваете что-то по мануалу, приложите ссылку на него.
placeholder: |
1.
2.
3.
4.
validations:
required: true
- type: textarea
id: expected
attributes:
label: ✅ Ожидаемое поведение
description: Четкое и краткое описание того, что должно было произойти
placeholder: Опишите ожидаемое поведение
validations:
required: true
- type: textarea
id: environment
attributes:
label: 🖥️ Информация о системе
description: |
Информация о вашей системе (заполните всё применимое)
value: |
- **OpenWrt версия**:
- **Podkop версия**:
- **Роутер модель**:
- **Sing-box версия**:
render: markdown
validations:
required: true
- type: textarea
id: config
attributes:
label: ⚙️ Конфигурация
description: |
Релевантные части конфигурации (удалите чувствительную информацию!)
placeholder: |
Например:
- Содержимое /etc/config/podkop
- Конфигурация sing-box (если релевантно)
- Дополнительные конфиги, которые потребуются wireless/network/dhcp и т.д.
render: shell

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: 💬 Если у вас что-то не работает, прежде всего прочитайте README проекта
url: https://github.com/itdoginfo/podkop
about: README проекта
- name: 📚 Если вы не нашли в README документацию, то вот ссылка на неё
url: https://podkop.net
about: Официальная документация PodKop

View File

@@ -0,0 +1,68 @@
---
name: ✨ Запрос новой функции
description: Предложите новую функцию или улучшение для Podkop
title: "[FEATURE] "
labels: ["enhancement", "needs-discussion"]
assignees: []
body:
- type: markdown
attributes:
value: |
Спасибо за предложение новой функции!
Перед отправкой, пожалуйста:
- Проверьте [существующие запросы](https://github.com/itdoginfo/podkop/issues?q=is%3Aissue+label%3Aenhancement)
- Убедитесь, что функции не существует в [документации](https://podkop.net)
- type: textarea
id: summary
attributes:
label: Краткое описание
description: Краткое описание предлагаемой функции
placeholder: В одном предложении опишите, что вы хотите добавить...
validations:
required: true
- type: textarea
id: problem
attributes:
label: Проблема, которую решает
description: |
Описание проблемы или неудобства, которое решит эта функция
placeholder: |
Сейчас нет возможности [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: 💡 Предлагаемое решение
description: Четкое и краткое описание того, что вы хотите реализовать
placeholder: |
Я хочу, чтобы Podkop мог [...]
Предлагаю добавить функцию, которая [...]
Можно было бы улучшить [...] путем [...]
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Workaround
description: |
Опишите альтернативные решения или функции, которые вы рассматривали
Есть ли обходные пути, которые вы используете сейчас?
placeholder: |
Сейчас я решаю это проблему путем [...]
Альтернативой могло бы быть [...]
Пробовал использовать [...], но это не подходит потому что [...]
- type: textarea
id: implementation
attributes:
label: Идеи реализации (опционально)
description: |
Если у вас есть идеи о том, как это можно реализовать, поделитесь ими. Помните про ограничения LuCI.
placeholder: |
Это можно реализовать с помощью [...]

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
# Описание изменений
Краткое описание ваших изменений и их цель.
## Что изменено
Детальное описание изменений:
-
-
-
(Этим вы экономите время ревьювера)

View File

@@ -10,12 +10,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.1
with:
fetch-depth: 0
- name: Extract version
id: version
run: |
VERSION=$(git describe --tags --exact-match 2>/dev/null || echo "dev_$(date +%d%m%Y)")
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v6.9.0
with:
context: .
tags: podkop:ci
build-args: |
PKG_VERSION=${{ steps.version.outputs.version }}
- name: Create Docker container
run: docker create --name podkop podkop:ci

78
.github/workflows/frontend-ci.yml vendored Normal file
View File

@@ -0,0 +1,78 @@
name: Frontend CI
on:
pull_request:
paths:
- 'fe-app-podkop/**'
- '.github/workflows/frontend-ci.yml'
jobs:
frontend-checks:
name: Frontend Quality Checks
runs-on: ubuntu-24.04
defaults:
run:
working-directory: fe-app-podkop
steps:
- name: Checkout code
uses: actions/checkout@v5.0.0
- name: Setup Node.js
uses: actions/setup-node@v5.0.0
with:
node-version: '22'
- name: Enable Corepack
run: corepack enable
- name: Get yarn cache directory path
id: yarn-cache-dir-path
working-directory: fe-app-podkop
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- name: Cache yarn dependencies
uses: actions/cache@v4.3.0
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('fe-app-podkop/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Check formatting
id: format
run: |
yarn format
if ! git diff --exit-code; then
echo "::error::Code is not formatted. Run 'yarn format' locally."
exit 1
fi
- name: Run linter
run: yarn lint --max-warnings=0
- name: Run tests
run: yarn test --run
- name: Build project
id: build
run: |
yarn build
if ! git diff --exit-code; then
echo "::error::Build generated changes. Check build output."
exit 1
fi
- name: Summary
if: always()
run: |
echo "## Frontend CI Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Format check: ${{ steps.format.outcome }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Lint check: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Tests: ${{ job.status }}" >> $GITHUB_STEP_SUMMARY
echo "- ✅ Build: ${{ steps.build.outcome }}" >> $GITHUB_STEP_SUMMARY
echo "![Success](https://cdn2.combot.org/boratbrat/webp/6xf09f988f.webp)" >> $GITHUB_STEP_SUMMARY

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea
fe-app-podkop/node_modules
fe-app-podkop/.env

View File

@@ -1,6 +1,7 @@
FROM openwrt/sdk:x86_64-v23.05.5
FROM itdoginfo/openwrt-sdk:24.10.1
RUN ./scripts/feeds update -a && ./scripts/feeds install luci-base && mkdir -p /builder/package/feeds/utilites/ && mkdir -p /builder/package/feeds/luci/
ARG PKG_VERSION
ENV PKG_VERSION=${PKG_VERSION}
COPY ./podkop /builder/package/feeds/utilites/podkop
COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop

3
Dockerfile-SDK Normal file
View File

@@ -0,0 +1,3 @@
FROM openwrt/sdk:x86_64-v24.10.1
RUN ./scripts/feeds update -a && ./scripts/feeds install luci-base && mkdir -p /builder/package/feeds/utilites/ && mkdir -p /builder/package/feeds/luci/

262
README.md
View File

@@ -1,251 +1,55 @@
# Вещи, которые вам нужно знать перед установкой
- Это альфа версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
- Основной функционал работает, но побочные штуки сейчас могут сбоить.
- При обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
- Необходимо минимум 15МБ свободного места на роутере. Роутерами с флешками на 16МБ сразу мимо.
- Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
- При возникновении проблем, нужен технически грамотный фидбэк в чат.
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
- Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
- Необходимо минимум 25МБ свободного места на роутере. Роутеры с флешками на 16МБ сразу мимо.
- При старте программы редактируется конфиг Dnsmasq.
- Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен.
- Информация здесь может быть устаревшей. Все изменения фиксируются в телеграм-чате https://t.me/itdogchat - топик **Podkop**.
- Если у вас установлен Getdomains, его следует удалить.
- Информация здесь может быть устаревшей. Все изменения фиксируются в [телеграм-чате](https://t.me/itdogchat/81758/420321).
- [Если у вас не что-то не работает.](https://podkop.net/docs/diagnostics/)
- Если у вас установлен Getdomains, [его следует удалить](https://github.com/itdoginfo/domain-routing-openwrt?tab=readme-ov-file#%D1%81%D0%BA%D1%80%D0%B8%D0%BF%D1%82-%D0%B4%D0%BB%D1%8F-%D1%83%D0%B4%D0%B0%D0%BB%D0%B5%D0%BD%D0%B8%D1%8F).
- Требуется версия OpenWrt 24.10.
# Удаление GetDomains скриптом
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/domain-routing-openwrt/refs/heads/master/getdomains-uninstall.sh)
```
Оставляет туннели, зоны, forwarding. А также stubby и dnscrypt. Они не помешают. Конфиг sing-box будет перезаписан в podkop.
# Документация
https://podkop.net/
# Установка Podkop
Пакет работает на всех архитектурах.
Тестировался на OpenWrt 23.05 и OpenWrt 24.10.
Полная информация в [документации](https://podkop.net/docs/install/)
Поддержки APK на данный момент нет. APK будет сделан после того как разгребу основное.
## Автоматическая
Вкратце, достаточно одного скрипта для установки и обновления:
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
```
Скрипт также предложит выбрать, какой туннель будет использоваться. Для выбранного туннеля будут установлены нужные пакеты, а для Wireguard и AmneziaWG также будет предложена автоматическая настройка - прямо в консоли скрипт запросит данные конфига. Для AmneziaWG можно также выбрать вариант с использованием конфига обычного Wireguard и автоматической обфускацией до AmneziaWG.
Для AmneziaWG скрипт проверяет наличие пакетов под вашу платформу в [стороннем репозитории](https://github.com/Slava-Shchipunov/awg-openwrt/releases), так как в официальном репозитории OpenWRT они отсутствуют, и автоматически их устанавливает.
## Вручную
Сделать `opkg update`, чтоб установились зависимости.
Скачать пакеты `podkop_*.ipk` и `luci-app-podkop_*.ipk` из релиза. `opkg install` сначала первый, потом второй.
# Обновление
Та же самая команда, что для установки. Скрипт обнаружит уже установленный podkop и предложит обновиться.
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
```
# Удаление
```
opkg remove luci-app-podkop podkop
```
Если был установлен русский язык
```
opkg remove luci-i18n-podkop-ru
```
# Использование
Конфиг: /etc/config/podkop
Luci: Services/podkop
## Режимы
### Proxy
Для VLESS и Shadowsocks. Другие протоколы тоже будут, кидайте в чат примеры строк без чувствительных данных.
В этом режиме просто копируйте строку в **Proxy String** и из неё автоматически настроится sing-box.
### VPN
Здесь у вас должен быть уже настроен WG/OpenVPN/OpenConnect etc, зона Zone и Forwarding не обязательны.
Просто выбрать интерфейс из списка.
## Настройка доменов и подсетей
**Community Lists** - Включить списки комьюнити
**Subnets list enable** - Включить подсети из общего списка, выбрать из предложенных.
**Custom domains enable** - Добавить свои домены
**Custom subnets enable** - Добавить подсети или IP-адреса. Для подсетей задать маску.
# Известные баги
- [x] Не работает proxy при режимах main vpn, second proxy
- [x] Не всегда отрабатывает ucitrack (применение настроек из luci). Не удаётся повторить
- [x] All traffic for IP ломает инет на клиенте. Proxy mode
- [x] Не отрабатывает рестарт, при awg и не применяются изменения при awg
- [x] awg работает не стабильно
- [x] Сеть рестартится при любом раскладе
- [x] Выкл-вкл wg через luci не отрабатывает поднятие маршрута
- [ ] Если eof после последней строки в rt_tables, то скрипт не добавляет перенос строки
- [ ] Парсинг VLESS не отрабатывает, если в SNI два домена. Пример `sni=telegram.org%3Bwww.telegram.org`
- [ ] `service network restart` ломает маршруты при sing-box
- [ ] Совпадение секции с ruleset ломает конфиг sing-box
- [ ] В каких-то случаях плохо отрабатывает localfile
- [ ] exit 1 если в конфиге присуствует
```
option doh_backup_noresolv '0'
list doh_backup_server ''
list doh_backup_server ''
list doh_server '127.0.0.1#5053'
list doh_server '127.0.0.1#5054'
```
# ToDo
Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме.
Сделано
- [x] Скрипт для автоматической установки.
- [x] Подсети дискорда.
- [x] Удаление getdomains через скрипт. Кроме туннеля и sing-box.
- [x] Дополнительная вкладка для ещё одного туннеля. Домены, подсети.
- [x] Улучшение скрипта автоматической установки. Спрашивать про туннели.
- [x] Зависимость от dnsmasq-full
- [x] Весь трафик для устойства пускать в туннель\прокси
- [x] Исключение для IP, не ходить в туннель\прокси совсем 0x0
- [x] Врубать галочкой yacd в sing-box
- [x] Свои списки. Просто список доменов с переносом строки
- [x] Свои списки ipv4
- [x] В nft разделить правило tproxy на маркировку и tproxy
- [x] Вернуть две цепочки nft
- [x] Ntp (порт 123) делать маркировку 0x0. По галке
- [x] Открытый прокси порт на роутере для браузеров
- [x] Автонастройка wireguard по примеру getdomains
- [x] Автонастройка awg по примеру getdomains
- [x] RU перевод
- [x] Переделать на PROCD и выкинуть ucitrack.
- [x] Нужен дебаг. Restart ucitrack в отдельный скрипт postinst, не отрабатывает.
- [x] Закомментировать дефолтные значения у list. interface поставить в пустое.
- [x] Скрипт установки: проверка установлен ли уже podkop. Если да, то просто предлагать обновится без установки тунелей и прокси.
Основные задачи в issues.
Приоритет 1
- [x] Изменить название "Alternative Config"
- [x] "domain_service_enabled" Добавить _second
- [x] Установка Ru пакета в install.sh
- [x] Правка nft mark, tproxy
- [x] Правка перевода минимальная
- [x] Вставлять готовый outdbound вместо строки. Отдельная галка, которая в идеале должны скрывать поле для строки
- [ ] udp over tcp для ss сделать с выбором:
1) отключен (ПО на сервере -Shadowsocks)
2) включен, версия 2 (новые релизы xray-core, sing-box на сервере)
3) включен, версия 1 (старые релизы xray, sing-box на сервере)
Проблема в том, что это нужно только если SS. Выставлять выбор при парсинг из конфига вопрос можно ли. Если совсем тупо - сделать костыль в допонительные настройки
- [x] Проверка места в скрипте install. Если доступно меньше 20MB - exit 1 c выводом колько надо и сколько доступно. + показ модели роутера
- [ ] Правило запрещающее QUIC
- [ ] Проверить обновление списков, отрабатывает ли
- [ ] Проверка на ванильную openwrt
- [ ] Проверка откуда установлен sing-box. Например, проверять установлен ли он из официального репозитория
- [x] TG в сервисы
- [ ] Выбор ткуда направлять трафик в туннель. В том числе чтоб откуда угодно, а не только br-lan
- [ ] Диагностика: Proxy check completed successfully предположительно не показывает IP, если вернулся это IPv6.
- [ ] Диагностика: podkop_domains: 0 elements как проверять что доходят запросы при fakeip? Мб врубать логи dnsmasq и их чекать.
- [ ] Сделать галку запрещающую подкопу редачить dhcp. Допилить в исключение вместе с пустыми полями proxy и vpn
- [ ] Валидации предустановленных значений. Если прописаны другие, то вывод в лог о неизвестной переменной и продолжение работы
- [ ] Добавление в список доменов домены первого уровня (LuCI)
## Рефактор
- [x] Очевидные повторения в `/usr/bin/podkop` загнать в переменые
- [x] Возможно поменять структуру
Приоритет 2
- [x] Списки доменов и подсетей с роутера
- [ ] Кнопка обновления списка доменов и подсетей. Запихнуть в главное меню
- [ ] IPv6
## Списки
- [x] CloudFront
- [x] DO
- [x] HODCA
Wiki
- [x] Тема
- [x] Изначальное наполнение
## Будущее
- [ ] [Подписка](https://github.com/itdoginfo/podkop/issues/118). Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров.
- [x] Опция, когда все запросы (с роутера в первую очередь), а не только br-lan идут в прокси. С этим связана #95. Требуется много переделать для nftables.
- [ ] Весь трафик в Proxy\VPN. Вопрос, что делать с экстрасекциями в этом случае. FakeIP здесь скорее не нужен, а значит только main секция остаётся. Всё что касается fakeip проверок, придётся выключать в этом режиме.
- [x] Поддержка Source format. Нужна расшифровка в json и если присуствуют подсети, заносить их в custom subnet nftset.
- [x] Переделывание функции формирования кастомных списков в JSON. Обрабатывать сразу скопом, а не по одному.
- [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусcтвенно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. [Issue](https://github.com/itdoginfo/podkop/issues/111)
- [x] Формирование конфига sing-box в /tmp
- [ ] Галочка, которая режет доступ к doh серверам.
- [ ] IPv6. Только после наполнения Wiki.
Низкий приоритет
- [x] Переменная, раз во сколько часов обновлять списки
- [ ] Галочка, которая режет доступ к doh серверам
- [ ] Свой конфиг sing-box
- [x] Поменять curl на wget, убрать зависимость. Проверять доступность списков лучше всего curl`ом
Рефактор
- [ ] Handle для sing-box
- [ ] Handle для dnsmasq
- [ ] Формирование json для sing-box на уровне jq, а не шаблонов
## Тесты
- [ ] Unit тесты (BATS)
- [ ] Интеграционые тесты бекенда (OpenWrt rootfs + BATS)
Хз как сделать
- [ ] Добавить label от конфига vless\ss\etc в luci.
# Разработка
Есть два варианта:
- Просто поставить пакет на роутер или виртуалку и прям редактировать через SFTP (opkg install openssh-sftp-server)
- SDK, чтоб собирать пакеты
Для сборки пакетов нужен SDK, один из вариантов скачать прям файл и разархивировать
https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/
Нужен файл с SDK в имени
```
wget https://downloads.openwrt.org/releases/23.05.5/targets/x86/64/openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz
tar xf openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz
mv openwrt-sdk-23.05.5-x86-64_gcc-12.3.0_musl.Linux-x86_64 SDK
```
Последнее для удобства.
Создаём директорию для пакета
```
mkdir package/utilites
```
Симлинк из репозитория
```
ln -s ~/podkop/podkop package/utilites/podkop
ln -s ~/podkop/luci-app-podkop package/luci-app-podkop
```
В первый раз для сборки luci-app необходимо обновить пакеты
```
./scripts/feeds update -a
```
Для make можно добавить флаг -j N, где N - количество ядер для сборки. Первый раз пройдёт быстрее.
При первом make выводится менюшка, можно просто save, exit и всё. Первый раз долго грузит зависимости.
Сборка пакета. Сами пакеты собираются быстро.
```
make package/podkop/{clean,compile} V=s
```
Также для luci
```
make package/luci-app-podkop/{clean,compile} V=s
```
.ipk лежат в `bin/packages/x86_64/base/`
## Примеры строкs
https://github.com/itdoginfo/podkop/blob/main/String-example.md
## Ошибки
```
Makefile:17: /SDK/feeds/luci/luci.mk: No such file or directory
make[2]: *** No rule to make target '/SDK/feeds/luci/luci.mk'. Stop.
time: package/luci/luci-app-podkop/clean#0.00#0.00#0.00
ERROR: package/luci/luci-app-podkop failed to build.
make[1]: *** [package/Makefile:129: package/luci/luci-app-podkop/clean] Error 1
make[1]: Leaving directory '/SDK'
make: *** [/SDK/include/toplevel.mk:226: package/luci-app-podkop/clean] Error 2
```
Не загружены пакеты для luci
## make зависимости
https://openwrt.org/docs/guide-developer/toolchain/install-buildsystem
Ubuntu
```
sudo apt update
sudo apt install build-essential clang flex bison g++ gawk \
gcc-multilib g++-multilib gettext git libncurses-dev libssl-dev \
python3-distutils rsync unzip zlib1g-dev file wget
```
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/itdoginfo/podkop)

View File

@@ -1,63 +1,76 @@
# Shadowsocks
Тут всё просто
## Shadowsocks-old
## Shadowsocks
```
ss://YWVzLTI1Ni1nY206RmJwUDJnSStPczJKK1kzdkVhTnVuOUZ2ZjJZYUhNUlN1L1BBdEVqMks1VT0@example.com:80?type=tcp#example-ss-old
ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206ZG1DbHkvWmgxNVd3OStzK0dGWGlGVElrcHc3Yy9xQ0lTYUJyYWk3V2hoWT0@127.0.0.1:25144?type=tcp#shadowsocks-no-client
ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206S3FiWXZiNkhwb1RmTUt0N2VGcUZQSmJNNXBXaHlFU0ZKTXY2dEp1Ym1Fdz06dzRNMEx5RU9OTGQ5SWlkSGc0endTbzN2R3h4NS9aQ3hId0FpaWlxck5hcz0@127.0.0.1:26627?type=tcp#shadowsocks-client
ss://2022-blake3-aes-256-gcm:dmCly/Zh15Ww9+s+GFXiFTIkpw7c/qCISaBrai7WhhY=@127.0.0.1:27214?type=tcp#shadowsocks-plain-user
```
## Shadowsocks-2022
## VLESS
```
ss://2022-blake3-aes-128-gcm:5NgF%2B9eM8h4OnrTbHp%2B8UA%3D%3D%3Am8tbs5aKLYG7dN9f3xsiKA%3D%3D@example.com:80#example-ss2022
# tcp
vless://94792286-7bbe-4f33-8b36-18d1bbf70723@127.0.0.1:34520?type=tcp&encryption=none&security=none#vless-tcp-none
vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni=google.com&sid=f6&spx=%2F#vless-tcp-reality
vless://2e9e8288-060e-4da2-8b9f-a1c81826feb7@127.0.0.1:19316?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-tcp-tls
vless://0235c833-dc29-4202-8a7b-1bbba5b516a2@127.0.0.1:22993?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#vless-tcp-tls-insecure
vless://17776137-e747-4268-a84d-99fd798accac@127.0.0.1:48076?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com&ech=AFP%2BDQBPAAAgACDJXiKG5eoCHfd1MbMxgccxgrbGisBPPe3bz1KVIETUXQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAAAAAA%3D%3D#vless-tcp-tls-ech
# mKCP
vless://72e201d7-7841-4a32-b266-4aa3eb776d51@127.0.0.1:17270?type=kcp&encryption=none&headerType=none&seed=AirziWi4ng&security=none#vless-mKCP
# WebSocket
vless://d86daef7-565b-4ecd-a9ee-bac847ad38e6@127.0.0.1:12928?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=none#vless-websocket-none
vless://fe0f0941-09a9-4e46-bc69-e00190d7bb9c@127.0.0.1:10156?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-websocket-tls
vless://599e8659-e2ef-47d9-bf72-2f9b4b673474@127.0.0.1:36567?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#vless-websocket-tls-insecure
vless://4d21ce62-8723-4c4d-93e3-d586b107aa40@127.0.0.1:51394?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com&ech=AF3%2BDQBZAAAgACD7fjrtDMlcigKXFBKoLn6UDB9%2BWR6HBZpY96DlBiD%2BIwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D#vless-websocket-tls-ech
# 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://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
# HTTPUpgrade
vless://2b98f144-847f-42f7-8798-e1a32d27bdc7@127.0.0.1:47154?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=none#vless-httpupgrade-none
vless://76dbd0ff-1a35-4f0c-a9ba-3c5890b7dea6@127.0.0.1:50639?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-httpupgrade-tls
vless://6d229881-50ed-4f3f-995d-bd3e725fdbff@127.0.0.1:57616?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#vless-httpupgrade-tls-insecure
vless://1897e9e4-6f5d-4a85-9512-9192e76c3f04@127.0.0.1:38658?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com&ech=AF3%2BDQBZAAAgACCmXTMzlrdcCk2FyINAWKZ4DBxq4%2BCgmJ69v%2BmH4EMlEQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D#vless-httpupgrade-tls-ech
# XHTTP
vless://c2841505-ec32-4b8d-b6dd-3e19d648c321@127.0.0.1:45507?type=xhttp&encryption=none&path=%2Fxhttppath&host=xhttp&mode=auto&security=none#vless-xhttp
```
## Trojan
```
ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Y21lZklCdDhwMTJaZm1QWUplMnNCNThRd3R3NXNKeVpUV0Z6ZENKV2taOD06eEJHZUxiMWNPTjFIeE9CenF6UlN0VFdhUUh6YWM2cFhRVFNZd2dVV2R1RT0@example.com:81?type=tcp#example-ss2022
```
Может быть без `?type=tcp`
# tcp
trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none
trojan://cME3ZlUrYF@127.0.0.1:43772?type=tcp&security=reality&pbk=DckTwU6p6pTX9QxFXOi6vH4Vzt_RCE1vMCnj2c6hvjw&fp=chrome&sni=google.com&sid=221a80cf94&spx=%2F#trojan-tcp-reality
trojan://EJjpAj02lg@127.0.0.1:11381?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-tcp-tls
trojan://ZP2Ik5sxN3@127.0.0.1:16247?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-tcp-tls-insecure
trojan://90caP481ay@127.0.0.1:59708?type=tcp&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACC2y%2BAe4dqthLNpfvmtE6g%2BnaJ%2FciK6P%2BREbRLkR%2Fg%2FEgAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-tcp-tls-ech
# VLESS
# mKCP
trojan://N5v7iIOe9G@127.0.0.1:36319?type=kcp&headerType=none&seed=P91wFIfjzZ&security=none#trojan-mKCP
## Reality
```
vless://eb445f4b-ddb4-4c79-86d5-0833fc674379@example.com:443?type=tcp&security=reality&pbk=ARQzddtXPJZHinwkPbgVpah9uwPTuzdjU9GpbUkQJkc&fp=chrome&sni=yahoo.com&sid=6cabf01472a3&spx=%2F&flow=xtls-rprx-vision#vless-reality
```
# WebSocket
trojan://G3cE9phv1g@127.0.0.1:57370?type=ws&path=%2Fwspath&host=google.com&security=none#trojan-websocket-none
trojan://FBok41WczO@127.0.0.1:59919?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-websocket-tls
trojan://bhwvndUBPA@127.0.0.1:22969?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-websocket-tls-insecur
trojan://pwiduqFUWO@127.0.0.1:46765?type=ws&path=%2Fwspath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCFcQYEtwrFOidJJLYHvSiN%2BljRgaAIrNHoVnio3uXAOwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-websocket-tls-ech
```
vless://UUID@IP:2082?security=reality&sni=dash.cloudflare.com&alpn=h2,http/1.1&allowInsecure=1&fp=chrome&pbk=pukkey&sid=id&type=grpc&encryption=none#vless-reality-strange
```
# gRPC
trojan://WMR7qkKhsV@127.0.0.1:27897?type=grpc&serviceName=TunService&authority=authority&security=none#trojan-gRPC-none
trojan://KVuRNsu6KG@127.0.0.1:46077?type=grpc&serviceName=TunService&authority=authority&security=reality&pbk=Xn59i4gum3ppCICS6-_NuywrhHIVVAH54b2mjd5CFkE&fp=chrome&sni=google.com&sid=e5be&spx=%2F#trojan-gRPC-reality
trojan://7BJtbywy8h@127.0.0.1:10627?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-gRPC-tls
trojan://TI3PakvtP4@127.0.0.1:10435?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-gRPC-tls-insecure
trojan://mbzoVKL27h@127.0.0.1:38681?type=grpc&serviceName=TunService&authority=authority&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACCq72Ru3VbFlDpKttl3LccmInu8R2oAsCr8wzyxB0vZZQAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-gRPC-tls-ech
## TLS
1.
```
vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=tls&fp=&alpn=h3%2Ch2%2Chttp%2F1.1#vless-tls
```
# HTTPUpgrade
trojan://uc44gBwOKQ@127.0.0.1:29085?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=none#trojan-httpupgrade-none
trojan://MhNxbcVB14@127.0.0.1:32700?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#trojan-httpupgrade-tls
trojan://7SOQFUpLob@127.0.0.1:28474?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&allowInsecure=1&sni=google.com#trojan-httpupgrade-tls-insecure
trojan://ou8pLSyx9N@127.0.0.1:17737?type=httpupgrade&path=%2Fhttpupgradepath&host=google.com&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&ech=AF3%2BDQBZAAAgACB%2FlkIkit%2BblFzE7PtbYDVF3NXK8olXJ5a7YwY%2Biy9QQwAkAAEAAQABAAIAAQADAAIAAQACAAIAAgADAAMAAQADAAIAAwADAApnb29nbGUuY29tAAA%3D&sni=google.com#trojan-httpupgrade-tls-ech
2.
```
vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443?security=tls&sni=SITE&fp=chrome&type=tcp&flow=xtls-rprx-vision&encryption=none#vless-tls-withot-alpn
```
3.
```
vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=sni.server.com&fp=chrome#vless-tls-ws
```
4.
```
vless://[someid]@[someserver]?security=tls&sni=[somesni]&type=ws&path=/?ed%3D2560&host=[somesni]&encryption=none#vless-tls-ws-2
```
5.
```
vless://uuid@server:443?security=tls&sni=server&fp=chrome&type=ws&path=/websocket&encryption=none#vless-tls-ws-3
```
6.
```
vless://33333@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=example.com&fp=chrome#vless-tls-ws-4
```
## No security
```
vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443?type=tcp&security=none#vless-tls-no-encrypt
# XHTTP
trojan://VEetltxLtw@127.0.0.1:59072?type=xhttp&path=%2Fxhttppath&host=google.com&mode=auto&security=none#trojan-xhttp
```

View File

@@ -0,0 +1,8 @@
{
"printWidth": 80,
"tabWidth": 2,
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"bracketSpacing": true
}

View File

@@ -0,0 +1,27 @@
// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';
export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ['node_modules', 'watch-upload.js'],
},
{
rules: {
'no-console': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
},
prettier,
];

View File

@@ -0,0 +1,31 @@
{
"name": "fe-app-podkop",
"version": "1.0.0",
"license": "MIT",
"type": "module",
"scripts": {
"format": "prettier --write src",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"build": "tsup src/main.ts",
"dev": "tsup src/main.ts --watch",
"test": "vitest",
"ci": "yarn format && yarn lint --max-warnings=0 && yarn test --run && yarn build",
"watch:sftp": "node watch-upload.js"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"chokidar": "4.0.3",
"dotenv": "17.2.3",
"eslint": "9.36.0",
"eslint-config-prettier": "10.1.8",
"glob": "11.0.3",
"prettier": "3.6.2",
"ssh2-sftp-client": "12.0.1",
"tsup": "8.5.0",
"typescript": "5.9.3",
"typescript-eslint": "8.45.0",
"vitest": "3.2.4"
}
}

View File

@@ -0,0 +1,2 @@
export * from './types';
export * from './methods';

View File

@@ -0,0 +1,28 @@
import { IBaseApiResponse } from '../types';
export async function createBaseApiRequest<T>(
fetchFn: () => Promise<Response>,
): Promise<IBaseApiResponse<T>> {
try {
const response = await fetchFn();
if (!response.ok) {
return {
success: false as const,
message: `${_('HTTP error')} ${response.status}: ${response.statusText}`,
};
}
const data: T = await response.json();
return {
success: true as const,
data,
};
} catch (e) {
return {
success: false as const,
message: e instanceof Error ? e.message : _('Unknown error'),
};
}
}

View File

@@ -0,0 +1,14 @@
import { ClashAPI, IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function getClashConfig(): Promise<
IBaseApiResponse<ClashAPI.Config>
> {
return createBaseApiRequest<ClashAPI.Config>(() =>
fetch(`${getClashApiUrl()}/configs`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}),
);
}

View File

@@ -0,0 +1,20 @@
import { ClashAPI, IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function getClashGroupDelay(
group: string,
url = 'https://www.gstatic.com/generate_204',
timeout = 2000,
): Promise<IBaseApiResponse<ClashAPI.Delays>> {
const endpoint = `${getClashApiUrl()}/group/${group}/delay?url=${encodeURIComponent(
url,
)}&timeout=${timeout}`;
return createBaseApiRequest<ClashAPI.Delays>(() =>
fetch(endpoint, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}),
);
}

View File

@@ -0,0 +1,14 @@
import { ClashAPI, IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function getClashProxies(): Promise<
IBaseApiResponse<ClashAPI.Proxies>
> {
return createBaseApiRequest<ClashAPI.Proxies>(() =>
fetch(`${getClashApiUrl()}/proxies`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}),
);
}

View File

@@ -0,0 +1,14 @@
import { ClashAPI, IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function getClashVersion(): Promise<
IBaseApiResponse<ClashAPI.Version>
> {
return createBaseApiRequest<ClashAPI.Version>(() =>
fetch(`${getClashApiUrl()}/version`, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
}),
);
}

View File

@@ -0,0 +1,7 @@
export * from './createBaseApiRequest';
export * from './getConfig';
export * from './getGroupDelay';
export * from './getProxies';
export * from './getVersion';
export * from './triggerProxySelector';
export * from './triggerLatencyTest';

View File

@@ -0,0 +1,35 @@
import { IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function triggerLatencyGroupTest(
tag: string,
timeout: number = 5000,
url: string = 'https://www.gstatic.com/generate_204',
): Promise<IBaseApiResponse<void>> {
return createBaseApiRequest<void>(() =>
fetch(
`${getClashApiUrl()}/group/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
},
),
);
}
export async function triggerLatencyProxyTest(
tag: string,
timeout: number = 2000,
url: string = 'https://www.gstatic.com/generate_204',
): Promise<IBaseApiResponse<void>> {
return createBaseApiRequest<void>(() =>
fetch(
`${getClashApiUrl()}/proxies/${tag}/delay?url=${encodeURIComponent(url)}&timeout=${timeout}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
},
),
);
}

View File

@@ -0,0 +1,16 @@
import { IBaseApiResponse } from '../types';
import { createBaseApiRequest } from './createBaseApiRequest';
import { getClashApiUrl } from '../../helpers';
export async function triggerProxySelector(
selector: string,
outbound: string,
): Promise<IBaseApiResponse<void>> {
return createBaseApiRequest<void>(() =>
fetch(`${getClashApiUrl()}/proxies/${selector}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: outbound }),
}),
);
}

View File

@@ -0,0 +1,53 @@
export type IBaseApiResponse<T> =
| {
success: true;
data: T;
}
| {
success: false;
message: string;
};
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ClashAPI {
export interface Version {
meta: boolean;
premium: boolean;
version: string;
}
export interface Config {
port: number;
'socks-port': number;
'redir-port': number;
'tproxy-port': number;
'mixed-port': number;
'allow-lan': boolean;
'bind-address': string;
mode: 'Rule' | 'Global' | 'Direct';
'mode-list': string[];
'log-level': 'debug' | 'info' | 'warn' | 'error';
ipv6: boolean;
tun: null | Record<string, unknown>;
}
export interface ProxyHistoryEntry {
time: string;
delay: number;
}
export interface ProxyBase {
type: string;
name: string;
udp: boolean;
history: ProxyHistoryEntry[];
now?: string;
all?: string[];
}
export interface Proxies {
proxies: Record<string, ProxyBase>;
}
export type Delays = Record<string, number>;
}

View File

@@ -0,0 +1,107 @@
export const STATUS_COLORS = {
SUCCESS: '#4caf50',
ERROR: '#f44336',
WARNING: '#ff9800',
};
export const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi';
export const IP_CHECK_DOMAIN = 'ip.podkop.fyi';
export const REGIONAL_OPTIONS = [
'russia_inside',
'russia_outside',
'ukraine_inside',
];
export const ALLOWED_WITH_RUSSIA_INSIDE = [
'russia_inside',
'meta',
'twitter',
'discord',
'telegram',
'cloudflare',
'google_ai',
'google_play',
'hetzner',
'ovh',
'hodca',
'digitalocean',
'cloudfront',
];
export const DOMAIN_LIST_OPTIONS = {
russia_inside: 'Russia inside',
russia_outside: 'Russia outside',
ukraine_inside: 'Ukraine',
geoblock: 'Geo Block',
block: 'Block',
porn: 'Porn',
news: 'News',
anime: 'Anime',
youtube: 'Youtube',
discord: 'Discord',
meta: 'Meta',
twitter: 'Twitter (X)',
hdrezka: 'HDRezka',
tiktok: 'Tik-Tok',
telegram: 'Telegram',
cloudflare: 'Cloudflare',
google_ai: 'Google AI',
google_play: 'Google Play',
hodca: 'H.O.D.C.A',
hetzner: 'Hetzner ASN',
ovh: 'OVH ASN',
digitalocean: 'Digital Ocean ASN',
cloudfront: 'CloudFront ASN',
};
export const UPDATE_INTERVAL_OPTIONS = {
'1h': 'Every hour',
'3h': 'Every 3 hours',
'12h': 'Every 12 hours',
'1d': 'Every day',
'3d': 'Every 3 days',
};
export const DNS_SERVER_OPTIONS = {
'1.1.1.1': '1.1.1.1 (Cloudflare)',
'8.8.8.8': '8.8.8.8 (Google)',
'9.9.9.9': '9.9.9.9 (Quad9)',
'dns.adguard-dns.com': 'dns.adguard-dns.com (AdGuard Default)',
'unfiltered.adguard-dns.com':
'unfiltered.adguard-dns.com (AdGuard Unfiltered)',
'family.adguard-dns.com': 'family.adguard-dns.com (AdGuard Family)',
};
export const BOOTSTRAP_DNS_SERVER_OPTIONS = {
'77.88.8.8': '77.88.8.8 (Yandex DNS)',
'77.88.8.1': '77.88.8.1 (Yandex DNS)',
'1.1.1.1': '1.1.1.1 (Cloudflare DNS)',
'1.0.0.1': '1.0.0.1 (Cloudflare DNS)',
'8.8.8.8': '8.8.8.8 (Google DNS)',
'8.8.4.4': '8.8.4.4 (Google DNS)',
'9.9.9.9': '9.9.9.9 (Quad9 DNS)',
'9.9.9.11': '9.9.9.11 (Quad9 DNS)',
};
export const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds
export const CACHE_TIMEOUT = DIAGNOSTICS_UPDATE_INTERVAL - 1000; // 9 seconds
export const ERROR_POLL_INTERVAL = 10000; // 10 seconds
export const COMMAND_TIMEOUT = 10000; // 10 seconds
export const FETCH_TIMEOUT = 10000; // 10 seconds
export const BUTTON_FEEDBACK_TIMEOUT = 1000; // 1 second
export const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds
// Command scheduling intervals in diagnostics (in milliseconds)
export const COMMAND_SCHEDULING = {
P0_PRIORITY: 0, // Highest priority (no delay)
P1_PRIORITY: 100, // Very high priority
P2_PRIORITY: 300, // High priority
P3_PRIORITY: 500, // Above average
P4_PRIORITY: 700, // Standard priority
P5_PRIORITY: 900, // Below average
P6_PRIORITY: 1100, // Low priority
P7_PRIORITY: 1300, // Very low priority
P8_PRIORITY: 1500, // Background execution
P9_PRIORITY: 1700, // Idle mode execution
P10_PRIORITY: 1900, // Lowest priority
} as const;

View File

@@ -0,0 +1,32 @@
import { COMMAND_TIMEOUT } from '../constants';
import { withTimeout } from './withTimeout';
interface ExecuteShellCommandParams {
command: string;
args: string[];
timeout?: number;
}
interface ExecuteShellCommandResponse {
stdout: string;
stderr: string;
code?: number;
}
export async function executeShellCommand({
command,
args,
timeout = COMMAND_TIMEOUT,
}: ExecuteShellCommandParams): Promise<ExecuteShellCommandResponse> {
try {
return withTimeout(
fs.exec(command, args),
timeout,
[command, ...args].join(' '),
);
} catch (err) {
const error = err as Error;
return { stdout: '', stderr: error?.message, code: 0 };
}
}

View File

@@ -0,0 +1,4 @@
export function getBaseUrl(): string {
const { protocol, hostname } = window.location;
return `${protocol}//${hostname}`;
}

View File

@@ -0,0 +1,11 @@
export function getClashApiUrl(): string {
const { protocol, hostname } = window.location;
return `${protocol}//${hostname}:9090`;
}
export function getClashWsUrl(): string {
const { hostname } = window.location;
return `ws://${hostname}:9090`;
}

View File

@@ -0,0 +1,13 @@
export function getProxyUrlName(url: string) {
try {
const [_link, hash] = url.split('#');
if (!hash) {
return '';
}
return decodeURIComponent(hash);
} catch {
return '';
}
}

View File

@@ -0,0 +1,9 @@
export * from './getBaseUrl';
export * from './parseValueList';
export * from './injectGlobalStyles';
export * from './withTimeout';
export * from './executeShellCommand';
export * from './maskIP';
export * from './getProxyUrlName';
export * from './onMount';
export * from './getClashApiUrl';

View File

@@ -0,0 +1,12 @@
import { GlobalStyles } from '../styles';
export function injectGlobalStyles() {
document.head.insertAdjacentHTML(
'beforeend',
`
<style>
${GlobalStyles}
</style>
`,
);
}

View File

@@ -0,0 +1,5 @@
export function maskIP(ip: string = ''): string {
const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
return ip.replace(ipv4Regex, (_match, _p1, _p2, _p3, p4) => `XX.XX.XX.${p4}`);
}

View File

@@ -0,0 +1,30 @@
export async function onMount(id: string): Promise<HTMLElement> {
return new Promise((resolve) => {
const el = document.getElementById(id);
if (el && el.offsetParent !== null) {
return resolve(el);
}
const observer = new MutationObserver(() => {
const target = document.getElementById(id);
if (target) {
const io = new IntersectionObserver((entries) => {
const visible = entries.some((e) => e.isIntersecting);
if (visible) {
observer.disconnect();
io.disconnect();
resolve(target);
}
});
io.observe(target);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
});
}

View File

@@ -0,0 +1,9 @@
export function parseValueList(value: string): string[] {
return value
.split(/\n/) // Split to array by newline separator
.map((line) => line.split('//')[0]) // Remove comments
.join(' ') // Build clean string
.split(/[,\s]+/) // Split to array by comma and space
.map((s) => s.trim()) // Remove extra spaces
.filter(Boolean); // Leave nonempty items
}

View File

@@ -0,0 +1,12 @@
// steal from https://github.com/sindresorhus/pretty-bytes/blob/master/index.js
export function prettyBytes(n: number) {
const UNITS = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
if (n < 1000) {
return n + ' B';
}
const exponent = Math.min(Math.floor(Math.log10(n) / 3), UNITS.length - 1);
n = Number((n / Math.pow(1000, exponent)).toPrecision(3));
const unit = UNITS[exponent];
return n + ' ' + unit;
}

View File

@@ -0,0 +1,42 @@
import { describe, expect, it } from 'vitest';
import { maskIP } from '../maskIP';
export const validIPs = [
['Standard private IP', '192.168.0.1', 'XX.XX.XX.1'],
['Public IP', '8.8.8.8', 'XX.XX.XX.8'],
['Mixed digits', '10.0.255.99', 'XX.XX.XX.99'],
['Edge values', '255.255.255.255', 'XX.XX.XX.255'],
['Zeros', '0.0.0.0', 'XX.XX.XX.0'],
];
export const invalidIPs = [
['Empty string', '', ''],
['Missing octets', '192.168.1', '192.168.1'],
['Extra octets', '1.2.3.4.5', '1.2.3.4.5'],
['Letters inside', 'abc.def.ghi.jkl', 'abc.def.ghi.jkl'],
['Spaces inside', '1. 2.3.4', '1. 2.3.4'],
['Just dots', '...', '...'],
['IP with port', '127.0.0.1:8080', '127.0.0.1:8080'],
['IP with text', 'ip=192.168.0.1', 'ip=192.168.0.1'],
];
describe('maskIP', () => {
describe.each(validIPs)('Valid IPv4: %s', (_desc, ip, expected) => {
it(`masks "${ip}" → "${expected}"`, () => {
expect(maskIP(ip)).toBe(expected);
});
});
describe.each(invalidIPs)(
'Invalid or malformed IP: %s',
(_desc, ip, expected) => {
it(`returns original string for "${ip}"`, () => {
expect(maskIP(ip)).toBe(expected);
});
},
);
it('defaults to empty string if no param passed', () => {
expect(maskIP()).toBe('');
});
});

View File

@@ -0,0 +1,21 @@
export async function withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
operationName: string,
timeoutMessage = _('Operation timed out'),
): Promise<T> {
let timeoutId;
const start = performance.now();
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutId);
const elapsed = performance.now() - start;
console.log(`[${operationName}] Execution time: ${elapsed.toFixed(2)} ms`);
}
}

40
fe-app-podkop/src/luci.d.ts vendored Normal file
View File

@@ -0,0 +1,40 @@
type HtmlTag = keyof HTMLElementTagNameMap;
type HtmlElement<T extends HtmlTag> = HTMLElementTagNameMap[T];
type HtmlAttributes<T extends HtmlTag = 'div'> = Partial<
Omit<HtmlElement<T>, 'style' | 'children'> & {
style?: string | Partial<CSSStyleDeclaration>;
class?: string;
onclick?: (event: MouseEvent) => void;
}
>;
declare global {
const fs: {
exec(
command: string,
args?: string[],
env?: Record<string, string>,
): Promise<{
stdout: string;
stderr: string;
code?: number;
}>;
};
const E: <T extends HtmlTag>(
type: T,
attr?: HtmlAttributes<T> | null,
children?: (Node | string)[] | Node | string,
) => HTMLElementTagNameMap[T];
const uci: {
load: (packages: string | string[]) => Promise<string>;
sections: (conf: string, type?: string, cb?: () => void) => Promise<T>;
};
const _ = (_key: string) => string;
}
export {};

10
fe-app-podkop/src/main.ts Normal file
View File

@@ -0,0 +1,10 @@
'use strict';
'require baseclass';
'require fs';
'require uci';
export * from './validators';
export * from './helpers';
export * from './clash';
export * from './podkop';
export * from './constants';

View File

@@ -0,0 +1,3 @@
export * from './methods';
export * from './services';
export * from './tabs';

View File

@@ -0,0 +1,5 @@
import { Podkop } from '../types';
export async function getConfigSections(): Promise<Podkop.ConfigSection[]> {
return uci.load('podkop').then(() => uci.sections('podkop'));
}

View File

@@ -0,0 +1,153 @@
import { Podkop } from '../types';
import { getConfigSections } from './getConfigSections';
import { getClashProxies } from '../../clash';
import { getProxyUrlName } from '../../helpers';
interface IGetDashboardSectionsResponse {
success: boolean;
data: Podkop.OutboundGroup[];
}
export async function getDashboardSections(): Promise<IGetDashboardSectionsResponse> {
const configSections = await getConfigSections();
const clashProxies = await getClashProxies();
if (!clashProxies.success) {
return {
success: false,
data: [],
};
}
const proxies = Object.entries(clashProxies.data.proxies).map(
([key, value]) => ({
code: key,
value,
}),
);
const data = configSections
.filter((section) => section.mode !== 'block')
.map((section) => {
if (section.mode === 'proxy') {
if (section.proxy_config_type === 'url') {
const outbound = proxies.find(
(proxy) => proxy.code === `${section['.name']}-out`,
);
return {
withTagSelect: false,
code: outbound?.code || section['.name'],
displayName: section['.name'],
outbounds: [
{
code: outbound?.code || section['.name'],
displayName:
getProxyUrlName(section.proxy_string) ||
outbound?.value?.name ||
'',
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '',
selected: true,
},
],
};
}
if (section.proxy_config_type === 'outbound') {
const outbound = proxies.find(
(proxy) => proxy.code === `${section['.name']}-out`,
);
return {
withTagSelect: false,
code: outbound?.code || section['.name'],
displayName: section['.name'],
outbounds: [
{
code: outbound?.code || section['.name'],
displayName:
decodeURIComponent(JSON.parse(section.outbound_json)?.tag) ||
outbound?.value?.name ||
'',
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '',
selected: true,
},
],
};
}
if (section.proxy_config_type === 'urltest') {
const selector = proxies.find(
(proxy) => proxy.code === `${section['.name']}-out`,
);
const outbound = proxies.find(
(proxy) => proxy.code === `${section['.name']}-urltest-out`,
);
const outbounds = (outbound?.value?.all ?? [])
.map((code) => proxies.find((item) => item.code === code))
.map((item, index) => ({
code: item?.code || '',
displayName:
getProxyUrlName(section.urltest_proxy_links?.[index]) ||
item?.value?.name ||
'',
latency: item?.value?.history?.[0]?.delay || 0,
type: item?.value?.type || '',
selected: selector?.value?.now === item?.code,
}));
return {
withTagSelect: true,
code: selector?.code || section['.name'],
displayName: section['.name'],
outbounds: [
{
code: outbound?.code || '',
displayName: _('Fastest'),
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '',
selected: selector?.value?.now === outbound?.code,
},
...outbounds,
],
};
}
}
if (section.mode === 'vpn') {
const outbound = proxies.find(
(proxy) => proxy.code === `${section['.name']}-out`,
);
return {
withTagSelect: false,
code: outbound?.code || section['.name'],
displayName: section['.name'],
outbounds: [
{
code: outbound?.code || section['.name'],
displayName: section.interface || outbound?.value?.name || '',
latency: outbound?.value?.history?.[0]?.delay || 0,
type: outbound?.value?.type || '',
selected: true,
},
],
};
}
return {
withTagSelect: false,
code: section['.name'],
displayName: section['.name'],
outbounds: [],
};
});
return {
success: true,
data,
};
}

View File

@@ -0,0 +1,21 @@
import { executeShellCommand } from '../../helpers';
export async function getPodkopStatus(): Promise<{
enabled: number;
status: string;
}> {
const response = await executeShellCommand({
command: '/usr/bin/podkop',
args: ['get_status'],
timeout: 1000,
});
if (response.stdout) {
return JSON.parse(response.stdout.replace(/\n/g, '')) as {
enabled: number;
status: string;
};
}
return { enabled: 0, status: 'unknown' };
}

View File

@@ -0,0 +1,23 @@
import { executeShellCommand } from '../../helpers';
export async function getSingboxStatus(): Promise<{
running: number;
enabled: number;
status: string;
}> {
const response = await executeShellCommand({
command: '/usr/bin/podkop',
args: ['get_sing_box_status'],
timeout: 1000,
});
if (response.stdout) {
return JSON.parse(response.stdout.replace(/\n/g, '')) as {
running: number;
enabled: number;
status: string;
};
}
return { running: 0, enabled: 0, status: 'unknown' };
}

View File

@@ -0,0 +1,4 @@
export * from './getConfigSections';
export * from './getDashboardSections';
export * from './getPodkopStatus';
export * from './getSingboxStatus';

View File

@@ -0,0 +1,13 @@
import { TabServiceInstance } from './tab.service';
import { store } from '../../store';
export function coreService() {
TabServiceInstance.onChange((activeId, tabs) => {
store.set({
tabService: {
current: activeId || '',
all: tabs.map((tab) => tab.id),
},
});
});
}

View File

@@ -0,0 +1,2 @@
export * from './tab.service';
export * from './core.service';

View File

@@ -0,0 +1,92 @@
type TabInfo = {
el: HTMLElement;
id: string;
active: boolean;
};
type TabChangeCallback = (activeId: string | null, allTabs: TabInfo[]) => void;
export class TabService {
private static instance: TabService;
private observer: MutationObserver | null = null;
private callback?: TabChangeCallback;
private lastActiveId: string | null = null;
private constructor() {
this.init();
}
public static getInstance(): TabService {
if (!TabService.instance) {
TabService.instance = new TabService();
}
return TabService.instance;
}
private init() {
this.observer = new MutationObserver(() => this.handleMutations());
this.observer.observe(document.body, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ['class'],
});
// initial check
this.notify();
}
private handleMutations() {
this.notify();
}
private getTabsInfo(): TabInfo[] {
const tabs = Array.from(
document.querySelectorAll<HTMLElement>('.cbi-tab, .cbi-tab-disabled'),
);
return tabs.map((el) => ({
el,
id: el.dataset.tab || '',
active:
el.classList.contains('cbi-tab') &&
!el.classList.contains('cbi-tab-disabled'),
}));
}
private getActiveTabId(): string | null {
const active = document.querySelector<HTMLElement>(
'.cbi-tab:not(.cbi-tab-disabled)',
);
return active?.dataset.tab || null;
}
private notify() {
const tabs = this.getTabsInfo();
const activeId = this.getActiveTabId();
if (activeId !== this.lastActiveId) {
this.lastActiveId = activeId;
this.callback?.(activeId, tabs);
}
}
public onChange(callback: TabChangeCallback) {
this.callback = callback;
this.notify();
}
public getAllTabs(): TabInfo[] {
return this.getTabsInfo();
}
public getActiveTab(): string | null {
return this.getActiveTabId();
}
public disconnect() {
this.observer?.disconnect();
this.observer = null;
}
}
export const TabServiceInstance = TabService.getInstance();

View File

@@ -0,0 +1,2 @@
export * from './renderDashboard';
export * from './initDashboardController';

View File

@@ -0,0 +1,393 @@
import {
getDashboardSections,
getPodkopStatus,
getSingboxStatus,
} from '../../methods';
import { getClashWsUrl, onMount } from '../../../helpers';
import {
triggerLatencyGroupTest,
triggerLatencyProxyTest,
triggerProxySelector,
} from '../../../clash';
import { store, StoreType } from '../../../store';
import { socket } from '../../../socket';
import { prettyBytes } from '../../../helpers/prettyBytes';
import { renderSections } from './renderSections';
import { renderWidget } from './renderWidget';
// Fetchers
async function fetchDashboardSections() {
const prev = store.get().sectionsWidget;
store.set({
sectionsWidget: {
...prev,
failed: false,
},
});
const { data, success } = await getDashboardSections();
store.set({
sectionsWidget: {
loading: false,
failed: !success,
data,
},
});
}
async function fetchServicesInfo() {
const [podkop, singbox] = await Promise.all([
getPodkopStatus(),
getSingboxStatus(),
]);
store.set({
servicesInfoWidget: {
loading: false,
failed: false,
data: { singbox: singbox.running, podkop: podkop.enabled },
},
});
}
async function connectToClashSockets() {
socket.subscribe(
`${getClashWsUrl()}/traffic?token=`,
(msg) => {
const parsedMsg = JSON.parse(msg);
store.set({
bandwidthWidget: {
loading: false,
failed: false,
data: { up: parsedMsg.up, down: parsedMsg.down },
},
});
},
(_err) => {
store.set({
bandwidthWidget: {
loading: false,
failed: true,
data: { up: 0, down: 0 },
},
});
},
);
socket.subscribe(
`${getClashWsUrl()}/connections?token=`,
(msg) => {
const parsedMsg = JSON.parse(msg);
store.set({
trafficTotalWidget: {
loading: false,
failed: false,
data: {
downloadTotal: parsedMsg.downloadTotal,
uploadTotal: parsedMsg.uploadTotal,
},
},
systemInfoWidget: {
loading: false,
failed: false,
data: {
connections: parsedMsg.connections?.length,
memory: parsedMsg.memory,
},
},
});
},
(_err) => {
store.set({
trafficTotalWidget: {
loading: false,
failed: true,
data: { downloadTotal: 0, uploadTotal: 0 },
},
systemInfoWidget: {
loading: false,
failed: true,
data: {
connections: 0,
memory: 0,
},
},
});
},
);
}
// Handlers
async function handleChooseOutbound(selector: string, tag: string) {
await triggerProxySelector(selector, tag);
await fetchDashboardSections();
}
async function handleTestGroupLatency(tag: string) {
await triggerLatencyGroupTest(tag);
await fetchDashboardSections();
}
async function handleTestProxyLatency(tag: string) {
await triggerLatencyProxyTest(tag);
await fetchDashboardSections();
}
function replaceTestLatencyButtonsWithSkeleton() {
document
.querySelectorAll('.dashboard-sections-grid-item-test-latency')
.forEach((el) => {
const newDiv = document.createElement('div');
newDiv.className = 'skeleton';
newDiv.style.width = '99px';
newDiv.style.height = '28px';
el.replaceWith(newDiv);
});
}
// Renderer
async function renderSectionsWidget() {
console.log('renderSectionsWidget');
const sectionsWidget = store.get().sectionsWidget;
const container = document.getElementById('dashboard-sections-grid');
if (sectionsWidget.loading || sectionsWidget.failed) {
const renderedWidget = renderSections({
loading: sectionsWidget.loading,
failed: sectionsWidget.failed,
section: {
code: '',
displayName: '',
outbounds: [],
withTagSelect: false,
},
onTestLatency: () => {},
onChooseOutbound: () => {},
});
return container!.replaceChildren(renderedWidget);
}
const renderedWidgets = sectionsWidget.data.map((section) =>
renderSections({
loading: sectionsWidget.loading,
failed: sectionsWidget.failed,
section,
onTestLatency: (tag) => {
replaceTestLatencyButtonsWithSkeleton();
if (section.withTagSelect) {
return handleTestGroupLatency(tag);
}
return handleTestProxyLatency(tag);
},
onChooseOutbound: (selector, tag) => {
handleChooseOutbound(selector, tag);
},
}),
);
return container!.replaceChildren(...renderedWidgets);
}
async function renderBandwidthWidget() {
console.log('renderBandwidthWidget');
const traffic = store.get().bandwidthWidget;
const container = document.getElementById('dashboard-widget-traffic');
if (traffic.loading || traffic.failed) {
const renderedWidget = renderWidget({
loading: traffic.loading,
failed: traffic.failed,
title: '',
items: [],
});
return container!.replaceChildren(renderedWidget);
}
const renderedWidget = renderWidget({
loading: traffic.loading,
failed: traffic.failed,
title: _('Traffic'),
items: [
{ key: _('Uplink'), value: `${prettyBytes(traffic.data.up)}/s` },
{ key: _('Downlink'), value: `${prettyBytes(traffic.data.down)}/s` },
],
});
container!.replaceChildren(renderedWidget);
}
async function renderTrafficTotalWidget() {
console.log('renderTrafficTotalWidget');
const trafficTotalWidget = store.get().trafficTotalWidget;
const container = document.getElementById('dashboard-widget-traffic-total');
if (trafficTotalWidget.loading || trafficTotalWidget.failed) {
const renderedWidget = renderWidget({
loading: trafficTotalWidget.loading,
failed: trafficTotalWidget.failed,
title: '',
items: [],
});
return container!.replaceChildren(renderedWidget);
}
const renderedWidget = renderWidget({
loading: trafficTotalWidget.loading,
failed: trafficTotalWidget.failed,
title: _('Traffic Total'),
items: [
{
key: _('Uplink'),
value: String(prettyBytes(trafficTotalWidget.data.uploadTotal)),
},
{
key: _('Downlink'),
value: String(prettyBytes(trafficTotalWidget.data.downloadTotal)),
},
],
});
container!.replaceChildren(renderedWidget);
}
async function renderSystemInfoWidget() {
console.log('renderSystemInfoWidget');
const systemInfoWidget = store.get().systemInfoWidget;
const container = document.getElementById('dashboard-widget-system-info');
if (systemInfoWidget.loading || systemInfoWidget.failed) {
const renderedWidget = renderWidget({
loading: systemInfoWidget.loading,
failed: systemInfoWidget.failed,
title: '',
items: [],
});
return container!.replaceChildren(renderedWidget);
}
const renderedWidget = renderWidget({
loading: systemInfoWidget.loading,
failed: systemInfoWidget.failed,
title: _('System info'),
items: [
{
key: _('Active Connections'),
value: String(systemInfoWidget.data.connections),
},
{
key: _('Memory Usage'),
value: String(prettyBytes(systemInfoWidget.data.memory)),
},
],
});
container!.replaceChildren(renderedWidget);
}
async function renderServicesInfoWidget() {
console.log('renderServicesInfoWidget');
const servicesInfoWidget = store.get().servicesInfoWidget;
const container = document.getElementById('dashboard-widget-service-info');
if (servicesInfoWidget.loading || servicesInfoWidget.failed) {
const renderedWidget = renderWidget({
loading: servicesInfoWidget.loading,
failed: servicesInfoWidget.failed,
title: '',
items: [],
});
return container!.replaceChildren(renderedWidget);
}
const renderedWidget = renderWidget({
loading: servicesInfoWidget.loading,
failed: servicesInfoWidget.failed,
title: _('Services info'),
items: [
{
key: _('Podkop'),
value: servicesInfoWidget.data.podkop
? _('✔ Enabled')
: _('✘ Disabled'),
attributes: {
class: servicesInfoWidget.data.podkop
? 'pdk_dashboard-page__widgets-section__item__row--success'
: 'pdk_dashboard-page__widgets-section__item__row--error',
},
},
{
key: _('Sing-box'),
value: servicesInfoWidget.data.singbox
? _('✔ Running')
: _('✘ Stopped'),
attributes: {
class: servicesInfoWidget.data.singbox
? 'pdk_dashboard-page__widgets-section__item__row--success'
: 'pdk_dashboard-page__widgets-section__item__row--error',
},
},
],
});
container!.replaceChildren(renderedWidget);
}
async function onStoreUpdate(
next: StoreType,
prev: StoreType,
diff: Partial<StoreType>,
) {
if (diff.sectionsWidget) {
renderSectionsWidget();
}
if (diff.bandwidthWidget) {
renderBandwidthWidget();
}
if (diff.trafficTotalWidget) {
renderTrafficTotalWidget();
}
if (diff.systemInfoWidget) {
renderSystemInfoWidget();
}
if (diff.servicesInfoWidget) {
renderServicesInfoWidget();
}
}
export async function initDashboardController(): Promise<void> {
onMount('dashboard-status').then(() => {
// Remove old listener
store.unsubscribe(onStoreUpdate);
// Clear store
store.reset();
// Add new listener
store.subscribe(onStoreUpdate);
// Initial sections fetch
fetchDashboardSections();
fetchServicesInfo();
connectToClashSockets();
});
}

View File

@@ -0,0 +1,54 @@
import { renderSections } from './renderSections';
import { renderWidget } from './renderWidget';
export function renderDashboard() {
return E(
'div',
{
id: 'dashboard-status',
class: 'pdk_dashboard-page',
},
[
// Widgets section
E('div', { class: 'pdk_dashboard-page__widgets-section' }, [
E(
'div',
{ id: 'dashboard-widget-traffic' },
renderWidget({ loading: true, failed: false, title: '', items: [] }),
),
E(
'div',
{ id: 'dashboard-widget-traffic-total' },
renderWidget({ loading: true, failed: false, title: '', items: [] }),
),
E(
'div',
{ id: 'dashboard-widget-system-info' },
renderWidget({ loading: true, failed: false, title: '', items: [] }),
),
E(
'div',
{ id: 'dashboard-widget-service-info' },
renderWidget({ loading: true, failed: false, title: '', items: [] }),
),
]),
// All outbounds
E(
'div',
{ id: 'dashboard-sections-grid' },
renderSections({
loading: true,
failed: false,
section: {
code: '',
displayName: '',
outbounds: [],
withTagSelect: false,
},
onTestLatency: () => {},
onChooseOutbound: () => {},
}),
),
],
);
}

View File

@@ -0,0 +1,125 @@
import { Podkop } from '../../types';
interface IRenderSectionsProps {
loading: boolean;
failed: boolean;
section: Podkop.OutboundGroup;
onTestLatency: (tag: string) => void;
onChooseOutbound: (selector: string, tag: string) => void;
}
function renderFailedState() {
return E(
'div',
{
class: 'pdk_dashboard-page__outbound-section centered',
style: 'height: 127px',
},
E('span', {}, _('Dashboard currently unavailable')),
);
}
function renderLoadingState() {
return E('div', {
id: 'dashboard-sections-grid-skeleton',
class: 'pdk_dashboard-page__outbound-section skeleton',
style: 'height: 127px',
});
}
export function renderDefaultState({
section,
onChooseOutbound,
onTestLatency,
}: IRenderSectionsProps) {
function testLatency() {
if (section.withTagSelect) {
return onTestLatency(section.code);
}
if (section.outbounds.length) {
return onTestLatency(section.outbounds[0].code);
}
}
function renderOutbound(outbound: Podkop.Outbound) {
function getLatencyClass() {
if (!outbound.latency) {
return 'pdk_dashboard-page__outbound-grid__item__latency--empty';
}
if (outbound.latency < 200) {
return 'pdk_dashboard-page__outbound-grid__item__latency--green';
}
if (outbound.latency < 400) {
return 'pdk_dashboard-page__outbound-grid__item__latency--yellow';
}
return 'pdk_dashboard-page__outbound-grid__item__latency--red';
}
return E(
'div',
{
class: `pdk_dashboard-page__outbound-grid__item ${outbound.selected ? 'pdk_dashboard-page__outbound-grid__item--active' : ''} ${section.withTagSelect ? 'pdk_dashboard-page__outbound-grid__item--selectable' : ''}`,
click: () =>
section.withTagSelect &&
onChooseOutbound(section.code, outbound.code),
},
[
E('b', {}, outbound.displayName),
E('div', { class: 'pdk_dashboard-page__outbound-grid__item__footer' }, [
E(
'div',
{ class: 'pdk_dashboard-page__outbound-grid__item__type' },
outbound.type,
),
E(
'div',
{ class: getLatencyClass() },
outbound.latency ? `${outbound.latency}ms` : 'N/A',
),
]),
],
);
}
return E('div', { class: 'pdk_dashboard-page__outbound-section' }, [
// Title with test latency
E('div', { class: 'pdk_dashboard-page__outbound-section__title-section' }, [
E(
'div',
{
class: 'pdk_dashboard-page__outbound-section__title-section__title',
},
section.displayName,
),
E(
'button',
{
class: 'btn dashboard-sections-grid-item-test-latency',
click: () => testLatency(),
},
'Test latency',
),
]),
E(
'div',
{ class: 'pdk_dashboard-page__outbound-grid' },
section.outbounds.map((outbound) => renderOutbound(outbound)),
),
]);
}
export function renderSections(props: IRenderSectionsProps) {
if (props.failed) {
return renderFailedState();
}
if (props.loading) {
return renderLoadingState();
}
return renderDefaultState(props);
}

View File

@@ -0,0 +1,78 @@
interface IRenderWidgetProps {
loading: boolean;
failed: boolean;
title: string;
items: Array<{
key: string;
value: string;
attributes?: {
class?: string;
};
}>;
}
function renderFailedState() {
return E(
'div',
{
id: '',
style: 'height: 78px',
class: 'pdk_dashboard-page__widgets-section__item centered',
},
_('Currently unavailable'),
);
}
function renderLoadingState() {
return E(
'div',
{
id: '',
style: 'height: 78px',
class: 'pdk_dashboard-page__widgets-section__item skeleton',
},
'',
);
}
function renderDefaultState({ title, items }: IRenderWidgetProps) {
return E('div', { class: 'pdk_dashboard-page__widgets-section__item' }, [
E(
'b',
{ class: 'pdk_dashboard-page__widgets-section__item__title' },
title,
),
...items.map((item) =>
E(
'div',
{
class: `pdk_dashboard-page__widgets-section__item__row ${item?.attributes?.class || ''}`,
},
[
E(
'span',
{ class: 'pdk_dashboard-page__widgets-section__item__row__key' },
`${item.key}: `,
),
E(
'span',
{ class: 'pdk_dashboard-page__widgets-section__item__row__value' },
item.value,
),
],
),
),
]);
}
export function renderWidget(props: IRenderWidgetProps) {
if (props.loading) {
return renderLoadingState();
}
if (props.failed) {
return renderFailedState();
}
return renderDefaultState(props);
}

View File

@@ -0,0 +1 @@
export * from './dashboard';

View File

@@ -0,0 +1,56 @@
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Podkop {
export interface Outbound {
code: string;
displayName: string;
latency: number;
type: string;
selected: boolean;
}
export interface OutboundGroup {
withTagSelect: boolean;
code: string;
displayName: string;
outbounds: Outbound[];
}
export interface ConfigProxyUrlTestSection {
mode: 'proxy';
proxy_config_type: 'urltest';
urltest_proxy_links: string[];
}
export interface ConfigProxyUrlSection {
mode: 'proxy';
proxy_config_type: 'url';
proxy_string: string;
}
export interface ConfigProxyOutboundSection {
mode: 'proxy';
proxy_config_type: 'outbound';
outbound_json: string;
}
export interface ConfigVpnSection {
mode: 'vpn';
interface: string;
}
export interface ConfigBlockSection {
mode: 'block';
}
export type ConfigBaseSection =
| ConfigProxyUrlTestSection
| ConfigProxyUrlSection
| ConfigProxyOutboundSection
| ConfigVpnSection
| ConfigBlockSection;
export type ConfigSection = ConfigBaseSection & {
'.name': string;
'.type': 'main' | 'extra';
};
}

121
fe-app-podkop/src/socket.ts Normal file
View File

@@ -0,0 +1,121 @@
// eslint-disable-next-line
type Listener = (data: any) => void;
type ErrorListener = (error: Event | string) => void;
class SocketManager {
private static instance: SocketManager;
private sockets = new Map<string, WebSocket>();
private listeners = new Map<string, Set<Listener>>();
private connected = new Map<string, boolean>();
private errorListeners = new Map<string, Set<ErrorListener>>();
private constructor() {}
static getInstance(): SocketManager {
if (!SocketManager.instance) {
SocketManager.instance = new SocketManager();
}
return SocketManager.instance;
}
connect(url: string): void {
if (this.sockets.has(url)) return;
const ws = new WebSocket(url);
this.sockets.set(url, ws);
this.connected.set(url, false);
this.listeners.set(url, new Set());
this.errorListeners.set(url, new Set());
ws.addEventListener('open', () => {
this.connected.set(url, true);
console.info(`Connected: ${url}`);
});
ws.addEventListener('message', (event) => {
const handlers = this.listeners.get(url);
if (handlers) {
for (const handler of handlers) {
try {
handler(event.data);
} catch (err) {
console.error(`Handler error for ${url}:`, err);
}
}
}
});
ws.addEventListener('close', () => {
this.connected.set(url, false);
console.warn(`Disconnected: ${url}`);
this.triggerError(url, 'Connection closed');
});
ws.addEventListener('error', (err) => {
console.error(`Socket error for ${url}:`, err);
this.triggerError(url, err);
});
}
subscribe(url: string, listener: Listener, onError?: ErrorListener): void {
if (!this.sockets.has(url)) {
this.connect(url);
}
this.listeners.get(url)?.add(listener);
if (onError) {
this.errorListeners.get(url)?.add(onError);
}
}
unsubscribe(url: string, listener: Listener, onError?: ErrorListener): void {
this.listeners.get(url)?.delete(listener);
if (onError) {
this.errorListeners.get(url)?.delete(onError);
}
}
// eslint-disable-next-line
send(url: string, data: any): void {
const ws = this.sockets.get(url);
if (ws && this.connected.get(url)) {
ws.send(typeof data === 'string' ? data : JSON.stringify(data));
} else {
console.warn(`Cannot send: not connected to ${url}`);
this.triggerError(url, 'Not connected');
}
}
disconnect(url: string): void {
const ws = this.sockets.get(url);
if (ws) {
ws.close();
this.sockets.delete(url);
this.listeners.delete(url);
this.errorListeners.delete(url);
this.connected.delete(url);
}
}
disconnectAll(): void {
for (const url of this.sockets.keys()) {
this.disconnect(url);
}
}
private triggerError(url: string, err: Event | string): void {
const handlers = this.errorListeners.get(url);
if (handlers) {
for (const cb of handlers) {
try {
cb(err);
} catch (e) {
console.error(`Error handler threw for ${url}:`, e);
}
}
}
}
}
export const socket = SocketManager.getInstance();

179
fe-app-podkop/src/store.ts Normal file
View File

@@ -0,0 +1,179 @@
import { Podkop } from './podkop/types';
function jsonStableStringify<T, V>(obj: T): string {
return JSON.stringify(obj, (_, value) => {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return Object.keys(value)
.sort()
.reduce(
(acc, key) => {
acc[key] = value[key];
return acc;
},
{} as Record<string, V>,
);
}
return value;
});
}
function jsonEqual<A, B>(a: A, b: B): boolean {
try {
return jsonStableStringify(a) === jsonStableStringify(b);
} catch {
return false;
}
}
type Listener<T> = (next: T, prev: T, diff: Partial<T>) => void;
// eslint-disable-next-line
class Store<T extends Record<string, any>> {
private value: T;
private readonly initial: T;
private listeners = new Set<Listener<T>>();
private lastHash = '';
constructor(initial: T) {
this.value = initial;
this.initial = structuredClone(initial);
this.lastHash = jsonStableStringify(initial);
}
get(): T {
return this.value;
}
set(next: Partial<T>): void {
const prev = this.value;
const merged = { ...prev, ...next };
if (jsonEqual(prev, merged)) return;
this.value = merged;
this.lastHash = jsonStableStringify(merged);
const diff: Partial<T> = {};
for (const key in merged) {
if (!jsonEqual(merged[key], prev[key])) diff[key] = merged[key];
}
this.listeners.forEach((cb) => cb(this.value, prev, diff));
}
reset(): void {
const prev = this.value;
const next = structuredClone(this.initial);
if (jsonEqual(prev, next)) return;
this.value = next;
this.lastHash = jsonStableStringify(next);
const diff: Partial<T> = {};
for (const key in next) {
if (!jsonEqual(next[key], prev[key])) diff[key] = next[key];
}
this.listeners.forEach((cb) => cb(this.value, prev, diff));
}
subscribe(cb: Listener<T>): () => void {
this.listeners.add(cb);
cb(this.value, this.value, {});
return () => this.listeners.delete(cb);
}
unsubscribe(cb: Listener<T>): void {
this.listeners.delete(cb);
}
patch<K extends keyof T>(key: K, value: T[K]): void {
this.set({ [key]: value } as unknown as Partial<T>);
}
getKey<K extends keyof T>(key: K): T[K] {
return this.value[key];
}
subscribeKey<K extends keyof T>(
key: K,
cb: (value: T[K]) => void,
): () => void {
let prev = this.value[key];
const wrapper: Listener<T> = (val) => {
if (!jsonEqual(val[key], prev)) {
prev = val[key];
cb(val[key]);
}
};
this.listeners.add(wrapper);
return () => this.listeners.delete(wrapper);
}
}
export interface StoreType {
tabService: {
current: string;
all: string[];
};
bandwidthWidget: {
loading: boolean;
failed: boolean;
data: { up: number; down: number };
};
trafficTotalWidget: {
loading: boolean;
failed: boolean;
data: { downloadTotal: number; uploadTotal: number };
};
systemInfoWidget: {
loading: boolean;
failed: boolean;
data: { connections: number; memory: number };
};
servicesInfoWidget: {
loading: boolean;
failed: boolean;
data: { singbox: number; podkop: number };
};
sectionsWidget: {
loading: boolean;
failed: boolean;
data: Podkop.OutboundGroup[];
};
}
const initialStore: StoreType = {
tabService: {
current: '',
all: [],
},
bandwidthWidget: {
loading: true,
failed: false,
data: { up: 0, down: 0 },
},
trafficTotalWidget: {
loading: true,
failed: false,
data: { downloadTotal: 0, uploadTotal: 0 },
},
systemInfoWidget: {
loading: true,
failed: false,
data: { connections: 0, memory: 0 },
},
servicesInfoWidget: {
loading: true,
failed: false,
data: { singbox: 0, podkop: 0 },
},
sectionsWidget: {
loading: true,
failed: false,
data: [],
},
};
export const store = new Store<StoreType>(initialStore);

177
fe-app-podkop/src/styles.ts Normal file
View File

@@ -0,0 +1,177 @@
// language=CSS
export const GlobalStyles = `
.cbi-value {
margin-bottom: 10px !important;
}
#diagnostics-status .table > div {
background: var(--background-color-primary);
border: 1px solid var(--border-color-medium);
border-radius: var(--border-radius);
}
#diagnostics-status .table > div pre,
#diagnostics-status .table > div div[style*="monospace"] {
color: var(--color-text-primary);
}
#diagnostics-status .alert-message {
background: var(--background-color-primary);
border-color: var(--border-color-medium);
}
#cbi-podkop:has(.cbi-tab-disabled[data-tab="basic"]) #cbi-podkop-extra {
display: none;
}
#cbi-podkop-main-_status > div {
width: 100%;
}
/* Dashboard styles */
.pdk_dashboard-page {
width: 100%;
--dashboard-grid-columns: 4;
}
@media (max-width: 900px) {
.pdk_dashboard-page {
--dashboard-grid-columns: 2;
}
}
.pdk_dashboard-page__widgets-section {
margin-top: 10px;
display: grid;
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
grid-gap: 10px;
}
.pdk_dashboard-page__widgets-section__item {
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
padding: 10px;
}
.pdk_dashboard-page__widgets-section__item__title {}
.pdk_dashboard-page__widgets-section__item__row {}
.pdk_dashboard-page__widgets-section__item__row--success .pdk_dashboard-page__widgets-section__item__row__value {
color: var(--success-color-medium, green);
}
.pdk_dashboard-page__widgets-section__item__row--error .pdk_dashboard-page__widgets-section__item__row__value {
color: var(--error-color-medium, red);
}
.pdk_dashboard-page__widgets-section__item__row__key {}
.pdk_dashboard-page__widgets-section__item__row__value {}
.pdk_dashboard-page__outbound-section {
margin-top: 10px;
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
padding: 10px;
}
.pdk_dashboard-page__outbound-section__title-section {
display: flex;
align-items: center;
justify-content: space-between;
}
.pdk_dashboard-page__outbound-section__title-section__title {
color: var(--text-color-high);
font-weight: 700;
}
.pdk_dashboard-page__outbound-grid {
margin-top: 5px;
display: grid;
grid-template-columns: repeat(var(--dashboard-grid-columns), 1fr);
grid-gap: 10px;
}
.pdk_dashboard-page__outbound-grid__item {
border: 2px var(--background-color-low, lightgray) solid;
border-radius: 4px;
padding: 10px;
transition: border 0.2s ease;
}
.pdk_dashboard-page__outbound-grid__item--selectable {
cursor: pointer;
}
.pdk_dashboard-page__outbound-grid__item--selectable:hover {
border-color: var(--primary-color-high, dodgerblue);
}
.pdk_dashboard-page__outbound-grid__item--active {
border-color: var(--success-color-medium, green);
}
.pdk_dashboard-page__outbound-grid__item__footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.pdk_dashboard-page__outbound-grid__item__type {}
.pdk_dashboard-page__outbound-grid__item__latency--empty {
color: var(--primary-color-low, lightgray);
}
.pdk_dashboard-page__outbound-grid__item__latency--green {
color: var(--success-color-medium, green);
}
.pdk_dashboard-page__outbound-grid__item__latency--yellow {
color: var(--warn-color-medium, orange);
}
.pdk_dashboard-page__outbound-grid__item__latency--red {
color: var(--error-color-medium, red);
}
.centered {
display: flex;
align-items: center;
justify-content: center;
}
/* Skeleton styles*/
.skeleton {
background-color: var(--background-color-low, #e0e0e0);
border-radius: 4px;
position: relative;
overflow: hidden;
}
.skeleton::after {
content: '';
position: absolute;
top: 0;
left: -150%;
width: 150%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
animation: skeleton-shimmer 1.6s infinite;
}
@keyframes skeleton-shimmer {
100% {
left: 150%;
}
}
`;

View File

@@ -0,0 +1,13 @@
import { BulkValidationResult, ValidationResult } from './types';
export function bulkValidate<T>(
values: T[],
validate: (value: T) => ValidationResult,
): BulkValidationResult<T> {
const results = values.map((value) => ({ ...validate(value), value }));
return {
valid: results.every((r) => r.valid),
results,
};
}

View File

@@ -0,0 +1,12 @@
export * from './validateIp';
export * from './validateDomain';
export * from './validateDns';
export * from './validateUrl';
export * from './validatePath';
export * from './validateSubnet';
export * from './bulkValidate';
export * from './validateShadowsocksUrl';
export * from './validateVlessUrl';
export * from './validateOutboundJson';
export * from './validateTrojanUrl';
export * from './validateProxyUrl';

View File

@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { validateDNS } from '../validateDns.js';
import { invalidIPs, validIPs } from './validateIp.test';
import { invalidDomains, validDomains } from './validateDomain.test';
const validDns = [...validIPs, ...validDomains];
const invalidDns = [...invalidIPs, ...invalidDomains];
describe('validateDns', () => {
describe.each(validDns)('Valid dns: %s', (_desc, domain) => {
it(`returns valid=true for "${domain}"`, () => {
const res = validateDNS(domain);
expect(res.valid).toBe(true);
});
});
describe.each(invalidDns)('Invalid dns: %s', (_desc, domain) => {
it(`returns valid=false for "${domain}"`, () => {
const res = validateDNS(domain);
expect(res.valid).toBe(false);
});
});
});

View File

@@ -0,0 +1,46 @@
import { describe, expect, it } from 'vitest';
import { validateDomain } from '../validateDomain';
export const validDomains = [
['Simple domain', 'example.com'],
['Subdomain', 'sub.example.com'],
['With dash', 'my-site.org'],
['With numbers', 'site123.net'],
['Deep subdomain', 'a.b.c.example.co.uk'],
['With path', 'example.com/path/to/resource'],
['Punycode RU', 'xn--d1acufc.xn--p1ai'],
['Adguard dns', 'dns.adguard-dns.com'],
['Nextdns dns', 'dns.nextdns.io/xxxxxxx'],
['Long domain (63 chars in label)', 'a'.repeat(63) + '.com'],
];
export const invalidDomains = [
['No TLD', 'localhost'],
['Only TLD', '.com'],
['Double dot', 'example..com'],
['Illegal chars', 'exa!mple.com'],
['Space inside', 'exa mple.com'],
['Ending with dash', 'example-.com'],
['Starting with dash', '-example.com'],
['Trailing dot', 'example.com.'],
['Too short TLD', 'example.c'],
['With protocol (not allowed)', 'http://example.com'],
['Too long label (>63 chars)', 'a'.repeat(64) + '.com'],
['Too long domain (>253 chars)', Array(40).fill('abcdef').join('.') + '.com'],
];
describe('validateDomain', () => {
describe.each(validDomains)('Valid domain: %s', (_desc, domain) => {
it(`returns valid=true for "${domain}"`, () => {
const res = validateDomain(domain);
expect(res.valid).toBe(true);
});
});
describe.each(invalidDomains)('Invalid domain: %s', (_desc, domain) => {
it(`returns valid=false for "${domain}"`, () => {
const res = validateDomain(domain);
expect(res.valid).toBe(false);
});
});
});

View File

@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { validateIPV4 } from '../validateIp';
export const validIPs = [
['Private LAN', '192.168.1.1'],
['All zeros', '0.0.0.0'],
['Broadcast', '255.255.255.255'],
['Simple', '1.2.3.4'],
['Loopback', '127.0.0.1'],
];
export const invalidIPs = [
['Octet too large', '256.0.0.1'],
['Too few octets', '192.168.1'],
['Too many octets', '1.2.3.4.5'],
['Leading zero (1st octet)', '01.2.3.4'],
['Leading zero (2nd octet)', '1.02.3.4'],
['Leading zero (3rd octet)', '1.2.003.4'],
['Leading zero (4th octet)', '1.2.3.004'],
['Four digits in octet', '1.2.3.0004'],
['Trailing dot', '1.2.3.'],
];
describe('validateIPV4', () => {
describe.each(validIPs)('Valid IP: %s', (_desc, ip) => {
it(`returns {valid:true} for "${ip}"`, () => {
const res = validateIPV4(ip);
expect(res.valid).toBe(true);
});
});
describe.each(invalidIPs)('Invalid IP: %s', (_desc, ip) => {
it(`returns {valid:false} for "${ip}"`, () => {
const res = validateIPV4(ip);
expect(res.valid).toBe(false);
});
});
});

View File

@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { validatePath } from '../validatePath';
export const validPaths = [
['Single level', '/etc'],
['Nested path', '/usr/local/bin'],
['With dash', '/var/log/nginx-access'],
['With underscore', '/opt/my_app/config'],
['With numbers', '/data123/files'],
['With dots', '/home/user/.config'],
['Deep nested', '/a/b/c/d/e/f/g'],
];
export const invalidPaths = [
['Empty string', ''],
['Missing starting slash', 'usr/local'],
['Only dot', '.'],
['Space inside', '/path with space'],
['Illegal char', '/path$'],
['Backslash not allowed', '\\windows\\path'],
['Relative path ./', './relative'],
['Relative path ../', '../parent'],
];
describe('validatePath', () => {
describe.each(validPaths)('Valid path: %s', (_desc, path) => {
it(`returns valid=true for "${path}"`, () => {
const res = validatePath(path);
expect(res.valid).toBe(true);
});
});
describe.each(invalidPaths)('Invalid path: %s', (_desc, path) => {
it(`returns valid=false for "${path}"`, () => {
const res = validatePath(path);
expect(res.valid).toBe(false);
});
});
});

View File

@@ -0,0 +1,41 @@
import { describe, it, expect } from 'vitest';
import { validateSubnet } from '../validateSubnet';
export const validSubnets = [
['Simple IP', '192.168.1.1'],
['With CIDR /24', '192.168.1.1/24'],
['CIDR /0', '10.0.0.1/0'],
['CIDR /32', '172.16.0.1/32'],
['Loopback', '127.0.0.1'],
['Broadcast with mask', '255.255.255.255/32'],
];
export const invalidSubnets = [
['Empty string', ''],
['Bad format letters', 'abc.def.ghi.jkl'],
['Octet too large', '300.1.1.1'],
['Negative octet', '-1.2.3.4'],
['Too many octets', '1.2.3.4.5'],
['Not enough octets', '192.168.1'],
['Leading zero octet', '01.2.3.4'],
['Invalid CIDR (too high)', '192.168.1.1/33'],
['Invalid CIDR (negative)', '192.168.1.1/-1'],
['CIDR not number', '192.168.1.1/abc'],
['Forbidden 0.0.0.0', '0.0.0.0'],
];
describe('validateSubnet', () => {
describe.each(validSubnets)('Valid subnet: %s', (_desc, subnet) => {
it(`returns {valid:true} for "${subnet}"`, () => {
const res = validateSubnet(subnet);
expect(res.valid).toBe(true);
});
});
describe.each(invalidSubnets)('Invalid subnet: %s', (_desc, subnet) => {
it(`returns {valid:false} for "${subnet}"`, () => {
const res = validateSubnet(subnet);
expect(res.valid).toBe(false);
});
});
});

View File

@@ -0,0 +1,40 @@
import { describe, it, expect } from 'vitest';
import { validateUrl } from '../validateUrl';
const validUrls = [
['Simple HTTP', 'http://example.com'],
['Simple HTTPS', 'https://example.com'],
['With path', 'https://example.com/path/to/page'],
['With query', 'https://example.com/?q=test'],
['With port', 'http://example.com:8080'],
['With subdomain', 'https://sub.example.com'],
];
const invalidUrls = [
['Invalid format', 'not a url'],
['Missing protocol', 'example.com'],
['Unsupported protocol (ftp)', 'ftp://example.com'],
['Unsupported protocol (ws)', 'ws://example.com'],
['Empty string', ''],
];
describe('validateUrl', () => {
describe.each(validUrls)('Valid URL: %s', (_desc, url) => {
it(`returns valid=true for "${url}"`, () => {
const res = validateUrl(url);
expect(res.valid).toBe(true);
});
});
describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => {
it(`returns valid=false for "${url}"`, () => {
const res = validateUrl(url);
expect(res.valid).toBe(false);
});
});
it('allows custom protocol list (ftp)', () => {
const res = validateUrl('ftp://example.com', ['ftp:']);
expect(res.valid).toBe(true);
});
});

View File

@@ -0,0 +1,100 @@
import { describe, it, expect } from 'vitest';
import { validateVlessUrl } from '../validateVlessUrl';
const validUrls = [
// TCP
[
'tcp + none',
'vless://94792286-7bbe-4f33-8b36-18d1bbf70723@127.0.0.1:34520?type=tcp&encryption=none&security=none#vless-tcp-none',
],
[
'tcp + reality',
'vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni=google.com&sid=f6&spx=%2F#vless-tcp-reality',
],
[
'tcp + tls',
'vless://2e9e8288-060e-4da2-8b9f-a1c81826feb7@127.0.0.1:19316?type=tcp&encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=google.com#vless-tcp-tls',
],
// mKCP
[
'mKCP + none',
'vless://72e201d7-7841-4a32-b266-4aa3eb776d51@127.0.0.1:17270?type=kcp&encryption=none&headerType=none&seed=AirziWi4ng&security=none#vless-mKCP',
],
// WebSocket
[
'ws + none',
'vless://d86daef7-565b-4ecd-a9ee-bac847ad38e6@127.0.0.1:12928?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=none#vless-websocket-none',
],
[
'ws + tls',
'vless://fe0f0941-09a9-4e46-bc69-e00190d7bb9c@127.0.0.1:10156?type=ws&encryption=none&path=%2Fwspath&host=google.com&security=tls&fp=chrome&sni=google.com#vless-websocket-tls',
],
// gRPC
[
'grpc + none',
'vless://974b39e3-f7bf-42b9-933c-16699c635e77@127.0.0.1:15633?type=grpc&encryption=none&serviceName=TunService&security=none#vless-gRPC-none',
],
[
'grpc + reality',
'vless://651e7eca-5152-46f1-baf2-d502e0af7b27@127.0.0.1:28535?type=grpc&encryption=none&serviceName=TunService&security=reality&pbk=nhZ7NiKfcqESa5ZeBFfsq9o18W-OWOAHLln9UmuVXSk&fp=chrome&sni=google.com&sid=11cbaeaa&spx=%2F#vless-gRPC-reality',
],
// HTTPUpgrade
[
'httpupgrade + none',
'vless://2b98f144-847f-42f7-8798-e1a32d27bdc7@127.0.0.1:47154?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=none#vless-httpupgrade-none',
],
[
'httpupgrade + tls',
'vless://76dbd0ff-1a35-4f0c-a9ba-3c5890b7dea6@127.0.0.1:50639?type=httpupgrade&encryption=none&path=%2Fhttpupgradepath&host=google.com&security=tls&sni=google.com#vless-httpupgrade-tls',
],
// XHTTP
[
'xhttp + none',
'vless://c2841505-ec32-4b8d-b6dd-3e19d648c321@127.0.0.1:45507?type=xhttp&encryption=none&path=%2Fxhttppath&host=xhttp&mode=auto&security=none#vless-xhttp',
],
];
const invalidUrls = [
['No prefix', 'uuid@host:443?type=tcp&security=tls'],
['No uuid', 'vless://@127.0.0.1:443?type=tcp&security=tls'],
['No host', 'vless://uuid@:443?type=tcp&security=tls'],
['No port', 'vless://uuid@127.0.0.1?type=tcp&security=tls'],
['Invalid port', 'vless://uuid@127.0.0.1:abc?type=tcp&security=tls'],
['Missing type', 'vless://uuid@127.0.0.1:443?security=tls'],
['Missing security', 'vless://uuid@127.0.0.1:443?type=tcp'],
[
'reality without pbk',
'vless://uuid@127.0.0.1:443?type=tcp&security=reality&fp=chrome',
],
[
'reality without fp',
'vless://uuid@127.0.0.1:443?type=tcp&security=reality&pbk=abc',
],
[
'tcp + reality + unexpected spaces',
'vless://e95163dc-905e-480a-afe5-20b146288679@127.0.0.1:16399?type=tcp&encryption=none&security=reality&pbk=tqhSkeDR6jsqC-BYCnZWBrdL33g705ba8tV5-ZboWTM&fp=chrome&sni= google.com&sid=f6&spx=%2F#vless-tcp-reality',
],
];
describe('validateVlessUrl', () => {
describe.each(validUrls)('Valid URL: %s', (_desc, url) => {
it(`returns valid=true for "${url}"`, () => {
const res = validateVlessUrl(url);
expect(res.valid).toBe(true);
});
});
describe.each(invalidUrls)('Invalid URL: %s', (_desc, url) => {
it(`returns valid=false for "${url}"`, () => {
const res = validateVlessUrl(url);
expect(res.valid).toBe(false);
});
});
it('detects invalid port range', () => {
const res = validateVlessUrl(
'vless://uuid@127.0.0.1:99999?type=tcp&security=tls',
);
expect(res.valid).toBe(false);
});
});

View File

@@ -0,0 +1,13 @@
export interface ValidationResult {
valid: boolean;
message: string;
}
export interface BulkValidationResultItem<T> extends ValidationResult {
value: T;
}
export interface BulkValidationResult<T> {
valid: boolean;
results: BulkValidationResultItem<T>[];
}

View File

@@ -0,0 +1,24 @@
import { validateDomain } from './validateDomain';
import { validateIPV4 } from './validateIp';
import { ValidationResult } from './types';
export function validateDNS(value: string): ValidationResult {
if (!value) {
return { valid: false, message: _('DNS server address cannot be empty') };
}
if (validateIPV4(value).valid) {
return { valid: true, message: _('Valid') };
}
if (validateDomain(value).valid) {
return { valid: true, message: _('Valid') };
}
return {
valid: false,
message: _(
'Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH',
),
};
}

View File

@@ -0,0 +1,21 @@
import { ValidationResult } from './types';
export function validateDomain(domain: string): ValidationResult {
const domainRegex =
/^(?=.{1,253}(?:\/|$))(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)\.)+(?:[a-zA-Z]{2,}|xn--[a-zA-Z0-9-]{1,59}[a-zA-Z0-9])(?:\/[^\s]*)?$/;
if (!domainRegex.test(domain)) {
return { valid: false, message: _('Invalid domain address') };
}
const hostname = domain.split('/')[0];
const parts = hostname.split('.');
const atLeastOneInvalidPart = parts.some((part) => part.length > 63);
if (atLeastOneInvalidPart) {
return { valid: false, message: _('Invalid domain address') };
}
return { valid: true, message: _('Valid') };
}

View File

@@ -0,0 +1,12 @@
import { ValidationResult } from './types';
export function validateIPV4(ip: string): ValidationResult {
const ipRegex =
/^(?:(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
if (ipRegex.test(ip)) {
return { valid: true, message: _('Valid') };
}
return { valid: false, message: _('Invalid IP address') };
}

View File

@@ -0,0 +1,21 @@
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',
),
};
}
return { valid: true, message: _('Valid') };
} catch {
return { valid: false, message: _('Invalid JSON format') };
}
}

View File

@@ -0,0 +1,26 @@
import { ValidationResult } from './types';
export function validatePath(value: string): ValidationResult {
if (!value) {
return {
valid: false,
message: _('Path cannot be empty'),
};
}
const pathRegex = /^\/[a-zA-Z0-9_\-/.]+$/;
if (pathRegex.test(value)) {
return {
valid: true,
message: _('Valid'),
};
}
return {
valid: false,
message: _(
'Invalid path format. Path must start with "/" and contain valid characters',
),
};
}

View File

@@ -0,0 +1,24 @@
import { ValidationResult } from './types';
import { validateShadowsocksUrl } from './validateShadowsocksUrl';
import { validateVlessUrl } from './validateVlessUrl';
import { validateTrojanUrl } from './validateTrojanUrl';
// TODO refactor current validation and add tests
export function validateProxyUrl(url: string): ValidationResult {
if (url.startsWith('ss://')) {
return validateShadowsocksUrl(url);
}
if (url.startsWith('vless://')) {
return validateVlessUrl(url);
}
if (url.startsWith('trojan://')) {
return validateTrojanUrl(url);
}
return {
valid: false,
message: _('URL must start with vless:// or ss:// or trojan://'),
};
}

View File

@@ -0,0 +1,96 @@
import { ValidationResult } from './types';
// TODO refactor current validation and add tests
export function validateShadowsocksUrl(url: string): ValidationResult {
if (!url.startsWith('ss://')) {
return {
valid: false,
message: _('Invalid Shadowsocks URL: must start with ss://'),
};
}
try {
if (!url || /\s/.test(url)) {
return {
valid: false,
message: _('Invalid Shadowsocks URL: must not contain spaces'),
};
}
const mainPart = url.includes('?') ? url.split('?')[0] : url.split('#')[0];
const encryptedPart = mainPart.split('/')[2]?.split('@')[0];
if (!encryptedPart) {
return {
valid: false,
message: _('Invalid Shadowsocks URL: missing credentials'),
};
}
try {
const decoded = atob(encryptedPart);
if (!decoded.includes(':')) {
return {
valid: false,
message: _(
'Invalid Shadowsocks URL: decoded credentials must contain method:password',
),
};
}
} catch (_e) {
if (!encryptedPart.includes(':') && !encryptedPart.includes('-')) {
return {
valid: false,
message: _(
'Invalid Shadowsocks URL: missing method and password separator ":"',
),
};
}
}
const serverPart = url.split('@')[1];
if (!serverPart) {
return {
valid: false,
message: _('Invalid Shadowsocks URL: missing server address'),
};
}
const [server, portAndRest] = serverPart.split(':');
if (!server) {
return {
valid: false,
message: _('Invalid Shadowsocks URL: missing server'),
};
}
const port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
if (!port) {
return {
valid: false,
message: _('Invalid Shadowsocks URL: missing port'),
};
}
const portNum = parseInt(port, 10);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return {
valid: false,
message: _('Invalid port number. Must be between 1 and 65535'),
};
}
} catch (_e) {
return {
valid: false,
message: _('Invalid Shadowsocks URL: parsing failed'),
};
}
return { valid: true, message: _('Valid') };
}

View File

@@ -0,0 +1,39 @@
import { ValidationResult } from './types';
import { validateIPV4 } from './validateIp';
export function validateSubnet(value: string): ValidationResult {
// Must be in form X.X.X.X or X.X.X.X/Y
const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(?:\/\d{1,2})?$/;
if (!subnetRegex.test(value)) {
return {
valid: false,
message: _('Invalid format. Use X.X.X.X or X.X.X.X/Y'),
};
}
const [ip, cidr] = value.split('/');
if (ip === '0.0.0.0') {
return { valid: false, message: _('IP address 0.0.0.0 is not allowed') };
}
const ipCheck = validateIPV4(ip);
if (!ipCheck.valid) {
return ipCheck;
}
// Validate CIDR if present
if (cidr) {
const cidrNum = parseInt(cidr, 10);
if (cidrNum < 0 || cidrNum > 32) {
return {
valid: false,
message: _('CIDR must be between 0 and 32'),
};
}
}
return { valid: true, message: _('Valid') };
}

View File

@@ -0,0 +1,35 @@
import { ValidationResult } from './types';
// TODO refactor current validation and add tests
export function validateTrojanUrl(url: string): ValidationResult {
if (!url.startsWith('trojan://')) {
return {
valid: false,
message: _('Invalid Trojan URL: must start with trojan://'),
};
}
if (!url || /\s/.test(url)) {
return {
valid: false,
message: _('Invalid Trojan URL: must not contain spaces'),
};
}
try {
const parsedUrl = new URL(url);
if (!parsedUrl.username || !parsedUrl.hostname || !parsedUrl.port) {
return {
valid: false,
message: _(
'Invalid Trojan URL: must contain username, hostname and port',
),
};
}
} catch (_e) {
return { valid: false, message: _('Invalid Trojan URL: parsing failed') };
}
return { valid: true, message: _('Valid') };
}

View File

@@ -0,0 +1,20 @@
import { ValidationResult } from './types';
export function validateUrl(
url: string,
protocols: string[] = ['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) {
return { valid: false, message: _('Invalid URL format') };
}
}

View File

@@ -0,0 +1,112 @@
import { ValidationResult } from './types';
export function validateVlessUrl(url: string): ValidationResult {
try {
const parsedUrl = new URL(url);
if (!url || /\s/.test(url)) {
return {
valid: false,
message: _('Invalid VLESS URL: must not contain spaces'),
};
}
if (parsedUrl.protocol !== 'vless:') {
return {
valid: false,
message: _('Invalid VLESS URL: must start with vless://'),
};
}
if (!parsedUrl.username) {
return { valid: false, message: _('Invalid VLESS URL: missing UUID') };
}
if (!parsedUrl.hostname) {
return { valid: false, message: _('Invalid VLESS URL: missing server') };
}
if (!parsedUrl.port) {
return { valid: false, message: _('Invalid VLESS URL: missing port') };
}
if (
isNaN(+parsedUrl.port) ||
+parsedUrl.port < 1 ||
+parsedUrl.port > 65535
) {
return {
valid: false,
message: _(
'Invalid VLESS URL: invalid port number. Must be between 1 and 65535',
),
};
}
if (!parsedUrl.search) {
return {
valid: false,
message: _('Invalid VLESS URL: missing query parameters'),
};
}
const params = new URLSearchParams(parsedUrl.search);
const type = params.get('type');
const validTypes = [
'tcp',
'raw',
'udp',
'grpc',
'http',
'httpupgrade',
'xhttp',
'ws',
'kcp',
];
if (!type || !validTypes.includes(type)) {
return {
valid: false,
message: _(
'Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws',
),
};
}
const security = params.get('security');
const validSecurities = ['tls', 'reality', 'none'];
if (!security || !validSecurities.includes(security)) {
return {
valid: false,
message: _(
'Invalid VLESS URL: security must be one of tls, reality, none',
),
};
}
if (security === 'reality') {
if (!params.get('pbk')) {
return {
valid: false,
message: _(
'Invalid VLESS URL: missing pbk parameter for reality security',
),
};
}
if (!params.get('fp')) {
return {
valid: false,
message: _(
'Invalid VLESS URL: missing fp parameter for reality security',
),
};
}
}
return { valid: true, message: _('Valid') };
} catch (_e) {
return { valid: false, message: _('Invalid VLESS URL: parsing failed') };
}
}

View File

@@ -0,0 +1,2 @@
// tests/setup/global-mocks.ts
globalThis._ = (key: string) => key;

View File

@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist"
},
"include": ["src"]
}

View File

@@ -0,0 +1,35 @@
import { defineConfig } from 'tsup';
import fs from 'fs';
import path from 'path';
export default defineConfig({
entry: ['src/main.ts'],
format: ['esm'], // пусть tsup генерит export {...}
outDir: '../luci-app-podkop/htdocs/luci-static/resources/view/podkop',
outExtension: () => ({ js: '.js' }),
dts: false,
clean: false,
sourcemap: false,
banner: {
js: `// This file is autogenerated, please don't change manually \n"use strict";`,
},
esbuildOptions(options) {
options.legalComments = 'none';
},
onSuccess: () => {
const outDir =
'../luci-app-podkop/htdocs/luci-static/resources/view/podkop';
const file = path.join(outDir, 'main.js');
let code = fs.readFileSync(file, 'utf8');
code = code.replace(
/export\s*{([\s\S]*?)}/,
(match, group) => {
return `return baseclass.extend({${group}})`;
}
);
fs.writeFileSync(file, code, 'utf8');
console.log(`✅ Patched LuCI build: ${file}`);
},
});

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
setupFiles: ['./tests/setup/global-mocks.ts'],
},
});

View File

@@ -0,0 +1,82 @@
import 'dotenv/config';
import chokidar from 'chokidar';
import SFTPClient from 'ssh2-sftp-client';
import path from 'path';
import fs from 'fs';
import { glob } from 'glob';
const sftp = new SFTPClient();
const config = {
host: process.env.SFTP_HOST,
port: Number(process.env.SFTP_PORT || 22),
username: process.env.SFTP_USER,
...(process.env.SFTP_PRIVATE_KEY
? { privateKey: fs.readFileSync(process.env.SFTP_PRIVATE_KEY) }
: { password: process.env.SFTP_PASS }),
};
const localDir = path.resolve(process.env.LOCAL_DIR || './dist');
const remoteDir = process.env.REMOTE_DIR || '/www/luci-static/mypkg';
async function uploadFile(filePath) {
const relativePath = path.relative(localDir, filePath);
const remotePath = path.posix.join(remoteDir, relativePath);
console.log(`Uploading: ${relativePath} -> ${remotePath}`);
try {
await sftp.fastPut(filePath, remotePath);
console.log(`Uploaded: ${relativePath}`);
} catch (err) {
console.error(`Failed: ${relativePath}: ${err.message}`);
}
}
async function deleteFile(filePath) {
const relativePath = path.relative(localDir, filePath);
const remotePath = path.posix.join(remoteDir, relativePath);
console.log(`Removing: ${relativePath}`);
try {
await sftp.delete(remotePath);
console.log(`Removed: ${relativePath}`);
} catch (err) {
console.warn(`Could not delete ${relativePath}: ${err.message}`);
}
}
async function uploadAllFiles() {
console.log('Uploading all files from', localDir);
const files = await glob(`${localDir}/**/*`, { nodir: true });
for (const file of files) {
await uploadFile(file);
}
console.log('Initial upload complete!');
}
async function main() {
await sftp.connect(config);
console.log(`Connected to ${config.host}`);
await uploadAllFiles();
chokidar
.watch(localDir, { ignoreInitial: true })
.on('all', async (event, filePath) => {
if (event === 'add' || event === 'change') {
await uploadFile(filePath);
} else if (event === 'unlink') {
await deleteFile(filePath);
}
});
process.on('SIGINT', async () => {
console.log('Disconnecting...');
await sftp.end();
process.exit();
});
}
main().catch(console.error);

2025
fe-app-podkop/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,417 +1,172 @@
#!/bin/sh
REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest"
IS_SHOULD_RESTART_NETWORK=
DOWNLOAD_DIR="/tmp/podkop"
COUNT=3
rm -rf "$DOWNLOAD_DIR"
mkdir -p "$DOWNLOAD_DIR"
msg() {
printf "\033[32;1m%s\033[0m\n" "$1"
}
main() {
check_system
sing_box
wget -qO- "$REPO" | grep -o 'https://[^"[:space:]]*\.ipk' | while read -r url; do
filename=$(basename "$url")
filepath="$DOWNLOAD_DIR/$filename"
attempt=0
while [ $attempt -lt $COUNT ]; do
if [ -f "$filepath" ] && [ -s "$filepath" ]; then
echo "$filename has already been uploaded"
break
fi
/usr/sbin/ntpd -q -p 194.190.168.1 -p 216.239.35.0 -p 216.239.35.4 -p 162.159.200.1 -p 162.159.200.123
echo "Download $filename (count $((attempt+1)))..."
wget -q -O "$filepath" "$url"
if [ -s "$filepath" ]; then
echo "$filename successfully downloaded"
break
else
echo "Download error $filename. Retry..."
rm -f "$filepath"
fi
attempt=$((attempt+1))
done
done
echo "opkg update"
opkg update
opkg update || { echo "opkg update failed"; exit 1; }
if [ -f "/etc/init.d/podkop" ]; then
printf "\033[32;1mPodkop is already installed. Just upgrade it? (y/n)\033[0m\n"
printf "\033[32;1my - Only upgrade podkop\033[0m\n"
printf "\033[32;1mn - Upgrade and install proxy or tunnels\033[0m\n"
msg "Podkop is already installed. Upgraded..."
else
msg "Installed podkop..."
fi
if command -v curl &> /dev/null; then
check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest")
while true; do
read -r -p '' UPDATE
case $UPDATE in
y)
echo "Upgraded podkop..."
break
;;
if echo "$check_response" | grep -q 'API rate limit '; then
msg "You've reached rate limit from GitHub. Repeat in five minutes."
exit 1
fi
fi
n)
add_tunnel
break
;;
*)
echo "Please enter y or n"
;;
esac
download_success=0
while read -r url; do
filename=$(basename "$url")
filepath="$DOWNLOAD_DIR/$filename"
attempt=0
while [ $attempt -lt $COUNT ]; do
msg "Download $filename (count $((attempt+1)))..."
if wget -q -O "$filepath" "$url"; then
if [ -s "$filepath" ]; then
msg "$filename successfully downloaded"
download_success=1
break
fi
fi
msg "Download error $filename. Retry..."
rm -f "$filepath"
attempt=$((attempt+1))
done
else
echo "Installed podkop..."
add_tunnel
fi
opkg install $DOWNLOAD_DIR/podkop*.ipk
opkg install $DOWNLOAD_DIR/luci-app-podkop*.ipk
echo "Русский язык интерфейса ставим? y/n (Need a Russian translation?)"
while true; do
read -r -p '' RUS
case $RUS in
y)
opkg install $DOWNLOAD_DIR/luci-i18n-podkop-ru*.ipk
break
;;
n)
break
;;
*)
echo "Please enter y or n"
;;
esac
done
rm -f $DOWNLOAD_DIR/podkop*.ipk $DOWNLOAD_DIR/luci-app-podkop*.ipk $DOWNLOAD_DIR/luci-i18n-podkop-ru*.ipk
if [ "$IS_SHOULD_RESTART_NETWORK" ]; then
printf "\033[32;1mRestart network\033[0m\n"
/etc/init.d/network restart
fi
}
add_tunnel() {
echo "Will you be using Wireguard, AmneziaWG, OpenVPN, OpenConnect? If yes, select a number and they will be automatically installed"
echo "1) Wireguard"
echo "2) AmneziaWG"
echo "3) OpenVPN"
echo "4) OpenConnect"
echo "5) I use VLESS/SS. Skip this step"
while true; do
read -r -p '' TUNNEL
case $TUNNEL in
1)
opkg install wireguard-tools luci-proto-wireguard luci-app-wireguard
printf "\033[32;1mDo you want to configure the wireguard interface? (y/n): \033[0m\n"
read IS_SHOULD_CONFIGURE_WG_INTERFACE
if [ "$IS_SHOULD_CONFIGURE_WG_INTERFACE" = "y" ] || [ "$IS_SHOULD_CONFIGURE_WG_INTERFACE" = "Y" ]; then
wg_awg_setup Wireguard
else
printf "\e[1;32mUse these instructions to manual configure https://itdog.info/nastrojka-klienta-wireguard-na-openwrt/\e[0m\n"
fi
break
;;
2)
install_awg_packages
printf "\033[32;1mThere are no instructions for manual configure yet. Do you want to configure the amneziawg interface? (y/n): \033[0m\n"
read IS_SHOULD_CONFIGURE_WG_INTERFACE
if [ "$IS_SHOULD_CONFIGURE_WG_INTERFACE" = "y" ] || [ "$IS_SHOULD_CONFIGURE_WG_INTERFACE" = "Y" ]; then
wg_awg_setup AmneziaWG
fi
break
;;
3)
opkg install opkg install openvpn-openssl luci-app-openvpn
printf "\e[1;32mUse these instructions to configure https://itdog.info/nastrojka-klienta-openvpn-na-openwrt/\e[0m\n"
break
;;
4)
opkg install opkg install openconnect luci-proto-openconnect
printf "\e[1;32mUse these instructions to configure https://itdog.info/nastrojka-klienta-openconnect-na-openwrt/\e[0m\n"
break
;;
5)
echo "Skip. Use this if you're installing an upgrade."
break
;;
*)
echo "Choose from the following options"
;;
esac
done
}
handler_network_restart() {
IS_SHOULD_RESTART_NETWORK=true
}
install_awg_packages() {
# Получение pkgarch с наибольшим приоритетом
PKGARCH=$(opkg print-architecture | awk 'BEGIN {max=0} {if ($3 > max) {max = $3; arch = $2}} END {print arch}')
TARGET=$(ubus call system board | jsonfilter -e '@.release.target' | cut -d '/' -f 1)
SUBTARGET=$(ubus call system board | jsonfilter -e '@.release.target' | cut -d '/' -f 2)
VERSION=$(ubus call system board | jsonfilter -e '@.release.version')
PKGPOSTFIX="_v${VERSION}_${PKGARCH}_${TARGET}_${SUBTARGET}.ipk"
BASE_URL="https://github.com/Slava-Shchipunov/awg-openwrt/releases/download/"
AWG_DIR="/tmp/amneziawg"
mkdir -p "$AWG_DIR"
if opkg list-installed | grep -q kmod-amneziawg; then
echo "kmod-amneziawg already installed"
else
KMOD_AMNEZIAWG_FILENAME="kmod-amneziawg${PKGPOSTFIX}"
DOWNLOAD_URL="${BASE_URL}v${VERSION}/${KMOD_AMNEZIAWG_FILENAME}"
wget -O "$AWG_DIR/$KMOD_AMNEZIAWG_FILENAME" "$DOWNLOAD_URL"
if [ $? -eq 0 ]; then
echo "kmod-amneziawg file downloaded successfully"
else
echo "Error downloading kmod-amneziawg. Please, install kmod-amneziawg manually and run the script again"
exit 1
fi
opkg install "$AWG_DIR/$KMOD_AMNEZIAWG_FILENAME"
if [ $? -eq 0 ]; then
echo "kmod-amneziawg file downloaded successfully"
else
echo "Error installing kmod-amneziawg. Please, install kmod-amneziawg manually and run the script again"
exit 1
fi
fi
if opkg list-installed | grep -q amneziawg-tools; then
echo "amneziawg-tools already installed"
else
AMNEZIAWG_TOOLS_FILENAME="amneziawg-tools${PKGPOSTFIX}"
DOWNLOAD_URL="${BASE_URL}v${VERSION}/${AMNEZIAWG_TOOLS_FILENAME}"
wget -O "$AWG_DIR/$AMNEZIAWG_TOOLS_FILENAME" "$DOWNLOAD_URL"
if [ $? -eq 0 ]; then
echo "amneziawg-tools file downloaded successfully"
else
echo "Error downloading amneziawg-tools. Please, install amneziawg-tools manually and run the script again"
exit 1
fi
opkg install "$AWG_DIR/$AMNEZIAWG_TOOLS_FILENAME"
if [ $? -eq 0 ]; then
echo "amneziawg-tools file downloaded successfully"
else
echo "Error installing amneziawg-tools. Please, install amneziawg-tools manually and run the script again"
exit 1
if [ $attempt -eq $COUNT ]; then
msg "Failed to download $filename after $COUNT attempts"
fi
done < <(wget -qO- "$REPO" | grep -o 'https://[^"[:space:]]*\.ipk')
if [ $download_success -eq 0 ]; then
msg "No packages were downloaded successfully"
exit 1
fi
if opkg list-installed | grep -q luci-app-amneziawg; then
echo "luci-app-amneziawg already installed"
else
LUCI_APP_AMNEZIAWG_FILENAME="luci-app-amneziawg${PKGPOSTFIX}"
DOWNLOAD_URL="${BASE_URL}v${VERSION}/${LUCI_APP_AMNEZIAWG_FILENAME}"
wget -O "$AWG_DIR/$LUCI_APP_AMNEZIAWG_FILENAME" "$DOWNLOAD_URL"
if [ $? -eq 0 ]; then
echo "luci-app-amneziawg file downloaded successfully"
else
echo "Error downloading luci-app-amneziawg. Please, install luci-app-amneziawg manually and run the script again"
exit 1
fi
opkg install "$AWG_DIR/$LUCI_APP_AMNEZIAWG_FILENAME"
if [ $? -eq 0 ]; then
echo "luci-app-amneziawg file downloaded successfully"
else
echo "Error installing luci-app-amneziawg. Please, install luci-app-amneziawg manually and run the script again"
exit 1
fi
fi
rm -rf "$AWG_DIR"
}
wg_awg_setup() {
PROTOCOL_NAME=$1
printf "\033[32;1mConfigure ${PROTOCOL_NAME}\033[0m\n"
if [ "$PROTOCOL_NAME" = 'Wireguard' ]; then
INTERFACE_NAME="wg0"
CONFIG_NAME="wireguard_wg0"
PROTO="wireguard"
ZONE_NAME="wg"
fi
if [ "$PROTOCOL_NAME" = 'AmneziaWG' ]; then
INTERFACE_NAME="awg0"
CONFIG_NAME="amneziawg_awg0"
PROTO="amneziawg"
ZONE_NAME="awg"
echo "Do you want to use AmneziaWG config or basic Wireguard config + automatic obfuscation?"
echo "1) AmneziaWG"
echo "2) Wireguard + automatic obfuscation"
read CONFIG_TYPE
fi
read -r -p "Enter the private key (from [Interface]):"$'\n' WG_PRIVATE_KEY_INT
while true; do
read -r -p "Enter internal IP address with subnet, example 192.168.100.5/24 (from [Interface]):"$'\n' WG_IP
if echo "$WG_IP" | egrep -oq '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]+$'; then
break
else
echo "This IP is not valid. Please repeat"
for pkg in podkop luci-app-podkop; do
file=$(ls "$DOWNLOAD_DIR" | grep "^$pkg" | head -n 1)
if [ -n "$file" ]; then
msg "Installing $file"
opkg install "$DOWNLOAD_DIR/$file"
sleep 3
fi
done
read -r -p "Enter the public key (from [Peer]):"$'\n' WG_PUBLIC_KEY_INT
read -r -p "If use PresharedKey, Enter this (from [Peer]). If your don't use leave blank:"$'\n' WG_PRESHARED_KEY_INT
read -r -p "Enter Endpoint host without port (Domain or IP) (from [Peer]):"$'\n' WG_ENDPOINT_INT
read -r -p "Enter Endpoint host port (from [Peer]) [51820]:"$'\n' WG_ENDPOINT_PORT_INT
WG_ENDPOINT_PORT_INT=${WG_ENDPOINT_PORT_INT:-51820}
if [ "$WG_ENDPOINT_PORT_INT" = '51820' ]; then
echo $WG_ENDPOINT_PORT_INT
fi
if [ "$PROTOCOL_NAME" = 'AmneziaWG' ]; then
if [ "$CONFIG_TYPE" = '1' ]; then
read -r -p "Enter Jc value (from [Interface]):"$'\n' AWG_JC
read -r -p "Enter Jmin value (from [Interface]):"$'\n' AWG_JMIN
read -r -p "Enter Jmax value (from [Interface]):"$'\n' AWG_JMAX
read -r -p "Enter S1 value (from [Interface]):"$'\n' AWG_S1
read -r -p "Enter S2 value (from [Interface]):"$'\n' AWG_S2
read -r -p "Enter H1 value (from [Interface]):"$'\n' AWG_H1
read -r -p "Enter H2 value (from [Interface]):"$'\n' AWG_H2
read -r -p "Enter H3 value (from [Interface]):"$'\n' AWG_H3
read -r -p "Enter H4 value (from [Interface]):"$'\n' AWG_H4
elif [ "$CONFIG_TYPE" = '2' ]; then
#Default values to wg automatic obfuscation
AWG_JC=4
AWG_JMIN=40
AWG_JMAX=70
AWG_S1=0
AWG_S2=0
AWG_H1=1
AWG_H2=2
AWG_H3=3
AWG_H4=4
ru=$(ls "$DOWNLOAD_DIR" | grep "luci-i18n-podkop-ru" | head -n 1)
if [ -n "$ru" ]; then
if opkg list-installed | grep -q luci-i18n-podkop-ru; then
msg "Upgraded ru translation..."
opkg remove luci-i18n-podkop*
opkg install "$DOWNLOAD_DIR/$ru"
else
msg "Русский язык интерфейса ставим? y/n (Need a Russian translation?)"
while true; do
read -r -p '' RUS
case $RUS in
y)
opkg remove luci-i18n-podkop*
opkg install "$DOWNLOAD_DIR/$ru"
break
;;
n)
break
;;
*)
echo "Введите y или n"
;;
esac
done
fi
fi
uci set network.${INTERFACE_NAME}=interface
uci set network.${INTERFACE_NAME}.proto=$PROTO
uci set network.${INTERFACE_NAME}.private_key=$WG_PRIVATE_KEY_INT
uci set network.${INTERFACE_NAME}.listen_port='51821'
uci set network.${INTERFACE_NAME}.addresses=$WG_IP
if [ "$PROTOCOL_NAME" = 'AmneziaWG' ]; then
uci set network.${INTERFACE_NAME}.awg_jc=$AWG_JC
uci set network.${INTERFACE_NAME}.awg_jmin=$AWG_JMIN
uci set network.${INTERFACE_NAME}.awg_jmax=$AWG_JMAX
uci set network.${INTERFACE_NAME}.awg_s1=$AWG_S1
uci set network.${INTERFACE_NAME}.awg_s2=$AWG_S2
uci set network.${INTERFACE_NAME}.awg_h1=$AWG_H1
uci set network.${INTERFACE_NAME}.awg_h2=$AWG_H2
uci set network.${INTERFACE_NAME}.awg_h3=$AWG_H3
uci set network.${INTERFACE_NAME}.awg_h4=$AWG_H4
fi
if ! uci show network | grep -q ${CONFIG_NAME}; then
uci add network ${CONFIG_NAME}
fi
uci set network.@${CONFIG_NAME}[0]=$CONFIG_NAME
uci set network.@${CONFIG_NAME}[0].name="${INTERFACE_NAME}_client"
uci set network.@${CONFIG_NAME}[0].public_key=$WG_PUBLIC_KEY_INT
uci set network.@${CONFIG_NAME}[0].preshared_key=$WG_PRESHARED_KEY_INT
uci set network.@${CONFIG_NAME}[0].route_allowed_ips='0'
uci set network.@${CONFIG_NAME}[0].persistent_keepalive='25'
uci set network.@${CONFIG_NAME}[0].endpoint_host=$WG_ENDPOINT_INT
uci set network.@${CONFIG_NAME}[0].allowed_ips='0.0.0.0/0'
uci set network.@${CONFIG_NAME}[0].endpoint_port=$WG_ENDPOINT_PORT_INT
uci commit network
if ! uci show firewall | grep -q "@zone.*name='${ZONE_NAME}'"; then
printf "\033[32;1mZone Create\033[0m\n"
uci add firewall zone
uci set firewall.@zone[-1].name=$ZONE_NAME
uci set firewall.@zone[-1].network=$INTERFACE_NAME
uci set firewall.@zone[-1].forward='REJECT'
uci set firewall.@zone[-1].output='ACCEPT'
uci set firewall.@zone[-1].input='REJECT'
uci set firewall.@zone[-1].masq='1'
uci set firewall.@zone[-1].mtu_fix='1'
uci set firewall.@zone[-1].family='ipv4'
uci commit firewall
fi
if ! uci show firewall | grep -q "@forwarding.*name='${ZONE_NAME}'"; then
printf "\033[32;1mConfigured forwarding\033[0m\n"
uci add firewall forwarding
uci set firewall.@forwarding[-1]=forwarding
uci set firewall.@forwarding[-1].name="${ZONE_NAME}-lan"
uci set firewall.@forwarding[-1].dest=${ZONE_NAME}
uci set firewall.@forwarding[-1].src='lan'
uci set firewall.@forwarding[-1].family='ipv4'
uci commit firewall
fi
handler_network_restart
find "$DOWNLOAD_DIR" -type f -name '*podkop*' -exec rm {} \;
}
check_system() {
# Get router model
MODEL=$(cat /tmp/sysinfo/model)
echo "Router model: $MODEL"
msg "Router model: $MODEL"
# Check OpenWrt version
openwrt_version=$(cat /etc/openwrt_release | grep DISTRIB_RELEASE | cut -d"'" -f2 | cut -d'.' -f1)
if [ "$openwrt_version" = "23" ]; then
msg "OpenWrt 23.05 не поддерживается начиная с podkop 0.5.0"
msg "Для OpenWrt 23.05 используйте podkop версии 0.4.11 или устанавливайте зависимости и podkop вручную"
msg "Подробности: https://podkop.net/docs/install/#%d1%83%d1%81%d1%82%d0%b0%d0%bd%d0%be%d0%b2%d0%ba%d0%b0-%d0%bd%d0%b0-2305"
exit 1
fi
# Check available space
AVAILABLE_SPACE=$(df /tmp | awk 'NR==2 {print $4}')
AVAILABLE_SPACE=$(df /overlay | awk 'NR==2 {print $4}')
REQUIRED_SPACE=15360 # 15MB in KB
echo "Available space: $((AVAILABLE_SPACE/1024))MB"
echo "Required space: $((REQUIRED_SPACE/1024))MB"
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then
echo "Error: Insufficient space in /tmp"
echo "Available: $((AVAILABLE_SPACE/1024))MB"
echo "Required: $((REQUIRED_SPACE/1024))MB"
msg "Error: Insufficient space in flash"
msg "Available: $((AVAILABLE_SPACE/1024))MB"
msg "Required: $((REQUIRED_SPACE/1024))MB"
exit 1
fi
}
fi
sing_box() {
sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}')
required_version="1.11.1"
if ! nslookup google.com >/dev/null 2>&1; then
msg "DNS not working"
exit 1
fi
if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
opkg remove sing-box
if opkg list-installed | grep -q https-dns-proxy; then
msg "Сonflicting package detected: https-dns-proxy. Remove?"
while true; do
read -r -p '' DNSPROXY
case $DNSPROXY in
yes|y|Y|yes)
opkg remove --force-depends luci-app-https-dns-proxy https-dns-proxy luci-i18n-https-dns-proxy*
break
;;
*)
msg "Exit"
exit 1
;;
esac
done
fi
}
main
sing_box() {
if ! opkg list-installed | grep -q "^sing-box"; then
return
fi
sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}')
required_version="1.12.4"
if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
msg "sing-box version $sing_box_version is older than required $required_version"
msg "Removing old version..."
service podkop stop
opkg remove sing-box --force-depends
fi
}
main

View File

@@ -1,7 +1,9 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-podkop
PKG_VERSION:=0.3.6
PKG_VERSION := $(if $(PKG_VERSION),$(PKG_VERSION),dev_$(shell date +%d%m%Y))
PKG_RELEASE:=1
LUCI_TITLE:=LuCI podkop app

View File

@@ -0,0 +1,362 @@
'use strict';
'require form';
'require baseclass';
'require tools.widgets as widgets';
'require view.podkop.main as main';
function createAdditionalSection(mainSection) {
let o = mainSection.tab('additional', _('Additional Settings'));
o = mainSection.taboption(
'additional',
form.Flag,
'yacd',
_('Yacd enable'),
`<a href="${main.getBaseUrl()}:9090/ui" target="_blank">${main.getBaseUrl()}:9090/ui</a>`,
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'additional',
form.Flag,
'exclude_ntp',
_('Exclude NTP'),
_('Allows you to exclude NTP protocol traffic from the tunnel'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'additional',
form.Flag,
'quic_disable',
_('QUIC disable'),
_('For issues with the video stream'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'additional',
form.ListValue,
'update_interval',
_('List Update Frequency'),
_('Select how often the lists will be updated'),
);
Object.entries(main.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = '1d';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'additional',
form.ListValue,
'dns_type',
_('DNS Protocol Type'),
_('Select DNS protocol to use'),
);
o.value('doh', _('DNS over HTTPS (DoH)'));
o.value('dot', _('DNS over TLS (DoT)'));
o.value('udp', _('UDP (Unprotected DNS)'));
o.default = 'udp';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'additional',
form.Value,
'dns_server',
_('DNS Server'),
_('Select or enter DNS server address'),
);
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = '8.8.8.8';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
const validation = main.validateDNS(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = mainSection.taboption(
'additional',
form.Value,
'bootstrap_dns_server',
_('Bootstrap DNS server'),
_(
'The DNS server used to look up the IP address of an upstream DNS server',
),
);
Object.entries(main.BOOTSTRAP_DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = '77.88.8.8';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
const validation = main.validateDNS(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = mainSection.taboption(
'additional',
form.Value,
'dns_rewrite_ttl',
_('DNS Rewrite TTL'),
_('Time in seconds for DNS record caching (default: 60)'),
);
o.default = '60';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
if (!value) {
return _('TTL value cannot be empty');
}
const ttl = parseInt(value);
if (isNaN(ttl) || ttl < 0) {
return _('TTL must be a positive number');
}
return true;
};
o = mainSection.taboption(
'additional',
form.ListValue,
'config_path',
_('Config File Path'),
_(
'Select path for sing-box config file. Change this ONLY if you know what you are doing',
),
);
o.value('/etc/sing-box/config.json', 'Flash (/etc/sing-box/config.json)');
o.value('/tmp/sing-box/config.json', 'RAM (/tmp/sing-box/config.json)');
o.default = '/etc/sing-box/config.json';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'additional',
form.Value,
'cache_path',
_('Cache File Path'),
_(
'Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing',
),
);
o.value('/tmp/sing-box/cache.db', 'RAM (/tmp/sing-box/cache.db)');
o.value(
'/usr/share/sing-box/cache.db',
'Flash (/usr/share/sing-box/cache.db)',
);
o.default = '/tmp/sing-box/cache.db';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
if (!value) {
return _('Cache file path cannot be empty');
}
if (!value.startsWith('/')) {
return _('Path must be absolute (start with /)');
}
if (!value.endsWith('cache.db')) {
return _('Path must end with cache.db');
}
const parts = value.split('/').filter(Boolean);
if (parts.length < 2) {
return _('Path must contain at least one directory (like /tmp/cache.db)');
}
return true;
};
o = mainSection.taboption(
'additional',
widgets.DeviceSelect,
'iface',
_('Source Network Interface'),
_('Select the network interface from which the traffic will originate'),
);
o.ucisection = 'main';
o.default = 'br-lan';
o.noaliases = true;
o.nobridges = false;
o.noinactive = false;
o.multiple = true;
o.filter = function (section_id, value) {
// Block specific interface names from being selectable
const blocked = ['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan'];
if (blocked.includes(value)) {
return false;
}
// Try to find the device object by its name
const device = this.devices.find((dev) => dev.getName() === value);
// If no device is found, allow the value
if (!device) {
return true;
}
// Check the type of the device
const type = device.getType();
// Consider any Wi-Fi / wireless / wlan device as invalid
const isWireless =
type === 'wifi' || type === 'wireless' || type.includes('wlan');
// Allow only non-wireless devices
return !isWireless;
};
o = mainSection.taboption(
'additional',
form.Flag,
'mon_restart_ifaces',
_('Interface monitoring'),
_('Interface monitoring for bad WAN'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'additional',
widgets.NetworkSelect,
'restart_ifaces',
_('Interface for monitoring'),
_('Select the WAN interfaces to be monitored'),
);
o.ucisection = 'main';
o.depends('mon_restart_ifaces', '1');
o.multiple = true;
o.filter = function (section_id, value) {
// Reject if the value is in the blocked list ['lan', 'loopback']
if (['lan', 'loopback'].includes(value)) {
return false;
}
// Reject if the value starts with '@' (means it's an alias/reference)
if (value.startsWith('@')) {
return false;
}
// Otherwise allow it
return true;
};
o = mainSection.taboption(
'additional',
form.Value,
'procd_reload_delay',
_('Interface Monitoring Delay'),
_('Delay in milliseconds before reloading podkop after interface UP'),
);
o.ucisection = 'main';
o.depends('mon_restart_ifaces', '1');
o.default = '2000';
o.rmempty = false;
o.validate = function (section_id, value) {
if (!value) {
return _('Delay value cannot be empty');
}
return true;
};
o = mainSection.taboption(
'additional',
form.Flag,
'dont_touch_dhcp',
_('Dont touch my DHCP!'),
_('Podkop will not change the DHCP config'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'additional',
form.Flag,
'detour',
_('Proxy download of lists'),
_('Downloading all lists via main Proxy/VPN'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
// Extra IPs and exclusions (main section)
o = mainSection.taboption(
'basic',
form.Flag,
'exclude_from_ip_enabled',
_('IP for exclusion'),
_('Specify local IP addresses that will never use the configured route'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
o = mainSection.taboption(
'basic',
form.DynamicList,
'exclude_traffic_ip',
_('Local IPs'),
_('Enter valid IPv4 addresses'),
);
o.placeholder = 'IP';
o.depends('exclude_from_ip_enabled', '1');
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateIPV4(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = mainSection.taboption(
'basic',
form.Flag,
'socks5',
_('Mixed enable'),
_('Browser port: 2080'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = 'main';
}
return baseclass.extend({
createAdditionalSection,
});

View File

@@ -0,0 +1,783 @@
'use strict';
'require baseclass';
'require form';
'require ui';
'require network';
'require view.podkop.main as main';
'require tools.widgets as widgets';
function createConfigSection(section) {
const s = section;
let o = s.tab('basic', _('Basic Settings'));
o = s.taboption(
'basic',
form.ListValue,
'mode',
_('Connection Type'),
_('Select between VPN and Proxy connection methods for traffic routing'),
);
o.value('proxy', 'Proxy');
o.value('vpn', 'VPN');
o.value('block', 'Block');
o.ucisection = s.section;
o = s.taboption(
'basic',
form.ListValue,
'proxy_config_type',
_('Configuration Type'),
_('Select how to configure the proxy'),
);
o.value('url', _('Connection URL'));
o.value('outbound', _('Outbound Config'));
o.value('urltest', _('URLTest'));
o.default = 'url';
o.depends('mode', 'proxy');
o.ucisection = s.section;
o = s.taboption(
'basic',
form.TextValue,
'proxy_string',
_('Proxy Configuration URL'),
'',
);
o.depends('proxy_config_type', 'url');
o.rows = 5;
// Enable soft wrapping for multi-line proxy URLs (e.g., for URLTest proxy links)
o.wrap = 'soft';
// Render as a textarea to allow multiple proxy URLs/configs
o.textarea = true;
o.rmempty = false;
o.ucisection = s.section;
o.sectionDescriptions = new Map();
o.placeholder =
'vless://uuid@server:port?type=tcp&security=tls#main\n// backup ss://method:pass@server:port\n// backup2 vless://uuid@server:port?type=grpc&security=reality#alt\n// backup3 trojan://04agAQapcl@127.0.0.1:33641?type=tcp&security=none#trojan-tcp-none';
o.renderWidget = function (section_id, option_index, cfgvalue) {
const original = form.TextValue.prototype.renderWidget.apply(this, [
section_id,
option_index,
cfgvalue,
]);
const container = E('div', {});
container.appendChild(original);
if (cfgvalue) {
try {
const activeConfig = cfgvalue
.split('\n')
.map((line) => line.trim())
.find((line) => line && !line.startsWith('//'));
if (activeConfig) {
if (activeConfig.includes('#')) {
const label = activeConfig.split('#').pop();
if (label && label.trim()) {
const decodedLabel = decodeURIComponent(label);
const descDiv = E(
'div',
{ class: 'cbi-value-description' },
_('Current config: ') + decodedLabel,
);
container.appendChild(descDiv);
} else {
const descDiv = E(
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
);
container.appendChild(descDiv);
}
} else {
const descDiv = E(
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
);
container.appendChild(descDiv);
}
}
} catch (e) {
console.error('Error parsing config label:', e);
const descDiv = E(
'div',
{ class: 'cbi-value-description' },
_('Config without description'),
);
container.appendChild(descDiv);
}
} else {
const defaultDesc = E(
'div',
{ class: 'cbi-value-description' },
_(
'Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs',
),
);
container.appendChild(defaultDesc);
}
return container;
};
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
try {
const activeConfigs = value
.split('\n')
.map((line) => line.trim())
.filter((line) => !line.startsWith('//'))
.filter(Boolean);
if (!activeConfigs.length) {
return _(
'No active configuration found. One configuration is required.',
);
}
if (activeConfigs.length > 1) {
return _(
'Multiply active configurations found. Please leave one configuration.',
);
}
const validation = main.validateProxyUrl(activeConfigs[0]);
if (validation.valid) {
return true;
}
return validation.message;
} catch (e) {
return `${_('Invalid URL format:')} ${e?.message}`;
}
};
o = s.taboption(
'basic',
form.TextValue,
'outbound_json',
_('Outbound Configuration'),
_('Enter complete outbound configuration in JSON format'),
);
o.depends('proxy_config_type', 'outbound');
o.rows = 10;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateOutboundJson(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.DynamicList,
'urltest_proxy_links',
_('URLTest Proxy Links'),
);
o.depends('proxy_config_type', 'urltest');
o.placeholder = 'vless://, ss://, trojan:// links';
o.rmempty = false;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateProxyUrl(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.Flag,
'ss_uot',
_('Shadowsocks UDP over TCP'),
_('Apply for SS2022'),
);
o.default = '0';
o.depends('mode', 'proxy');
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
widgets.DeviceSelect,
'interface',
_('Network Interface'),
_('Select network interface for VPN connection'),
);
o.depends('mode', 'vpn');
o.ucisection = s.section;
o.noaliases = true;
o.nobridges = false;
o.noinactive = false;
o.filter = function (section_id, value) {
// Blocked interface names that should never be selectable
const blockedInterfaces = [
'br-lan',
'eth0',
'eth1',
'wan',
'phy0-ap0',
'phy1-ap0',
'pppoe-wan',
'lan',
];
// Reject immediately if the value matches any blocked interface
if (blockedInterfaces.includes(value)) {
return false;
}
// Try to find the device object with the given name
const device = this.devices.find((dev) => dev.getName() === value);
// If no device is found, allow the value
if (!device) {
return true;
}
// Get the device type (e.g., "wifi", "ethernet", etc.)
const type = device.getType();
// Reject wireless-related devices
const isWireless =
type === 'wifi' || type === 'wireless' || type.includes('wlan');
return !isWireless;
};
o = s.taboption(
'basic',
form.Flag,
'domain_resolver_enabled',
_('Domain Resolver'),
_('Enable built-in DNS resolver for domains handled by this section'),
);
o.default = '0';
o.rmempty = false;
o.depends('mode', 'vpn');
o.ucisection = s.section;
o = s.taboption(
'basic',
form.ListValue,
'domain_resolver_dns_type',
_('DNS Protocol Type'),
_('Select the DNS protocol type for the domain resolver'),
);
o.value('doh', _('DNS over HTTPS (DoH)'));
o.value('dot', _('DNS over TLS (DoT)'));
o.value('udp', _('UDP (Unprotected DNS)'));
o.default = 'udp';
o.rmempty = false;
o.depends('domain_resolver_enabled', '1');
o.ucisection = s.section;
o = s.taboption(
'basic',
form.Value,
'domain_resolver_dns_server',
_('DNS Server'),
_('Select or enter DNS server address'),
);
Object.entries(main.DNS_SERVER_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.default = '8.8.8.8';
o.rmempty = false;
o.depends('domain_resolver_enabled', '1');
o.ucisection = s.section;
o.validate = function (section_id, value) {
const validation = main.validateDNS(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.Flag,
'community_lists_enabled',
_('Community Lists'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'community_lists',
_('Service List'),
_('Select predefined service for routing') +
' <a href="https://github.com/itdoginfo/allow-domains" target="_blank">github.com/itdoginfo/allow-domains</a>',
);
o.placeholder = 'Service list';
Object.entries(main.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => {
o.value(key, _(label));
});
o.depends('community_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
let lastValues = [];
let isProcessing = false;
o.onchange = function (ev, section_id, value) {
if (isProcessing) return;
isProcessing = true;
try {
const values = Array.isArray(value) ? value : [value];
let newValues = [...values];
let notifications = [];
const selectedRegionalOptions = main.REGIONAL_OPTIONS.filter((opt) =>
newValues.includes(opt),
);
if (selectedRegionalOptions.length > 1) {
const lastSelected =
selectedRegionalOptions[selectedRegionalOptions.length - 1];
const removedRegions = selectedRegionalOptions.slice(0, -1);
newValues = newValues.filter(
(v) => v === lastSelected || !main.REGIONAL_OPTIONS.includes(v),
);
notifications.push(
E('p', { class: 'alert-message warning' }, [
E('strong', {}, _('Regional options cannot be used together')),
E('br'),
_(
'Warning: %s cannot be used together with %s. Previous selections have been removed.',
).format(removedRegions.join(', '), lastSelected),
]),
);
}
if (newValues.includes('russia_inside')) {
const removedServices = newValues.filter(
(v) => !main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
);
if (removedServices.length > 0) {
newValues = newValues.filter((v) =>
main.ALLOWED_WITH_RUSSIA_INSIDE.includes(v),
);
notifications.push(
E('p', { class: 'alert-message warning' }, [
E('strong', {}, _('Russia inside restrictions')),
E('br'),
_(
'Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.',
).format(
main.ALLOWED_WITH_RUSSIA_INSIDE.map(
(key) => main.DOMAIN_LIST_OPTIONS[key],
)
.filter((label) => label !== 'Russia inside')
.join(', '),
removedServices.join(', '),
),
]),
);
}
}
if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) {
this.getUIElement(section_id).setValue(newValues);
}
notifications.forEach((notification) =>
ui.addNotification(null, notification),
);
lastValues = newValues;
} catch (e) {
console.error('Error in onchange handler:', e);
} finally {
isProcessing = false;
}
};
o = s.taboption(
'basic',
form.ListValue,
'user_domain_list_type',
_('User Domain List Type'),
_('Select how to add your custom domains'),
);
o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List'));
o.value('text', _('Text List'));
o.default = 'disabled';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'user_domains',
_('User Domains'),
_(
'Enter domain names without protocols (example: sub.example.com or example.com)',
),
);
o.placeholder = 'Domains list';
o.depends('user_domain_list_type', 'dynamic');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateDomain(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.TextValue,
'user_domains_text',
_('User Domains List'),
_(
'Enter domain names separated by comma, space or newline. You can add comments after //',
),
);
o.placeholder =
'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains';
o.depends('user_domain_list_type', 'text');
o.rows = 8;
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const domains = main.parseValueList(value);
if (!domains.length) {
return _(
'At least one valid domain must be specified. Comments-only content is not allowed.',
);
}
const { valid, results } = main.bulkValidate(domains, main.validateDomain);
if (!valid) {
const errors = results
.filter((validation) => !validation.valid) // Leave only failed validations
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
return [_('Validation errors:'), ...errors].join('\n');
}
return true;
};
o = s.taboption(
'basic',
form.Flag,
'local_domain_lists_enabled',
_('Local Domain Lists'),
_('Use the list from the router filesystem'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'local_domain_lists',
_('Local Domain List Paths'),
_('Enter the list file path'),
);
o.placeholder = '/path/file.lst';
o.depends('local_domain_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validatePath(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.Flag,
'remote_domain_lists_enabled',
_('Remote Domain Lists'),
_('Download and use domain lists from remote URLs'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'remote_domain_lists',
_('Remote Domain URLs'),
_('Enter full URLs starting with http:// or https://'),
);
o.placeholder = 'URL';
o.depends('remote_domain_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateUrl(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.Flag,
'local_subnet_lists_enabled',
_('Local Subnet Lists'),
_('Use the list from the router filesystem'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'local_subnet_lists',
_('Local Subnet List Paths'),
_('Enter the list file path'),
);
o.placeholder = '/path/file.lst';
o.depends('local_subnet_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validatePath(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.ListValue,
'user_subnet_list_type',
_('User Subnet List Type'),
_('Select how to add your custom subnets'),
);
o.value('disabled', _('Disabled'));
o.value('dynamic', _('Dynamic List'));
o.value('text', _('Text List (comma/space/newline separated)'));
o.default = 'disabled';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'user_subnets',
_('User Subnets'),
_(
'Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses',
),
);
o.placeholder = 'IP or subnet';
o.depends('user_subnet_list_type', 'dynamic');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateSubnet(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.TextValue,
'user_subnets_text',
_('User Subnets List'),
_(
'Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //',
),
);
o.placeholder =
'103.21.244.0/22\n// Google DNS\n8.8.8.8\n1.1.1.1/32, 9.9.9.9 // Cloudflare and Quad9';
o.depends('user_subnet_list_type', 'text');
o.rows = 10;
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const subnets = main.parseValueList(value);
if (!subnets.length) {
return _(
'At least one valid subnet or IP must be specified. Comments-only content is not allowed.',
);
}
const { valid, results } = main.bulkValidate(subnets, main.validateSubnet);
if (!valid) {
const errors = results
.filter((validation) => !validation.valid) // Leave only failed validations
.map((validation) => _(`${validation.value}: ${validation.message}`)); // Collect validation errors
return [_('Validation errors:'), ...errors].join('\n');
}
return true;
};
o = s.taboption(
'basic',
form.Flag,
'remote_subnet_lists_enabled',
_('Remote Subnet Lists'),
_('Download and use subnet lists from remote URLs'),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'remote_subnet_lists',
_('Remote Subnet URLs'),
_('Enter full URLs starting with http:// or https://'),
);
o.placeholder = 'URL';
o.depends('remote_subnet_lists_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateUrl(value);
if (validation.valid) {
return true;
}
return validation.message;
};
o = s.taboption(
'basic',
form.Flag,
'all_traffic_from_ip_enabled',
_('IP for full redirection'),
_(
'Specify local IP addresses whose traffic will always use the configured route',
),
);
o.default = '0';
o.rmempty = false;
o.ucisection = s.section;
o = s.taboption(
'basic',
form.DynamicList,
'all_traffic_ip',
_('Local IPs'),
_('Enter valid IPv4 addresses'),
);
o.placeholder = 'IP';
o.depends('all_traffic_from_ip_enabled', '1');
o.rmempty = false;
o.ucisection = s.section;
o.validate = function (section_id, value) {
// Optional
if (!value || value.length === 0) {
return true;
}
const validation = main.validateIPV4(value);
if (validation.valid) {
return true;
}
return validation.message;
};
}
return baseclass.extend({
createConfigSection,
});

View File

@@ -0,0 +1,26 @@
'use strict';
'require baseclass';
'require form';
'require ui';
'require uci';
'require fs';
'require view.podkop.utils as utils';
'require view.podkop.main as main';
function createDashboardSection(mainSection) {
let o = mainSection.tab('dashboard', _('Dashboard'));
o = mainSection.taboption('dashboard', form.DummyValue, '_status');
o.rawhtml = true;
o.cfgvalue = () => {
main.initDashboardController();
return main.renderDashboard();
};
}
const EntryPoint = {
createDashboardSection,
};
return baseclass.extend(EntryPoint);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
'use strict';
'require baseclass';
'require ui';
'require fs';
'require view.podkop.main as main';
// Flag to track if this is the first error check
let isInitialCheck = true;
// Set to track which errors we've already seen
const lastErrorsSet = new Set();
// Timer for periodic error polling
let errorPollTimer = null;
// Helper function to fetch errors from the podkop command
async function getPodkopErrors() {
return new Promise((resolve) => {
safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', (result) => {
if (!result || !result.stdout) return resolve([]);
const logs = result.stdout.split('\n');
const errors = logs.filter((log) => log.includes('[critical]'));
resolve(errors);
});
});
}
// Show error notification to the user
function showErrorNotification(error, isMultiple = false) {
const notificationContent = E('div', { class: 'alert-message error' }, [
E('pre', { class: 'error-log' }, error),
]);
ui.addNotification(null, notificationContent);
}
// Helper function for command execution with prioritization
function safeExec(
command,
args,
priority,
callback,
timeout = main.COMMAND_TIMEOUT,
) {
// Default to highest priority execution if priority is not provided or invalid
let schedulingDelay = main.COMMAND_SCHEDULING.P0_PRIORITY;
// If priority is a string, try to get the corresponding delay value
if (
typeof priority === 'string' &&
main.COMMAND_SCHEDULING[priority] !== undefined
) {
schedulingDelay = main.COMMAND_SCHEDULING[priority];
}
const executeCommand = async () => {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const result = await Promise.race([
fs.exec(command, args),
new Promise((_, reject) => {
controller.signal.addEventListener('abort', () => {
reject(new Error('Command execution timed out'));
});
}),
]);
clearTimeout(timeoutId);
if (callback && typeof callback === 'function') {
callback(result);
}
return result;
} catch (error) {
console.warn(
`Command execution failed or timed out: ${command} ${args.join(' ')}`,
);
const errorResult = { stdout: '', stderr: error.message, error: error };
if (callback && typeof callback === 'function') {
callback(errorResult);
}
return errorResult;
}
};
if (callback && typeof callback === 'function') {
setTimeout(executeCommand, schedulingDelay);
return;
} else {
return executeCommand();
}
}
// Check for critical errors and show notifications
async function checkForCriticalErrors() {
try {
const errors = await getPodkopErrors();
if (errors && errors.length > 0) {
// Filter out errors we've already seen
const newErrors = errors.filter((error) => !lastErrorsSet.has(error));
if (newErrors.length > 0) {
// On initial check, just store errors without showing notifications
if (!isInitialCheck) {
// Show each new error as a notification
newErrors.forEach((error) => {
showErrorNotification(error, newErrors.length > 1);
});
}
// Add new errors to our set of seen errors
newErrors.forEach((error) => lastErrorsSet.add(error));
}
}
// After first check, mark as no longer initial
isInitialCheck = false;
} catch (error) {
console.error('Error checking for critical messages:', error);
}
}
// Start polling for errors at regular intervals
function startErrorPolling() {
if (errorPollTimer) {
clearInterval(errorPollTimer);
}
// Reset initial check flag to make sure we show errors
isInitialCheck = false;
// Immediately check for errors on start
checkForCriticalErrors();
// Then set up periodic checks
errorPollTimer = setInterval(
checkForCriticalErrors,
main.ERROR_POLL_INTERVAL,
);
}
// Stop polling for errors
function stopErrorPolling() {
if (errorPollTimer) {
clearInterval(errorPollTimer);
errorPollTimer = null;
}
}
return baseclass.extend({
startErrorPolling,
stopErrorPolling,
checkForCriticalErrors,
safeExec,
});

View File

@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
PODIR="po"
POTFILE="$PODIR/templates/podkop.pot"
WIDTH=120
if [ $# -ne 1 ]; then
echo "Usage: $0 <language_code> (e.g., ru, de, fr)"
exit 1
fi
LANG="$1"
POFILE="$PODIR/$LANG/podkop.po"
if [ ! -f "$POTFILE" ]; then
echo "Template $POTFILE not found. Run xgettext first."
exit 1
fi
if [ -f "$POFILE" ]; then
echo "Updating $POFILE"
msgmerge --update --width="$WIDTH" --no-location "$POFILE" "$POTFILE"
else
echo "Creating new $POFILE using msginit"
mkdir -p "$PODIR/$LANG"
msginit --no-translator --no-location --locale="$LANG" --width="$WIDTH" --input="$POTFILE" --output-file="$POFILE"
fi
echo "Translation file for $LANG updated."

View File

@@ -1,35 +1,83 @@
# Russian translations for PODKOP package.
# Copyright (C) 2025 THE PODKOP'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PODKOP package.
# Automatically generated, 2025.
#
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "Podkop configuration"
msgstr "Настройка Podkop"
msgstr ""
"Project-Id-Version: PODKOP\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-07 16:55+0300\n"
"PO-Revision-Date: 2025-10-07 23:45+0300\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
msgid "Basic Settings"
msgstr "Основные настройки"
msgid "Additional Settings"
msgstr "Дополнительные настройки"
msgid "Secondary Config"
msgstr "Второй маршрут"
msgid "Secondary VPN/Proxy Enable"
msgstr "Включить второй VPN/Proxy"
msgid "Enable secondary VPN/Proxy configuration"
msgstr "Включить конфигурацию второго VPN/Proxy"
msgid "Connection Type"
msgstr "Тип подключения"
msgid "Select between VPN and Proxy connection methods for traffic routing"
msgstr "Выберите между VPN и Proxy методами для маршрутизации трафика"
msgid "Configuration Type"
msgstr "Тип конфигурации"
msgid "Select how to configure the proxy"
msgstr "Выберите способ настройки прокси"
msgid "Connection URL"
msgstr "URL подключения"
msgid "Outbound Config"
msgstr "Конфигурация Outbound"
msgid "URLTest"
msgstr "URLTest"
msgid "Proxy Configuration URL"
msgstr "URL конфигурации прокси"
msgid "Enter connection string starting with vless:// or ss:// for proxy configuration"
msgstr "Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси"
msgid "Current config: "
msgstr "Текущая конфигурация: "
msgid "Config without description"
msgstr "Конфигурация без описания"
msgid ""
"Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs"
msgstr ""
"Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для резервных конфигураций"
msgid "No active configuration found. One configuration is required."
msgstr "Активная конфигурация не найдена. Требуется хотя бы одна незакомментированная строка."
msgid "Multiply active configurations found. Please leave one configuration."
msgstr "Найдено несколько активных конфигураций. Оставьте только одну."
msgid "Invalid URL format:"
msgstr "Неверный формат URL:"
msgid "Outbound Configuration"
msgstr "Конфигурация исходящего соединения"
msgid "Enter complete outbound configuration in JSON format"
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
msgid "URLTest Proxy Links"
msgstr "Ссылки прокси для URLTest"
msgid "Shadowsocks UDP over TCP"
msgstr "Shadowsocks UDP через TCP"
msgid "Apply for SS2022"
msgstr "Применить для SS2022"
msgid "Network Interface"
msgstr "Сетевой интерфейс"
@@ -37,188 +85,53 @@ msgstr "Сетевой интерфейс"
msgid "Select network interface for VPN connection"
msgstr "Выберите сетевой интерфейс для VPN подключения"
msgid "Community Domain Lists"
msgstr "Предустановленные списки доменов"
msgid "Domain Resolver"
msgstr "Резолвер доменов"
msgid "Domain List"
msgstr "Список доменов"
msgid "Enable built-in DNS resolver for domains handled by this section"
msgstr "Включить встроенный DNS-резолвер для доменов, обрабатываемых в этом разделе"
msgid "Select a list"
msgstr "Выберите список доменов"
msgid "DNS Protocol Type"
msgstr "Тип протокола DNS"
msgid "Community Subnet Lists"
msgstr "Предустановленные сети сервисов"
msgid "Select the DNS protocol type for the domain resolver"
msgstr "Выберите тип протокола DNS для резолвера доменов"
msgid "Enable routing for popular services like Twitter, Meta, and Discord"
msgstr "Включить маршрутизацию для популярных сервисов, таких как Twitter, Meta и Discord"
msgid "DNS over HTTPS (DoH)"
msgstr "DNS через HTTPS (DoH)"
msgid "Service Networks"
msgstr "Сети сервисов"
msgid "DNS over TLS (DoT)"
msgstr "DNS через TLS (DoT)"
msgid "Select predefined service networks for routing"
msgstr "Выберите предустановленные сети сервисов для маршрутизации"
msgid "UDP (Unprotected DNS)"
msgstr "UDP (Незащищённый DNS)"
msgid "User Domain List"
msgstr "Пользовательский список доменов"
msgid "DNS Server"
msgstr "DNS-сервер"
msgid "Enable and manage your custom list of domains for selective routing"
msgstr "Включить и управлять пользовательским списком доменов для выборочной маршрутизации"
msgid "Select or enter DNS server address"
msgstr "Выберите или введите адрес DNS-сервера"
msgid "User Domains"
msgstr "Пользовательские домены"
msgid "Enter domain names without protocols (example: sub.example.com or example.com)"
msgstr "Введите имена доменов без протоколов (пример: sub.example.com или example.com)"
msgid "Remote Domain Lists"
msgstr "Удаленные списки доменов"
msgid "Download and use domain lists from remote URLs"
msgstr "Загрузка и использование списков доменов с удаленных URL"
msgid "Remote Domain URLs"
msgstr "URL удаленных доменов"
msgid "Enter full URLs starting with http:// or https://"
msgstr "Введите полные URL, начинающиеся с http:// или https://"
msgid "User Subnet List"
msgstr "Пользовательский список подсетей"
msgid "Enable and manage your custom list of IP subnets for selective routing"
msgstr "Включить и управлять пользовательским списком IP-подсетей для выборочной маршрутизации"
msgid "User Subnets"
msgstr "Пользовательские подсети"
msgid "Enter subnet in CIDR notation (example: 103.21.244.0/22)"
msgstr "Введите подсеть в нотации CIDR (пример: 103.21.244.0/22)"
msgid "Remote Subnet Lists"
msgstr "Удаленные списки подсетей"
msgid "Download and use subnet lists from remote URLs"
msgstr "Загрузка и использование списков подсетей с удаленных URL"
msgid "Remote Subnet URLs"
msgstr "URL удаленных подсетей"
msgid "Domain Exclusions"
msgstr "Исключения доменов"
msgid "Exclude specific domains from routing rules"
msgstr "Исключить определенные домены из правил маршрутизации"
msgid "Excluded Domains"
msgstr "Исключенные домены"
msgid "Domains to be excluded from routing"
msgstr "Домены, которые будут исключены из маршрутизации"
msgid "IP for full redirection"
msgstr "Принудительные прокси IP"
msgid "Specify local IP addresses whose traffic will always use the configured route"
msgstr "Укажите локальные IP-адреса, трафик которых всегда будет использовать настроенный маршрут"
msgid "Local IPs"
msgstr "Локальные IP"
msgid "Enter valid IPv4 addresses"
msgstr "Введите действительные IPv4 адреса"
msgid "IP for exclusion"
msgstr "Исключения прокси IP"
msgid "Specify local IP addresses that will never use the configured route"
msgstr "Укажите локальные IP-адреса, которые никогда не будут использовать настроенный маршрут"
msgid "List Update Frequency"
msgstr "Частота обновления списков"
msgid "Select how often the lists will be updated"
msgstr "Выберите, как часто будут обновляться списки"
msgid "Every hour"
msgstr "Каждый час"
msgid "Every 2 hours"
msgstr "Каждые 2 часа"
msgid "Every 4 hours"
msgstr "Каждые 4 часа"
msgid "Every 6 hours"
msgstr "Каждые 6 часов"
msgid "Every 12 hours"
msgstr "Каждые 12 часов"
msgid "Once a day at 04:00"
msgstr "Раз в день в 04:00"
msgid "Once a week on Sunday at 04:00"
msgstr "Раз в неделю в воскресенье в 04:00"
msgid "Yacd enable"
msgstr "Включить Yacd"
msgid "Mixed enable"
msgstr "Включить смешанный режим"
msgid "Browser port: 2080"
msgstr "Порт браузера: 2080"
msgid "Exclude NTP"
msgstr "Исключить NTP"
msgid "For issues with open connections sing-box"
msgstr "Для проблем с открытыми соединениями sing-box"
msgid "Service Domain List Enable"
msgstr "Включить список доменов сервисов"
msgid "Enable predefined service domain lists for routing"
msgstr "Включить предустановленные списки доменов для маршрутизации"
msgid "Community Lists"
msgstr "Списки сообщества"
msgid "Service List"
msgstr "Список сервисов"
msgid "Select predefined services for routing"
msgid "Select predefined service for routing"
msgstr "Выберите предустановленные сервисы для маршрутизации"
msgid "Domains"
msgstr "Домены"
msgid "Regional options cannot be used together"
msgstr "Нельзя использовать несколько региональных опций одновременно"
msgid "Subnet List"
msgstr "Список подсетей"
msgid "Warning: %s cannot be used together with %s. Previous selections have been removed."
msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены."
msgid "Configure custom subnets for routing"
msgstr "Настройка пользовательских подсетей для маршрутизации"
msgid "Russia inside restrictions"
msgstr "Ограничения Russia inside"
msgid "Subnets"
msgstr "Подсети"
msgid "Invalid domain format. Enter domain without protocol (example: sub.example.com)"
msgstr "Неверный формат домена. Введите домен без протокола (пример: sub.example.com)"
msgid "URL must use http:// or https:// protocol"
msgstr "URL должен использовать протокол http:// или https://"
msgid "Invalid URL format. URL must start with http:// or https://"
msgstr "Неверный формат URL. URL должен начинаться с http:// или https://"
msgid "Invalid subnet format. Use format: X.X.X.X/Y (like 192.168.1.0/24)"
msgstr "Неверный формат подсети. Используйте формат: X.X.X.X/Y (например: 192.168.1.0/24)"
msgid "IP address parts must be between 0 and 255"
msgstr "Части IP-адреса должны быть между 0 и 255"
msgid "CIDR must be between 0 and 32"
msgstr "CIDR должен быть между 0 и 32"
msgid "Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)"
msgstr "Неверный формат IP. Используйте формат: X.X.X.X (например: 192.168.1.1)"
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 "User Domain List Type"
msgstr "Тип пользовательского списка доменов"
@@ -235,14 +148,53 @@ msgstr "Динамический список"
msgid "Text List"
msgstr "Текстовый список"
msgid "User Domains"
msgstr "Пользовательские домены"
msgid "Enter domain names without protocols (example: sub.example.com or example.com)"
msgstr "Введите доменные имена без протоколов (например: sub.example.com или example.com)"
msgid "User Domains List"
msgstr "Список пользовательских доменов"
msgid "Enter domain names separated by comma, space or newline (example: sub.example.com, example.com or one domain per line)"
msgstr "Введите имена доменов через запятую, пробел или новую строку (пример: sub.example.com, example.com или один домен на строку)"
msgid "Enter domain names separated by comma, space or newline. You can add comments after //"
msgstr "Введите домены через запятую, пробел или с новой строки. Можно добавлять комментарии после //"
msgid "Invalid domain format: %s. Enter domain without protocol"
msgstr "Неверный формат домена: %s. Введите домен без протокола"
msgid "At least one valid domain must be specified. Comments-only content is not allowed."
msgstr "Необходимо указать хотя бы один действительный домен. Содержимое только из комментариев не допускается."
msgid "Validation errors:"
msgstr "Ошибки валидации:"
msgid "Local Domain Lists"
msgstr "Локальные списки доменов"
msgid "Use the list from the router filesystem"
msgstr "Использовать список из файловой системы роутера"
msgid "Local Domain List Paths"
msgstr "Пути к локальным спискам доменов"
msgid "Enter the list file path"
msgstr "Введите путь к файлу списка"
msgid "Remote Domain Lists"
msgstr "Удалённые списки доменов"
msgid "Download and use domain lists from remote URLs"
msgstr "Загружать и использовать списки доменов с удалённых URL"
msgid "Remote Domain URLs"
msgstr "URL удалённых доменов"
msgid "Enter full URLs starting with http:// or https://"
msgstr "Введите полные URL, начинающиеся с http:// или https://"
msgid "Local Subnet Lists"
msgstr "Локальные списки подсетей"
msgid "Local Subnet List Paths"
msgstr "Пути к локальным спискам подсетей"
msgid "User Subnet List Type"
msgstr "Тип пользовательского списка подсетей"
@@ -251,82 +203,376 @@ msgid "Select how to add your custom subnets"
msgstr "Выберите способ добавления пользовательских подсетей"
msgid "Text List (comma/space/newline separated)"
msgstr "Текстовый список (разделенный запятыми/пробелами/новыми строками)"
msgstr "Текстовый список (через запятую, пробел или новую строку)"
msgid "User Subnets"
msgstr "Пользовательские подсети"
msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses"
msgstr "Введите подсети в нотации CIDR (пример: 103.21.244.0/22) или отдельные IP-адреса"
msgstr "Введите подсети в нотации CIDR (например: 103.21.244.0/22) или отдельные IP-адреса"
msgid "User Subnets List"
msgstr "Список пользовательских подсетей"
msgid "Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline"
msgstr "Введите подсети в нотации CIDR или отдельные IP-адреса через запятую, пробел или новую строку"
msgid "Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline. You can add comments after //"
msgstr "Введите подсети в нотации CIDR или IP-адреса через запятую, пробел или новую строку. Можно добавлять комментарии после //"
msgid "Invalid format. Use format: X.X.X.X or X.X.X.X/Y"
msgstr "Неверный формат. Используйте формат: X.X.X.X или X.X.X.X/Y"
msgid "At least one valid subnet or IP must be specified. Comments-only content is not allowed."
msgstr "Необходимо указать хотя бы одну действительную подсеть или IP. Только комментарии недопустимы."
msgid "IP parts must be between 0 and 255 in: %s"
msgstr "Части IP-адреса должны быть между 0 и 255 в: %s"
msgid "Remote Subnet Lists"
msgstr "Удалённые списки подсетей"
msgid "Configuration Type"
msgstr "Тип конфигурации"
msgid "Download and use subnet lists from remote URLs"
msgstr "Загружать и использовать списки подсетей с удалённых URL"
msgid "Select how to configure the proxy"
msgstr "Выберите способ настройки прокси"
msgid "Remote Subnet URLs"
msgstr "URL удалённых подсетей"
msgid "Connection URL"
msgstr "URL подключения"
msgid "IP for full redirection"
msgstr "IP для полного перенаправления"
msgid "Outbound Config"
msgstr "Конфигурация Outbound"
msgid "Specify local IP addresses whose traffic will always use the configured route"
msgstr "Укажите локальные IP-адреса, трафик которых всегда будет использовать настроенный маршрут"
msgid "Outbound Configuration"
msgstr "Конфигурация исходящего соединения"
msgid "Local IPs"
msgstr "Локальные IP-адреса"
msgid "Enter complete outbound configuration in JSON format"
msgstr "Введите полную конфигурацию исходящего соединения в формате JSON"
msgid "Enter valid IPv4 addresses"
msgstr "Введите действительные IPv4-адреса"
msgid "JSON must contain at least type, server and server_port fields"
msgstr "JSON должен содержать как минимум поля type, server и server_port"
msgid "Extra configurations"
msgstr "Дополнительные конфигурации"
msgid "Add Section"
msgstr "Добавить раздел"
msgid "Dashboard"
msgstr "Дашборд"
msgid "Valid"
msgstr "Валидно"
msgid "Invalid IP address"
msgstr "Неверный IP-адрес"
msgid "Invalid domain address"
msgstr "Неверный домен"
msgid "DNS server address cannot be empty"
msgstr "Адрес DNS-сервера не может быть пустым"
msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH"
msgstr "Неверный формат DNS-сервера. Примеры: 8.8.8.8, dns.example.com или dns.example.com/nicedns для DoH"
msgid "URL must use one of the following protocols:"
msgstr "URL должен использовать один из следующих протоколов:"
msgid "Invalid URL format"
msgstr "Неверный формат URL"
msgid "Path cannot be empty"
msgstr "Путь не может быть пустым"
msgid "Invalid path format. Path must start with \"/\" and contain valid characters"
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 "IP address 0.0.0.0 is not allowed"
msgstr "IP-адрес 0.0.0.0 не допускается"
msgid "CIDR must be between 0 and 32"
msgstr "CIDR должен быть между 0 и 32"
msgid "Invalid Shadowsocks URL: must start with ss://"
msgstr "Неверный URL Shadowsocks: должен начинаться с ss://"
msgid "Invalid Shadowsocks URL: must not contain spaces"
msgstr "Неверный URL Shadowsocks: не должен содержать пробелов"
msgid "Invalid Shadowsocks URL: missing credentials"
msgstr "Неверный URL Shadowsocks: отсутствуют учетные данные"
msgid "Invalid Shadowsocks URL: decoded credentials must contain method:password"
msgstr "Неверный URL Shadowsocks: декодированные данные должны содержать method:password"
msgid "Invalid Shadowsocks URL: missing method and password separator \":\""
msgstr "Неверный URL Shadowsocks: отсутствует разделитель метода и пароля \":\""
msgid "Invalid Shadowsocks URL: missing server address"
msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера"
msgid "Invalid Shadowsocks URL: missing server"
msgstr "Неверный URL Shadowsocks: отсутствует сервер"
msgid "Invalid Shadowsocks URL: missing port"
msgstr "Неверный URL Shadowsocks: отсутствует порт"
msgid "Invalid port number. Must be between 1 and 65535"
msgstr "Неверный номер порта. Допустимо от 1 до 65535"
msgid "Invalid Shadowsocks URL: parsing failed"
msgstr "Неверный URL Shadowsocks: ошибка разбора"
msgid "Invalid VLESS URL: must not contain spaces"
msgstr "Неверный URL VLESS: не должен содержать пробелов"
msgid "Invalid VLESS URL: must start with vless://"
msgstr "Неверный URL VLESS: должен начинаться с vless://"
msgid "Invalid VLESS URL: missing UUID"
msgstr "Неверный URL VLESS: отсутствует UUID"
msgid "Invalid VLESS URL: missing server"
msgstr "Неверный URL VLESS: отсутствует сервер"
msgid "Invalid VLESS URL: missing port"
msgstr "Неверный URL VLESS: отсутствует порт"
msgid "Invalid VLESS URL: invalid port number. Must be between 1 and 65535"
msgstr "Неверный URL VLESS: недопустимый порт (165535)"
msgid "Invalid VLESS URL: missing query parameters"
msgstr "Неверный URL VLESS: отсутствуют параметры запроса"
msgid "Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws"
msgstr "Неверный URL VLESS: тип должен быть tcp, raw, udp, grpc, http или ws"
msgid "Invalid VLESS URL: security must be one of tls, reality, none"
msgstr "Неверный URL VLESS: параметр security должен быть tls, reality или none"
msgid "Invalid VLESS URL: missing pbk parameter for reality security"
msgstr "Неверный URL VLESS: отсутствует параметр pbk для security=reality"
msgid "Invalid VLESS URL: missing fp parameter for reality security"
msgstr "Неверный URL VLESS: отсутствует параметр fp для security=reality"
msgid "Invalid VLESS URL: parsing failed"
msgstr "Неверный URL VLESS: ошибка разбора"
msgid "Outbound JSON must contain at least \"type\", \"server\" and \"server_port\" fields"
msgstr "JSON должен содержать поля \"type\", \"server\" и \"server_port\""
msgid "Invalid JSON format"
msgstr "Неверный формат JSON"
msgid "Diagnostics"
msgstr "Диагностика"
msgid "Invalid Trojan URL: must start with trojan://"
msgstr "Неверный URL Trojan: должен начинаться с trojan://"
msgid "Main Check"
msgstr "Основная проверка"
msgid "Invalid Trojan URL: must not contain spaces"
msgstr "Неверный URL Trojan: не должен содержать пробелов"
msgid "Run a comprehensive diagnostic check of all components"
msgstr "Запустить комплексную диагностическую проверку всех компонентов"
msgid "Invalid Trojan URL: must contain username, hostname and port"
msgstr "Неверный URL Trojan: должен содержать имя пользователя, хост и порт"
msgid "Run Check"
msgstr "Запустить проверку"
msgid "Invalid Trojan URL: parsing failed"
msgstr "Неверный URL Trojan: ошибка разбора"
msgid "Full Diagnostic Results"
msgstr "Полные результаты диагностики"
msgid "URL must start with vless:// or ss:// or trojan://"
msgstr "URL должен начинаться с vless://, ss:// или trojan://"
msgid "Operation timed out"
msgstr "Время ожидания истекло"
msgid "HTTP error"
msgstr "Ошибка HTTP"
msgid "Unknown error"
msgstr "Неизвестная ошибка"
msgid "Fastest"
msgstr "Самый быстрый"
msgid "Dashboard currently unavailable"
msgstr "Дашборд сейчас недоступен"
msgid "Currently unavailable"
msgstr "Временно недоступно"
msgid "Traffic"
msgstr "Трафик"
msgid "Uplink"
msgstr "Исходящий"
msgid "Downlink"
msgstr "Входящий"
msgid "Traffic Total"
msgstr "Всего трафика"
msgid "System info"
msgstr "Системная информация"
msgid "Active Connections"
msgstr "Активные соединения"
msgid "Memory Usage"
msgstr "Использование памяти"
msgid "Services info"
msgstr "Информация о сервисах"
msgid "Podkop"
msgstr "Podkop"
msgid "✔ Enabled"
msgstr "✔ Включено"
msgid "✘ Disabled"
msgstr "✘ Отключено"
msgid "Sing-box"
msgstr "Sing-box"
msgid "✔ Running"
msgstr "✔ Работает"
msgid "✘ Stopped"
msgstr "✘ Остановлен"
msgid "Copied!"
msgstr "Скопировано!"
msgid "Failed to copy: "
msgstr "Ошибка копирования: "
msgstr "Не удалось скопировать: "
msgid "Loading..."
msgstr "Загрузка..."
msgid "Copy to Clipboard"
msgstr "Скопировать в буфер"
msgstr "Копировать в буфер"
msgid "Close"
msgstr "Закрыть"
msgid "No output"
msgstr "Нет данных"
msgstr "Нет вывода"
msgid "System Logs"
msgstr "Системные логи"
msgid "FakeIP is working in browser!"
msgstr "FakeIP работает в браузере!"
msgid "View recent system logs related to Podkop"
msgstr "Просмотр недавних системных логов, связанных с Podkop"
msgid "FakeIP is not working in browser"
msgstr "FakeIP не работает в браузере"
msgid "View Logs"
msgstr "Просмотр логов"
msgid "Check DNS server on current device (PC, phone)"
msgstr "Проверьте DNS-сервер на текущем устройстве (ПК, телефон)"
msgid "Failed to copy logs: "
msgstr "Ошибка копирования логов: "
msgid "Its must be router!"
msgstr "Это должен быть роутер!"
msgid "Proxy working correctly"
msgstr "Прокси работает корректно"
msgid "Direct IP: "
msgstr "Прямой IP: "
msgid "Proxy IP: "
msgstr "Прокси IP: "
msgid "Proxy is not working - same IP for both domains"
msgstr "Прокси не работает — одинаковый IP для обоих доменов"
msgid "IP: "
msgstr "IP: "
msgid "Proxy check failed"
msgstr "Проверка прокси не удалась"
msgid "Check failed: "
msgstr "Проверка не удалась: "
msgid "timeout"
msgstr "таймаут"
msgid "Error: "
msgstr "Ошибка: "
msgid "Podkop Status"
msgstr "Статус Podkop"
msgid "Global check"
msgstr "Глобальная проверка"
msgid "Click here for all the info"
msgstr "Нажмите для просмотра всей информации"
msgid "Update Lists"
msgstr "Обновить списки"
msgid "Lists Update Results"
msgstr "Результаты обновления списков"
msgid "Sing-box Status"
msgstr "Статус Sing-box"
msgid "Check NFT Rules"
msgstr "Проверить правила NFT"
msgid "NFT Rules"
msgstr "Правила NFT"
msgid "Check DNSMasq"
msgstr "Проверить DNSMasq"
msgid "DNSMasq Configuration"
msgstr "Конфигурация DNSMasq"
msgid "FakeIP Status"
msgstr "Статус FakeIP"
msgid "DNS Status"
msgstr "Статус DNS"
msgid "Main config"
msgstr "Основная конфигурация"
msgid "Version Information"
msgstr "Информация о версии"
msgid "Podkop: "
msgstr "Podkop: "
msgid "LuCI App: "
msgstr "LuCI App: "
msgid "Sing-box: "
msgstr "Sing-box: "
msgid "OpenWrt Version: "
msgstr "Версия OpenWrt: "
msgid "Device Model: "
msgstr "Модель устройства: "
msgid "Unknown"
msgstr "Неизвестно"
msgid "works in browser"
msgstr "работает в браузере"
msgid "does not work in browser"
msgstr "не работает в браузере"
msgid "works on router"
msgstr "работает на роутере"
msgid "does not work on router"
msgstr "не работает на роутере"
msgid "Config: "
msgstr "Конфигурация: "
msgid "Diagnostics"
msgstr "Диагностика"
msgid "Podkop"
msgstr "Podkop"
msgid "Extra configurations"
msgstr "Дополнительные конфигурации"
msgid "Add Section"
msgstr "Добавить раздел"

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,9 @@
"file": {
"/etc/init.d/podkop": [
"exec"
],
"/usr/bin/podkop": [
"exec"
]
},
"ubus": {

View File

@@ -0,0 +1,25 @@
#!/bin/bash
SRC_DIR="htdocs/luci-static/resources/view/podkop"
OUT_POT="po/templates/podkop.pot"
ENCODING="UTF-8"
WIDTH=120
mapfile -t FILES < <(find "$SRC_DIR" -type f -name "*.js")
if [ ${#FILES[@]} -eq 0 ]; then
echo "No JS files found in $SRC_DIR"
exit 1
fi
mkdir -p "$(dirname "$OUT_POT")"
echo "Generating POT template from JS files in $SRC_DIR"
xgettext --language=JavaScript \
--keyword=_ \
--from-code="$ENCODING" \
--output="$OUT_POT" \
--width="$WIDTH" \
--package-name="PODKOP" \
"${FILES[@]}"
echo "POT template generated: $OUT_POT"

Some files were not shown because too many files have changed in this diff Show More