Compare commits

...

216 Commits

Author SHA1 Message Date
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
33 changed files with 7317 additions and 5571 deletions

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,28 +10,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.1
with:
fetch-depth: 0
- name: Check version match
- name: Extract version
id: version
run: |
PODKOP_VERSION=$(grep '^PKG_VERSION:=' podkop/Makefile | cut -d '=' -f 2)
LUCI_APP_PODKOP_VERSION=$(grep '^PKG_VERSION:=' luci-app-podkop/Makefile | cut -d '=' -f 2)
TAG_VERSION=${GITHUB_REF#refs/tags/v}
echo "Podkop version: $PODKOP_VERSION"
echo "Luci-app-podkop version: $LUCI_APP_PODKOP_VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$PODKOP_VERSION" != "$TAG_VERSION" ] || [ "$LUCI_APP_PODKOP_VERSION" != "$TAG_VERSION" ]; then
echo "Error: Version mismatch"
exit 1
fi
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

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.idea

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/

View File

@@ -1,15 +1,16 @@
# Вещи, которые вам нужно знать перед установкой
- Это альфа версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
- Основной функционал работает, но побочные штуки сейчас могут сбоить.
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clearbrowsercache/).
- Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться.
- При возникновении проблем, нужен технически грамотный фидбэк в чат.
- При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clear-browser-cache/).
- Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться.
- Необходимо минимум 15МБ свободного места на роутере. Роутерами с флешками на 16МБ сразу мимо.
- Необходимо минимум 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.
# Документация
https://podkop.net/
@@ -17,25 +18,38 @@ https://podkop.net/
# Установка Podkop
Полная информация в [документации](https://podkop.net/docs/install/)
Вкратце, достаточно одного скрипта:
Вкратце, достаточно одного скрипта для установки и обновления:
```
sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh)
```
## Обновление
```
sh <(wget -qO- https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh) --upgrade
```
# ToDo
Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме.
Основные задачи в issues.
Низкий приоритет
- [ ] Галочка, которая режет доступ к doh серверам
- [ ] IPv6. Только после наполнения Wiki
## Рефактор
- [x] Очевидные повторения в `/usr/bin/podkop` загнать в переменые
- [x] Возможно поменять структуру
Рефактор
## Списки
- [x] CloudFront
- [x] DO
- [x] HODCA
## Будущее
- [ ] [Подписка](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.
## Тесты
- [ ] Unit тесты (BATS)
- [ ] Интеграционые тесты бекенда (OpenWrt rootfs + BATS)
[![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

@@ -1,66 +1,35 @@
#!/bin/sh
REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest"
IS_SHOULD_RESTART_NETWORK=
DOWNLOAD_DIR="/tmp/podkop"
COUNT=3
UPGRADE=0
rm -rf "$DOWNLOAD_DIR"
mkdir -p "$DOWNLOAD_DIR"
for arg in "$@"; do
if [ "$arg" = "--upgrade" ]; then
UPGRADE=1
fi
done
msg() {
printf "\033[32;1m%s\033[0m\n" "$1"
}
main() {
check_system
sing_box
opkg update
/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
opkg update || { echo "opkg update failed"; exit 1; }
if [ -f "/etc/init.d/podkop" ]; then
if [ "$UPGRADE" -eq 1 ]; then
echo "Upgraded podkop with flag..."
break
else
printf "\033[32;1mPodkop is already installed. Just upgrade it?\033[0m\n"
printf "\033[32;1my - Only upgrade podkop\033[0m\n"
printf "\033[32;1mn - Upgrade and install tunnels (WG, AWG, OpenVPN, OC)\033[0m\n"
while true; do
printf "\033[32;1mEnter (y/n): \033[0m"
read -r -p '' UPDATE
case $UPDATE in
y)
echo "Upgraded podkop..."
break
;;
n)
add_tunnel
break
;;
*)
echo "Please enter y or n"
;;
esac
done
fi
msg "Podkop is already installed. Upgraded..."
else
echo "Installed podkop..."
add_tunnel
msg "Installed podkop..."
fi
if command -v curl &> /dev/null; then
check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest")
if echo "$check_response" | grep -q 'API rate limit '; then
echo "You've reached rate limit from GitHub. Repeat in five minutes."
msg "You've reached rate limit from GitHub. Repeat in five minutes."
exit 1
fi
fi
@@ -72,33 +41,33 @@ main() {
attempt=0
while [ $attempt -lt $COUNT ]; do
echo "Download $filename (count $((attempt+1)))..."
msg "Download $filename (count $((attempt+1)))..."
if wget -q -O "$filepath" "$url"; then
if [ -s "$filepath" ]; then
echo "$filename successfully downloaded"
msg "$filename successfully downloaded"
download_success=1
break
fi
fi
echo "Download error $filename. Retry..."
msg "Download error $filename. Retry..."
rm -f "$filepath"
attempt=$((attempt+1))
done
if [ $attempt -eq $COUNT ]; then
echo "Failed to download $filename after $COUNT attempts"
msg "Failed to download $filename after $COUNT attempts"
fi
done < <(wget -qO- "$REPO" | grep -o 'https://[^"[:space:]]*\.ipk')
if [ $download_success -eq 0 ]; then
echo "No packages were downloaded successfully"
msg "No packages were downloaded successfully"
exit 1
fi
for pkg in podkop luci-app-podkop; do
file=$(ls "$DOWNLOAD_DIR" | grep "^$pkg" | head -n 1)
if [ -n "$file" ]; then
echo "Installing $file"
msg "Installing $file"
opkg install "$DOWNLOAD_DIR/$file"
sleep 3
fi
@@ -106,340 +75,66 @@ main() {
ru=$(ls "$DOWNLOAD_DIR" | grep "luci-i18n-podkop-ru" | head -n 1)
if [ -n "$ru" ]; then
printf "\033[32;1mРусский язык интерфейса ставим? y/n (Need a Russian translation?)\033[0m "
while true; do
read -r -p '' RUS
case $RUS in
y)
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"
break
;;
n)
break
;;
*)
echo "Введите y или n"
;;
esac
done
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
find "$DOWNLOAD_DIR" -type f -name '*podkop*' -exec rm {} \;
if [ "$IS_SHOULD_RESTART_NETWORK" ]; then
printf "\033[32;1mRestart network\033[0m\n"
/etc/init.d/network restart
fi
}
add_tunnel() {
printf "\033[32;1mWill you be using Wireguard, AmneziaWG, OpenVPN, OpenConnect? If yes, select a number and they will be automatically installed\033[0m\n"
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 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 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 "Installation without additional dependencies."
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
fi
fi
if opkg list-installed | grep -qE 'luci-app-amneziawg|luci-proto-amneziawg'; then
echo "luci-app-amneziawg or luci-proto-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"
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
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
}
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 /overlay | awk 'NR==2 {print $4}')
REQUIRED_SPACE=15360 # 15MB in KB
if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then
printf "\033[31;1mError: Insufficient space in flash\033[0m\n"
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
if ! nslookup google.com >/dev/null 2>&1; then
printf "\033[31;1mDNS not working\033[0m\n"
msg "DNS not working"
exit 1
fi
if opkg list-installed | grep -q https-dns-proxy; then
printf "\033[31;1mСonflicting package detected: https-dns-proxy. Remove? yes/no\033[0m\n"
msg "Сonflicting package detected: https-dns-proxy. Remove?"
while true; do
read -r -p '' DNSPROXY
@@ -450,16 +145,12 @@ check_system() {
break
;;
*)
echo "Exit"
msg "Exit"
exit 1
;;
esac
done
fi
if opkg list-installed | grep -q "iptables-mod-extra"; then
printf "\033[31;1mFound incompatible iptables packages. If you're using FriendlyWrt: https://t.me/itdogchat/44512/181082\033[0m\n"
fi
}
sing_box() {
@@ -468,10 +159,13 @@ sing_box() {
fi
sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}')
required_version="1.11.1"
required_version="1.12.4"
if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then
opkg remove sing-box
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
}

View File

@@ -1,7 +1,9 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-podkop
PKG_VERSION:=0.3.48
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,230 @@
'use strict';
'require form';
'require baseclass';
'require view.podkop.constants as constants';
'require tools.widgets as widgets';
function createAdditionalSection(mainSection, network) {
let o = mainSection.tab('additional', _('Additional Settings'));
o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), '<a href="http://openwrt.lan:9090/ui" target="_blank">openwrt.lan: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(constants.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(constants.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) {
if (!value) {
return _('DNS server address cannot be empty');
}
const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
const domainRegex = /^(?:https:\/\/)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}(:[0-9]{1,5})?(\/[^?#\s]*)?$/;
if (!ipRegex.test(value) && !domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
}
return true;
};
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'));
o.value('77.88.8.8', '77.88.8.8 (Yandex DNS)');
o.value('77.88.8.1', '77.88.8.1 (Yandex DNS)');
o.value('1.1.1.1', '1.1.1.1 (Cloudflare DNS)');
o.value('1.0.0.1', '1.0.0.1 (Cloudflare DNS)');
o.value('8.8.8.8', '8.8.8.8 (Google DNS)');
o.value('8.8.4.4', '8.8.4.4 (Google DNS)');
o.value('9.9.9.9', '9.9.9.9 (Quad9 DNS)');
o.value('9.9.9.11', '9.9.9.11 (Quad9 DNS)');
o.default = '77.88.8.8';
o.rmempty = false;
o.ucisection = 'main';
o.validate = function (section_id, value) {
if (!value) {
return _('DNS server address cannot be empty');
}
const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
if (!ipRegex.test(value)) {
return _('Invalid DNS server format. Example: 8.8.8.8');
}
return true;
};
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) {
if (['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan'].indexOf(value) !== -1) {
return false;
}
var device = this.devices.filter(function (dev) {
return dev.getName() === value;
})[0];
if (device) {
var type = device.getType();
return type !== 'wifi' && type !== 'wireless' && !type.includes('wlan');
}
return true;
};
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) {
return ['lan', 'loopback'].indexOf(value) === -1 && !value.startsWith('@');
};
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) {
if (!value || value.length === 0) return true;
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)');
const ipParts = value.split('.');
for (const part of ipParts) {
const num = parseInt(part);
if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
}
return true;
};
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,594 @@
'use strict';
'require baseclass';
'require form';
'require ui';
'require network';
'require view.podkop.constants as constants';
'require tools.widgets as widgets';
function validateUrl(url, protocols = ['http:', 'https:']) {
try {
const parsedUrl = new URL(url);
if (!protocols.includes(parsedUrl.protocol)) {
return _('URL must use one of the following protocols: ') + protocols.join(', ');
}
return true;
} catch (e) {
return _('Invalid URL format');
}
}
function createConfigSection(section, map, network) {
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;
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';
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) {
if (!value || value.length === 0) {
return true;
}
try {
const activeConfig = value.split('\n')
.map(line => line.trim())
.find(line => line && !line.startsWith('//'));
if (!activeConfig) {
return _('No active configuration found. At least one non-commented line is required.');
}
if (!activeConfig.startsWith('vless://') && !activeConfig.startsWith('ss://')) {
return _('URL must start with vless:// or ss://');
}
if (activeConfig.startsWith('ss://')) {
let encrypted_part;
try {
let mainPart = activeConfig.includes('?') ? activeConfig.split('?')[0] : activeConfig.split('#')[0];
encrypted_part = mainPart.split('/')[2].split('@')[0];
try {
let decoded = atob(encrypted_part);
if (!decoded.includes(':')) {
if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) {
return _('Invalid Shadowsocks URL format: missing method and password separator ":"');
}
}
} catch (e) {
if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) {
return _('Invalid Shadowsocks URL format: missing method and password separator ":"');
}
}
} catch (e) {
return _('Invalid Shadowsocks URL format');
}
try {
let serverPart = activeConfig.split('@')[1];
if (!serverPart) return _('Invalid Shadowsocks URL: missing server address');
let [server, portAndRest] = serverPart.split(':');
if (!server) return _('Invalid Shadowsocks URL: missing server');
let port = portAndRest ? portAndRest.split(/[?#]/)[0] : null;
if (!port) return _('Invalid Shadowsocks URL: missing port');
let portNum = parseInt(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return _('Invalid port number. Must be between 1 and 65535');
}
} catch (e) {
return _('Invalid Shadowsocks URL: missing or invalid server/port format');
}
}
if (activeConfig.startsWith('vless://')) {
let uuid = activeConfig.split('/')[2].split('@')[0];
if (!uuid || uuid.length === 0) return _('Invalid VLESS URL: missing UUID');
try {
let serverPart = activeConfig.split('@')[1];
if (!serverPart) return _('Invalid VLESS URL: missing server address');
let [server, portAndRest] = serverPart.split(':');
if (!server) return _('Invalid VLESS URL: missing server');
let port = portAndRest ? portAndRest.split(/[/?#]/)[0] : null;
if (!port) return _('Invalid VLESS URL: missing port');
let portNum = parseInt(port);
if (isNaN(portNum) || portNum < 1 || portNum > 65535) {
return _('Invalid port number. Must be between 1 and 65535');
}
} catch (e) {
return _('Invalid VLESS URL: missing or invalid server/port format');
}
let queryString = activeConfig.split('?')[1];
if (!queryString) return _('Invalid VLESS URL: missing query parameters');
let params = new URLSearchParams(queryString.split('#')[0]);
let type = params.get('type');
const validTypes = ['tcp', 'raw', 'udp', 'grpc', 'http', 'ws'];
if (!type || !validTypes.includes(type)) {
return _('Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws');
}
let security = params.get('security');
const validSecurities = ['tls', 'reality', 'none'];
if (!security || !validSecurities.includes(security)) {
return _('Invalid VLESS URL: security must be one of tls, reality, none');
}
if (security === 'reality') {
if (!params.get('pbk')) return _('Invalid VLESS URL: missing pbk parameter for reality security');
if (!params.get('fp')) return _('Invalid VLESS URL: missing fp parameter for reality security');
}
}
return true;
} catch (e) {
console.error('Validation error:', 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) {
if (!value || value.length === 0) return true;
try {
const parsed = JSON.parse(value);
if (!parsed.type || !parsed.server || !parsed.server_port) {
return _('JSON must contain at least type, server and server_port fields');
}
return true;
} catch (e) {
return _('Invalid JSON format');
}
};
o = s.taboption('basic', form.DynamicList, 'urltest_proxy_links', _('URLTest Proxy Links'));
o.depends('proxy_config_type', 'urltest');
o.placeholder = 'vless:// or ss:// link';
o.rmempty = false;
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) {
if (['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan', 'lan'].indexOf(value) !== -1) {
return false;
}
var device = this.devices.filter(function (dev) {
return dev.getName() === value;
})[0];
if (device) {
var type = device.getType();
return type !== 'wifi' && type !== 'wireless' && !type.includes('wlan');
}
return true;
};
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(constants.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) {
if (!value) {
return _('DNS server address cannot be empty');
}
const ipRegex = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(:[0-9]{1,5})?$/;
const domainRegex = /^(?:https:\/\/)?([a-zA-Z0-9]+(-[a-zA-Z0-9]+)*\.)+[a-zA-Z]{2,63}(:[0-9]{1,5})?(\/[^?#\s]*)?$/;
if (!ipRegex.test(value) && !domainRegex.test(value)) {
return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH');
}
return true;
};
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(constants.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 = constants.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 || !constants.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 => !constants.ALLOWED_WITH_RUSSIA_INSIDE.includes(v));
if (removedServices.length > 0) {
newValues = newValues.filter(v => constants.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(
constants.ALLOWED_WITH_RUSSIA_INSIDE.map(key => constants.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) {
if (!value || value.length === 0) return true;
const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
if (!domainRegex.test(value)) {
return _('Invalid domain format. Enter domain without protocol (example: sub.example.com or ru)');
}
return true;
};
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) {
if (!value || value.length === 0) return true;
const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/;
const lines = value.split(/\n/).map(line => line.trim());
let hasValidDomain = false;
for (const line of lines) {
// Skip empty lines
if (!line) continue;
// Extract domain part (before any //)
const domainPart = line.split('//')[0].trim();
// Skip if line is empty after removing comments
if (!domainPart) continue;
// Process each domain in the line (separated by comma or space)
const domains = domainPart.split(/[,\s]+/).map(d => d.trim()).filter(d => d.length > 0);
for (const domain of domains) {
if (!domainRegex.test(domain)) {
return _('Invalid domain format: %s. Enter domain without protocol').format(domain);
}
hasValidDomain = true;
}
}
if (!hasValidDomain) {
return _('At least one valid domain must be specified. Comments-only content is not allowed.');
}
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) {
if (!value || value.length === 0) return true;
const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/;
if (!pathRegex.test(value)) {
return _('Invalid path format. Path must start with "/" and contain valid characters');
}
return true;
};
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) {
if (!value || value.length === 0) return true;
return validateUrl(value);
};
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) {
if (!value || value.length === 0) return true;
const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/;
if (!pathRegex.test(value)) {
return _('Invalid path format. Path must start with "/" and contain valid characters');
}
return true;
};
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) {
if (!value || value.length === 0) return true;
const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
if (!subnetRegex.test(value)) return _('Invalid format. Use format: X.X.X.X or X.X.X.X/Y');
const [ip, cidr] = value.split('/');
if (ip === "0.0.0.0") {
return _('IP address 0.0.0.0 is not allowed');
}
const ipParts = ip.split('.');
for (const part of ipParts) {
const num = parseInt(part);
if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
}
if (cidr !== undefined) {
const cidrNum = parseInt(cidr);
if (cidrNum < 0 || cidrNum > 32) return _('CIDR must be between 0 and 32');
}
return true;
};
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) {
if (!value || value.length === 0) return true;
const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/;
const lines = value.split(/\n/).map(line => line.trim());
let hasValidSubnet = false;
for (const line of lines) {
// Skip empty lines
if (!line) continue;
// Extract subnet part (before any //)
const subnetPart = line.split('//')[0].trim();
// Skip if line is empty after removing comments
if (!subnetPart) continue;
// Process each subnet in the line (separated by comma or space)
const subnets = subnetPart.split(/[,\s]+/).map(s => s.trim()).filter(s => s.length > 0);
for (const subnet of subnets) {
if (!subnetRegex.test(subnet)) {
return _('Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y').format(subnet);
}
const [ip, cidr] = subnet.split('/');
const ipParts = ip.split('.');
for (const part of ipParts) {
const num = parseInt(part);
if (num < 0 || num > 255) {
return _('IP parts must be between 0 and 255 in: %s').format(subnet);
}
}
if (cidr !== undefined) {
const cidrNum = parseInt(cidr);
if (cidrNum < 0 || cidrNum > 32) {
return _('CIDR must be between 0 and 32 in: %s').format(subnet);
}
}
hasValidSubnet = true;
}
}
if (!hasValidSubnet) {
return _('At least one valid subnet or IP must be specified. Comments-only content is not allowed.');
}
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) {
if (!value || value.length === 0) return true;
return validateUrl(value);
};
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) {
if (!value || value.length === 0) return true;
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)');
const ipParts = value.split('.');
for (const part of ipParts) {
const num = parseInt(part);
if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255');
}
return true;
};
}
return baseclass.extend({
createConfigSection
});

View File

@@ -0,0 +1,113 @@
'use strict';
'require baseclass';
const STATUS_COLORS = {
SUCCESS: '#4caf50',
ERROR: '#f44336',
WARNING: '#ff9800'
};
const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi';
const IP_CHECK_DOMAIN = 'ip.podkop.fyi';
const REGIONAL_OPTIONS = ['russia_inside', 'russia_outside', 'ukraine_inside'];
const ALLOWED_WITH_RUSSIA_INSIDE = [
'russia_inside',
'meta',
'twitter',
'discord',
'telegram',
'cloudflare',
'google_ai',
'google_play',
'hetzner',
'ovh',
'hodca',
'digitalocean',
'cloudfront'
];
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'
};
const UPDATE_INTERVAL_OPTIONS = {
'1h': 'Every hour',
'3h': 'Every 3 hours',
'12h': 'Every 12 hours',
'1d': 'Every day',
'3d': 'Every 3 days'
};
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)'
};
const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds
const CACHE_TIMEOUT = DIAGNOSTICS_UPDATE_INTERVAL - 1000; // 9 seconds
const ERROR_POLL_INTERVAL = 10000; // 10 seconds
const COMMAND_TIMEOUT = 10000; // 10 seconds
const FETCH_TIMEOUT = 10000; // 10 seconds
const BUTTON_FEEDBACK_TIMEOUT = 1000; // 1 second
const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds
// Интервалы планирования команд в диагностике (в миллисекундах)
const COMMAND_SCHEDULING = {
P0_PRIORITY: 0, // Наивысший приоритет (без задержки)
P1_PRIORITY: 100, // Очень высокий приоритет
P2_PRIORITY: 300, // Высокий приоритет
P3_PRIORITY: 500, // Выше среднего
P4_PRIORITY: 700, // Стандартный приоритет
P5_PRIORITY: 900, // Ниже среднего
P6_PRIORITY: 1100, // Низкий приоритет
P7_PRIORITY: 1300, // Очень низкий приоритет
P8_PRIORITY: 1500, // Фоновое выполнение
P9_PRIORITY: 1700, // Выполнение в режиме простоя
P10_PRIORITY: 1900 // Наименьший приоритет
};
return baseclass.extend({
STATUS_COLORS,
FAKEIP_CHECK_DOMAIN,
IP_CHECK_DOMAIN,
REGIONAL_OPTIONS,
ALLOWED_WITH_RUSSIA_INSIDE,
DOMAIN_LIST_OPTIONS,
UPDATE_INTERVAL_OPTIONS,
DNS_SERVER_OPTIONS,
DIAGNOSTICS_UPDATE_INTERVAL,
ERROR_POLL_INTERVAL,
COMMAND_TIMEOUT,
FETCH_TIMEOUT,
BUTTON_FEEDBACK_TIMEOUT,
DIAGNOSTICS_INITIAL_DELAY,
COMMAND_SCHEDULING,
CACHE_TIMEOUT
});

View File

@@ -0,0 +1,887 @@
'use strict';
'require baseclass';
'require form';
'require ui';
'require uci';
'require fs';
'require view.podkop.constants as constants';
'require view.podkop.utils as utils';
// Cache system for network requests
const fetchCache = {};
// Helper function to fetch with cache
async function cachedFetch(url, options = {}) {
const cacheKey = url;
const currentTime = Date.now();
// If we have a valid cached response, return it
if (fetchCache[cacheKey] && currentTime - fetchCache[cacheKey].timestamp < constants.CACHE_TIMEOUT) {
console.log(`Using cached response for ${url}`);
return Promise.resolve(fetchCache[cacheKey].response.clone());
}
// Otherwise, make a new request
try {
const response = await fetch(url, options);
// Cache the response
fetchCache[cacheKey] = {
response: response.clone(),
timestamp: currentTime
};
return response;
} catch (error) {
throw error;
}
}
// Helper functions for command execution with prioritization - Using from utils.js now
function safeExec(command, args, priority, callback, timeout = constants.COMMAND_TIMEOUT) {
return utils.safeExec(command, args, priority, callback, timeout);
}
// Helper functions for handling checks
function runCheck(checkFunction, priority, callback) {
// Default to highest priority execution if priority is not provided or invalid
let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY;
// If priority is a string, try to get the corresponding delay value
if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) {
schedulingDelay = constants.COMMAND_SCHEDULING[priority];
}
const executeCheck = async () => {
try {
const result = await checkFunction();
if (callback && typeof callback === 'function') {
callback(result);
}
return result;
} catch (error) {
if (callback && typeof callback === 'function') {
callback({ error });
}
return { error };
}
};
if (callback && typeof callback === 'function') {
setTimeout(executeCheck, schedulingDelay);
return;
} else {
return executeCheck();
}
}
function runAsyncTask(taskFunction, priority) {
// Default to highest priority execution if priority is not provided or invalid
let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY;
// If priority is a string, try to get the corresponding delay value
if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) {
schedulingDelay = constants.COMMAND_SCHEDULING[priority];
}
setTimeout(async () => {
try {
await taskFunction();
} catch (error) {
console.error('Async task error:', error);
}
}, schedulingDelay);
}
// Helper Functions for UI and formatting
function createStatus(state, message, color) {
return {
state,
message: _(message),
color: constants.STATUS_COLORS[color]
};
}
function formatDiagnosticOutput(output) {
if (typeof output !== 'string') return '';
return output.trim()
.replace(/\x1b\[[0-9;]*m/g, '')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n');
}
function copyToClipboard(text, button) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
const originalText = button.textContent;
button.textContent = _('Copied!');
setTimeout(() => button.textContent = originalText, constants.BUTTON_FEEDBACK_TIMEOUT);
} catch (err) {
ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message));
}
document.body.removeChild(textarea);
}
// IP masking function
function maskIP(ip) {
if (!ip) return '';
const parts = ip.split('.');
if (parts.length !== 4) return ip;
return ['XX', 'XX', 'XX', parts[3]].join('.');
}
// Status Check Functions
async function checkFakeIP() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT);
try {
const response = await cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
const data = await response.json();
clearTimeout(timeoutId);
if (data.fakeip === true) {
return createStatus('working', 'working', 'SUCCESS');
} else {
return createStatus('not_working', 'not working', 'ERROR');
}
} catch (fetchError) {
clearTimeout(timeoutId);
const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error';
return createStatus('error', message, 'WARNING');
}
} catch (error) {
return createStatus('error', 'check error', 'WARNING');
}
}
async function checkFakeIPCLI() {
try {
return new Promise((resolve) => {
safeExec('nslookup', ['-timeout=2', constants.FAKEIP_CHECK_DOMAIN, '127.0.0.42'], 'P0_PRIORITY', result => {
if (result.stdout && result.stdout.includes('198.18')) {
resolve(createStatus('working', 'working on router', 'SUCCESS'));
} else {
resolve(createStatus('not_working', 'not working on router', 'ERROR'));
}
});
});
} catch (error) {
return createStatus('error', 'CLI check error', 'WARNING');
}
}
function checkDNSAvailability() {
return new Promise(async (resolve) => {
try {
safeExec('/usr/bin/podkop', ['check_dns_available'], 'P0_PRIORITY', dnsStatusResult => {
if (!dnsStatusResult || !dnsStatusResult.stdout) {
return resolve({
remote: createStatus('error', 'DNS check timeout', 'WARNING'),
local: createStatus('error', 'DNS check timeout', 'WARNING')
});
}
try {
const dnsStatus = JSON.parse(dnsStatusResult.stdout);
const remoteStatus = dnsStatus.is_available ?
createStatus('available', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, 'SUCCESS') :
createStatus('unavailable', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, 'ERROR');
const localStatus = dnsStatus.local_dns_working ?
createStatus('available', 'Router DNS working', 'SUCCESS') :
createStatus('unavailable', 'Router DNS not working', 'ERROR');
return resolve({
remote: remoteStatus,
local: localStatus
});
} catch (parseError) {
return resolve({
remote: createStatus('error', 'DNS check parse error', 'WARNING'),
local: createStatus('error', 'DNS check parse error', 'WARNING')
});
}
});
} catch (error) {
return resolve({
remote: createStatus('error', 'DNS check error', 'WARNING'),
local: createStatus('error', 'DNS check error', 'WARNING')
});
}
});
}
async function checkBypass() {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT);
try {
const response1 = await cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal });
const data1 = await response1.json();
const response2 = await cachedFetch(`https://${constants.IP_CHECK_DOMAIN}/check`, { signal: controller.signal });
const data2 = await response2.json();
clearTimeout(timeoutId);
if (data1.IP && data2.IP) {
if (data1.IP !== data2.IP) {
return createStatus('working', 'working', 'SUCCESS');
} else {
return createStatus('not_working', 'same IP for both domains', 'ERROR');
}
} else {
return createStatus('error', 'check error (no IP)', 'WARNING');
}
} catch (fetchError) {
clearTimeout(timeoutId);
const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error';
return createStatus('error', message, 'WARNING');
}
} catch (error) {
return createStatus('error', 'check error', 'WARNING');
}
}
// Modal Functions
function createModalContent(title, content) {
return [
E('div', {
'class': 'panel-body',
style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' +
'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' +
'line-height: 1.5; font-size: 14px;'
}, [
E('pre', { style: 'margin: 0;' }, content)
]),
E('div', {
'class': 'right',
style: 'margin-top: 1em;'
}, [
E('button', {
'class': 'btn',
'click': ev => copyToClipboard('```txt\n' + content + '\n```', ev.target)
}, _('Copy to Clipboard')),
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
];
}
function showConfigModal(command, title) {
// Create and show modal immediately with loading state
const modalContent = E('div', { 'class': 'panel-body' }, [
E('div', {
'class': 'panel-body',
style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' +
'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' +
'line-height: 1.5; font-size: 14px;'
}, [
E('pre', {
'id': 'modal-content-pre',
style: 'margin: 0;'
}, _('Loading...'))
]),
E('div', {
'class': 'right',
style: 'margin-top: 1em;'
}, [
E('button', {
'class': 'btn',
'id': 'copy-button',
'click': ev => copyToClipboard('```txt\n' + document.getElementById('modal-content-pre').innerText + '\n```', ev.target)
}, _('Copy to Clipboard')),
E('button', {
'class': 'btn',
'click': ui.hideModal
}, _('Close'))
])
]);
ui.showModal(_(title), modalContent);
// Function to update modal content
const updateModalContent = (content) => {
const pre = document.getElementById('modal-content-pre');
if (pre) {
pre.textContent = content;
}
};
try {
let formattedOutput = '';
if (command === 'global_check') {
safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => {
formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT);
cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal })
.then(response => response.json())
.then(data => {
clearTimeout(timeoutId);
if (data.fakeip === true) {
formattedOutput += '\n✅ ' + _('FakeIP is working in browser!') + '\n';
} else {
formattedOutput += '\n❌ ' + _('FakeIP is not working in browser') + '\n';
formattedOutput += _('Check DNS server on current device (PC, phone)') + '\n';
formattedOutput += _('Its must be router!') + '\n';
}
// Bypass check
cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal })
.then(bypassResponse => bypassResponse.json())
.then(bypassData => {
cachedFetch(`https://${constants.IP_CHECK_DOMAIN}/check`, { signal: controller.signal })
.then(bypassResponse2 => bypassResponse2.json())
.then(bypassData2 => {
formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
if (bypassData.IP && bypassData2.IP && bypassData.IP !== bypassData2.IP) {
formattedOutput += '✅ ' + _('Proxy working correctly') + '\n';
formattedOutput += _('Direct IP: ') + maskIP(bypassData.IP) + '\n';
formattedOutput += _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n';
} else if (bypassData.IP === bypassData2.IP) {
formattedOutput += '❌ ' + _('Proxy is not working - same IP for both domains') + '\n';
formattedOutput += _('IP: ') + maskIP(bypassData.IP) + '\n';
} else {
formattedOutput += '❌ ' + _('Proxy check failed') + '\n';
}
updateModalContent(formattedOutput);
})
.catch(error => {
formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
updateModalContent(formattedOutput);
});
})
.catch(error => {
formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
updateModalContent(formattedOutput);
});
})
.catch(error => {
formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n';
updateModalContent(formattedOutput);
});
} catch (error) {
formattedOutput += '\n❌ ' + _('Check failed: ') + error.message + '\n';
updateModalContent(formattedOutput);
}
});
} else {
safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => {
formattedOutput = formatDiagnosticOutput(res.stdout || _('No output'));
updateModalContent(formattedOutput);
});
}
} catch (error) {
updateModalContent(_('Error: ') + error.message);
}
}
// Button Factory
const ButtonFactory = {
createButton: function (config) {
return E('button', {
'class': `btn ${config.additionalClass || ''}`.trim(),
'click': config.onClick,
'style': config.style || ''
}, _(config.label));
},
createActionButton: function (config) {
return this.createButton({
label: config.label,
additionalClass: `cbi-button-${config.type || ''}`,
onClick: () => safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY')
.then(() => config.reload && location.reload()),
style: config.style
});
},
createInitActionButton: function (config) {
return this.createButton({
label: config.label,
additionalClass: `cbi-button-${config.type || ''}`,
onClick: () => safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY')
.then(() => config.reload && location.reload()),
style: config.style
});
},
createModalButton: function (config) {
return this.createButton({
label: config.label,
onClick: () => showConfigModal(config.command, config.title),
additionalClass: `cbi-button-${config.type || ''}`,
style: config.style
});
}
};
// Create a loading placeholder for status text
function createLoadingStatusText() {
return E('span', { 'class': 'loading-indicator' }, _('Loading...'));
}
// Create the status section with buttons loaded immediately but status indicators loading asynchronously
let createStatusSection = async function () {
// Get initial podkop status
let initialPodkopStatus = { enabled: false };
try {
const result = await fs.exec('/usr/bin/podkop', ['get_status']);
if (result && result.stdout) {
const status = JSON.parse(result.stdout);
initialPodkopStatus.enabled = status.enabled === 1;
}
} catch (e) {
console.error('Error getting initial podkop status:', e);
}
return E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [
// Podkop Status Panel
E('div', { 'id': 'podkop-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
E('div', { 'class': 'panel-heading' }, [
E('strong', {}, _('Podkop Status')),
E('br'),
E('span', { 'id': 'podkop-status-text' }, createLoadingStatusText())
]),
E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
ButtonFactory.createActionButton({
label: 'Restart Podkop',
type: 'apply',
action: 'restart',
reload: true
}),
ButtonFactory.createActionButton({
label: 'Stop Podkop',
type: 'apply',
action: 'stop',
reload: true
}),
// Autostart button - create with initial state
ButtonFactory.createInitActionButton({
label: initialPodkopStatus.enabled ? 'Disable Autostart' : 'Enable Autostart',
type: initialPodkopStatus.enabled ? 'remove' : 'apply',
action: initialPodkopStatus.enabled ? 'disable' : 'enable',
reload: true
}),
ButtonFactory.createModalButton({
label: _('Global check'),
command: 'global_check',
title: _('Click here for all the info')
}),
ButtonFactory.createModalButton({
label: 'View Logs',
command: 'check_logs',
title: 'Podkop Logs'
}),
ButtonFactory.createModalButton({
label: _('Update Lists'),
command: 'list_update',
title: _('Lists Update Results')
})
])
]),
// Sing-box Status Panel
E('div', { 'id': 'singbox-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
E('div', { 'class': 'panel-heading' }, [
E('strong', {}, _('Sing-box Status')),
E('br'),
E('span', { 'id': 'singbox-status-text' }, createLoadingStatusText())
]),
E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
ButtonFactory.createModalButton({
label: 'Show Config',
command: 'show_sing_box_config',
title: 'Sing-box Configuration'
}),
ButtonFactory.createModalButton({
label: 'View Logs',
command: 'check_sing_box_logs',
title: 'Sing-box Logs'
}),
ButtonFactory.createModalButton({
label: 'Check Connections',
command: 'check_sing_box_connections',
title: 'Active Connections'
}),
ButtonFactory.createModalButton({
label: _('Check NFT Rules'),
command: 'check_nft',
title: _('NFT Rules')
}),
ButtonFactory.createModalButton({
label: _('Check DNSMasq'),
command: 'check_dnsmasq',
title: _('DNSMasq Configuration')
})
])
]),
// FakeIP Status Panel
E('div', { 'id': 'fakeip-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
E('div', { 'class': 'panel-heading' }, [
E('strong', {}, _('FakeIP Status'))
]),
E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('span', { 'id': 'fakeip-browser-status' }, createLoadingStatusText())
]),
E('div', {}, [
E('span', { 'id': 'fakeip-router-status' }, createLoadingStatusText())
])
]),
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('strong', {}, _('DNS Status')),
E('br'),
E('span', { 'id': 'dns-remote-status' }, createLoadingStatusText()),
E('br'),
E('span', { 'id': 'dns-local-status' }, createLoadingStatusText())
])
]),
E('div', { style: 'margin-bottom: 5px;' }, [
E('div', {}, [
E('strong', { 'id': 'config-name-text' }, _('Main config')),
E('br'),
E('span', { 'id': 'bypass-status' }, createLoadingStatusText())
])
])
])
]),
// Version Information Panel
E('div', { 'id': 'version-info-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [
E('div', { 'class': 'panel-heading' }, [
E('strong', {}, _('Version Information'))
]),
E('div', { 'class': 'panel-body' }, [
E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [
E('strong', {}, _('Podkop: ')), E('span', { 'id': 'podkop-version' }, _('Loading...')), '\n',
E('strong', {}, _('LuCI App: ')), E('span', { 'id': 'luci-version' }, _('Loading...')), '\n',
E('strong', {}, _('Sing-box: ')), E('span', { 'id': 'singbox-version' }, _('Loading...')), '\n',
E('strong', {}, _('OpenWrt Version: ')), E('span', { 'id': 'openwrt-version' }, _('Loading...')), '\n',
E('strong', {}, _('Device Model: ')), E('span', { 'id': 'device-model' }, _('Loading...'))
])
])
])
])
]);
};
// Global variables for tracking state
let diagnosticsUpdateTimer = null;
let isInitialCheck = true;
showConfigModal.busy = false;
function startDiagnosticsUpdates() {
if (diagnosticsUpdateTimer) {
clearInterval(diagnosticsUpdateTimer);
}
// Immediately update when started
updateDiagnostics();
// Then set up periodic updates
diagnosticsUpdateTimer = setInterval(updateDiagnostics, constants.DIAGNOSTICS_UPDATE_INTERVAL);
}
function stopDiagnosticsUpdates() {
if (diagnosticsUpdateTimer) {
clearInterval(diagnosticsUpdateTimer);
diagnosticsUpdateTimer = null;
}
}
// Update individual text element with new content
function updateTextElement(elementId, content) {
const element = document.getElementById(elementId);
if (element) {
element.innerHTML = '';
element.appendChild(content);
}
}
async function updateDiagnostics() {
// Podkop Status check
safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', result => {
try {
const parsedPodkopStatus = JSON.parse(result.stdout || '{"enabled":0,"status":"error"}');
// Update Podkop status text
updateTextElement('podkop-status-text',
E('span', {
'style': `color: ${parsedPodkopStatus.enabled ? constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR}`
}, [
parsedPodkopStatus.enabled ? '✔ Autostart enabled' : '✘ Autostart disabled'
])
);
// Update autostart button
const autostartButton = parsedPodkopStatus.enabled ?
ButtonFactory.createInitActionButton({
label: 'Disable Autostart',
type: 'remove',
action: 'disable',
reload: true
}) :
ButtonFactory.createInitActionButton({
label: 'Enable Autostart',
type: 'apply',
action: 'enable',
reload: true
});
// Find the autostart button and replace it
const panel = document.getElementById('podkop-status-panel');
if (panel) {
const buttons = panel.querySelectorAll('.cbi-button');
if (buttons.length >= 3) {
buttons[2].parentNode.replaceChild(autostartButton, buttons[2]);
}
}
} catch (error) {
updateTextElement('podkop-status-text',
E('span', { 'style': `color: ${constants.STATUS_COLORS.ERROR}` }, '✘ Error')
);
}
});
// Sing-box Status check
safeExec('/usr/bin/podkop', ['get_sing_box_status'], 'P0_PRIORITY', result => {
try {
const parsedSingboxStatus = JSON.parse(result.stdout || '{"running":0,"enabled":0,"status":"error"}');
// Update Sing-box status text
updateTextElement('singbox-status-text',
E('span', {
'style': `color: ${parsedSingboxStatus.running && !parsedSingboxStatus.enabled ?
constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR}`
}, [
parsedSingboxStatus.running && !parsedSingboxStatus.enabled ?
'✔ running' : '✘ ' + parsedSingboxStatus.status
])
);
} catch (error) {
updateTextElement('singbox-status-text',
E('span', { 'style': `color: ${constants.STATUS_COLORS.ERROR}` }, '✘ Error')
);
}
});
// Version Information checks
safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', result => {
updateTextElement('podkop-version',
document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
);
});
safeExec('/usr/bin/podkop', ['show_luci_version'], 'P2_PRIORITY', result => {
updateTextElement('luci-version',
document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
);
});
safeExec('/usr/bin/podkop', ['show_sing_box_version'], 'P2_PRIORITY', result => {
updateTextElement('singbox-version',
document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown'))
);
});
safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', result => {
if (result.stdout) {
updateTextElement('openwrt-version',
document.createTextNode(result.stdout.split('\n')[1].trim())
);
updateTextElement('device-model',
document.createTextNode(result.stdout.split('\n')[4].trim())
);
} else {
updateTextElement('openwrt-version', document.createTextNode(_('Unknown')));
updateTextElement('device-model', document.createTextNode(_('Unknown')));
}
});
// FakeIP and DNS status checks
runCheck(checkFakeIP, 'P3_PRIORITY', result => {
updateTextElement('fakeip-browser-status',
E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [
result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
result.error ? 'check error' : result.state === 'working' ? _('works in browser') : _('does not work in browser')
])
);
});
runCheck(checkFakeIPCLI, 'P8_PRIORITY', result => {
updateTextElement('fakeip-router-status',
E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [
result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
result.error ? 'check error' : result.state === 'working' ? _('works on router') : _('does not work on router')
])
);
});
runCheck(checkDNSAvailability, 'P4_PRIORITY', result => {
if (result.error) {
updateTextElement('dns-remote-status',
E('span', { style: `color: ${constants.STATUS_COLORS.WARNING}` }, '! DNS check error')
);
updateTextElement('dns-local-status',
E('span', { style: `color: ${constants.STATUS_COLORS.WARNING}` }, '! DNS check error')
);
} else {
updateTextElement('dns-remote-status',
E('span', { style: `color: ${result.remote.color}` }, [
result.remote.state === 'available' ? '✔ ' : result.remote.state === 'unavailable' ? '✘ ' : '! ',
result.remote.message
])
);
updateTextElement('dns-local-status',
E('span', { style: `color: ${result.local.color}` }, [
result.local.state === 'available' ? '✔ ' : result.local.state === 'unavailable' ? '✘ ' : '! ',
result.local.message
])
);
}
});
runCheck(checkBypass, 'P1_PRIORITY', result => {
updateTextElement('bypass-status',
E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [
result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ',
result.error ? 'check error' : result.message
])
);
}, 'P1_PRIORITY');
// Config name
runAsyncTask(async () => {
try {
let configName = _('Main config');
const data = await uci.load('podkop');
const proxyString = uci.get('podkop', 'main', 'proxy_string');
if (proxyString) {
const activeConfig = proxyString.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()) {
configName = _('Config: ') + decodeURIComponent(label);
}
}
}
}
updateTextElement('config-name-text', document.createTextNode(configName));
} catch (e) {
console.error('Error getting config name from UCI:', e);
}
}, 'P1_PRIORITY');
}
function createDiagnosticsSection(mainSection) {
let o = mainSection.tab('diagnostics', _('Diagnostics'));
o = mainSection.taboption('diagnostics', form.DummyValue, '_status');
o.rawhtml = true;
o.cfgvalue = () => E('div', {
id: 'diagnostics-status',
'data-loading': 'true'
});
}
function setupDiagnosticsEventHandlers(node) {
const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop'));
node.insertBefore(titleDiv, node.firstChild);
// Function to initialize diagnostics
function initDiagnostics(container) {
if (container && container.hasAttribute('data-loading')) {
container.innerHTML = '';
showConfigModal.busy = false;
createStatusSection().then(section => {
container.appendChild(section);
startDiagnosticsUpdates();
// Start error polling when diagnostics tab is active
utils.startErrorPolling();
});
}
}
document.addEventListener('visibilitychange', function () {
const diagnosticsContainer = document.getElementById('diagnostics-status');
const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]');
if (document.hidden || !diagnosticsTab || !diagnosticsTab.classList.contains('cbi-tab-active')) {
stopDiagnosticsUpdates();
// Don't stop error polling here - it's managed in podkop.js for all tabs
} else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) {
startDiagnosticsUpdates();
// Ensure error polling is running when diagnostics tab is active
utils.startErrorPolling();
}
});
setTimeout(() => {
const diagnosticsContainer = document.getElementById('diagnostics-status');
const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]');
const otherTabs = document.querySelectorAll('.cbi-tab:not([data-tab="diagnostics"])');
// Check for direct page load case
const noActiveTabsExist = !Array.from(otherTabs).some(tab => tab.classList.contains('cbi-tab-active'));
if (diagnosticsContainer && diagnosticsTab && (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist)) {
initDiagnostics(diagnosticsContainer);
}
const tabs = node.querySelectorAll('.cbi-tabmenu');
if (tabs.length > 0) {
tabs[0].addEventListener('click', function (e) {
const tab = e.target.closest('.cbi-tab');
if (tab) {
const tabName = tab.getAttribute('data-tab');
if (tabName === 'diagnostics') {
const container = document.getElementById('diagnostics-status');
container.setAttribute('data-loading', 'true');
initDiagnostics(container);
} else {
stopDiagnosticsUpdates();
// Don't stop error polling - it should continue on all tabs
}
}
});
}
}, constants.DIAGNOSTICS_INITIAL_DELAY);
node.classList.add('fade-in');
return node;
}
return baseclass.extend({
createDiagnosticsSection,
setupDiagnosticsEventHandlers
});

View File

@@ -0,0 +1,152 @@
'use strict';
'require baseclass';
'require ui';
'require fs';
'require view.podkop.constants as constants';
// 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 = constants.COMMAND_TIMEOUT) {
// Default to highest priority execution if priority is not provided or invalid
let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY;
// If priority is a string, try to get the corresponding delay value
if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) {
schedulingDelay = constants.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, constants.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."

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,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"

View File

@@ -1,7 +1,9 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=podkop
PKG_VERSION:=0.3.48
PKG_VERSION := $(if $(PKG_VERSION),$(PKG_VERSION),dev_$(shell date +%d%m%Y))
PKG_RELEASE:=1
PKG_MAINTAINER:=ITDog <podkop@itdog.info>
@@ -13,7 +15,7 @@ define Package/podkop
SECTION:=net
CATEGORY:=Network
DEPENDS:=+sing-box +curl +jq +kmod-nft-tproxy +coreutils-base64
CONFLICTS:=https-dns-proxy
CONFLICTS:=https-dns-proxy nextdns luci-app-passwall luci-app-passwall2
TITLE:=Domain routing app
URL:=https://podkop.net
PKGARCH:=all
@@ -53,6 +55,9 @@ define Package/podkop/install
$(INSTALL_DIR) $(1)/usr/bin
$(INSTALL_BIN) ./files/usr/bin/podkop $(1)/usr/bin/podkop
$(INSTALL_DIR) $(1)/usr/lib/podkop
$(CP) ./files/usr/lib/* $(1)/usr/lib/podkop/
endef
$(eval $(call BuildPackage,podkop))

View File

@@ -4,25 +4,24 @@ config main 'main'
option proxy_config_type 'url'
#option outbound_json ''
option proxy_string ''
option domain_list_enabled '1'
list domain_list 'russia_inside'
option subnets_list_enabled '0'
option custom_domains_list_type 'disabled'
#list custom_domains ''
#option custom_domains_text ''
option custom_local_domains_list_enabled '0'
#list custom_local_domains ''
option custom_download_domains_list_enabled '0'
#list custom_download_domains ''
option custom_domains_list_type 'disable'
#list custom_subnets ''
#custom_subnets_text ''
option custom_download_subnets_list_enabled '0'
#list custom_download_subnets ''
option community_lists_enabled '1'
list community_lists 'russia_inside'
option user_domain_list_type 'disabled'
#list user_domains ''
#option user_domains_text ''
option local_domain_lists_enabled '0'
#list local_domain_lists ''
option remote_domain_lists_enabled '0'
#list remote_domain_lists ''
option user_subnet_list_type 'disable'
#list user_subnets ''
#option user_subnets_text ''
option local_subnet_lists_enabled '0'
#list local_subnet_lists ''
option remote_subnet_lists_enabled '0'
#list remote_subnet_lists ''
option all_traffic_from_ip_enabled '0'
#list all_traffic_ip ''
option delist_domains_enabled '0'
#list delist_domains ''
option exclude_from_ip_enabled '0'
#list exclude_traffic_ip ''
option yacd '0'
@@ -31,12 +30,18 @@ config main 'main'
option quic_disable '0'
option dont_touch_dhcp '0'
option update_interval '1d'
option dns_type 'doh'
option dns_type 'udp'
option dns_server '8.8.8.8'
option split_dns_enabled '1'
option split_dns_type 'udp'
option split_dns_server '1.1.1.1'
option dns_rewrite_ttl '60'
option cache_file '/tmp/cache.db'
option config_path '/etc/sing-box/config.json'
option cache_path '/tmp/sing-box/cache.db'
list iface 'br-lan'
option mon_restart_ifaces '0'
#list restart_ifaces 'wan'
option procd_reload_delay '2000'
option ss_uot '0'
option detour '0'
option detour '0'
option shutdown_correctly '1'

View File

@@ -34,13 +34,16 @@ service_triggers() {
config_get mon_restart_ifaces "main" "mon_restart_ifaces"
config_get restart_ifaces "main" "restart_ifaces"
config_get procd_reload_delay "main" "procd_reload_delay" "2000"
PROCD_RELOAD_DELAY=$procd_reload_delay
procd_open_trigger
procd_add_config_trigger "config.change" "$NAME" "$initscript" restart 'on_config_change'
if [ "$mon_restart_ifaces" = "1" ]; then
for iface in $restart_ifaces; do
procd_add_reload_interface_trigger $iface
procd_add_interface_trigger "interface.*.up" "$iface" /etc/init.d/podkop reload
done
fi
procd_close_trigger

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
# shellcheck disable=SC2034
## Common
PODKOP_CONFIG="/etc/config/podkop"
RESOLV_CONF="/etc/resolv.conf"
DNS_RESOLVERS="1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 9.9.9.9 9.9.9.11 94.140.14.14 94.140.15.15 208.67.220.220 208.67.222.222 77.88.8.1 77.88.8.8"
CHECK_PROXY_IP_DOMAIN="ip.podkop.fyi"
FAKEIP_TEST_DOMAIN="fakeip.podkop.fyi"
TMP_SING_BOX_FOLDER="/tmp/sing-box"
TMP_RULESET_FOLDER="$TMP_SING_BOX_FOLDER/rulesets"
CLOUDFLARE_OCTETS="8.47 162.159 188.114" # Endpoints https://github.com/ampetelin/warp-endpoint-checker
JQ_REQUIRED_VERSION="1.7.1"
COREUTILS_BASE64_REQUIRED_VERSION="9.7"
## nft
NFT_TABLE_NAME="PodkopTable"
NFT_LOCALV4_SET_NAME="localv4"
NFT_COMMON_SET_NAME="podkop_subnets"
NFT_DISCORD_SET_NAME="podkop_discord_subnets"
NFT_INTERFACE_SET_NAME="interfaces"
## sing-box
SB_REQUIRED_VERSION="1.12.0"
# Log
SB_DEFAULT_LOG_LEVEL="warn"
# DNS
SB_DNS_SERVER_TAG="dns-server"
SB_FAKEIP_DNS_SERVER_TAG="fakeip-server"
SB_FAKEIP_INET4_RANGE="198.18.0.0/15"
SB_BOOTSTRAP_SERVER_TAG="bootstrap-dns-server"
SB_FAKEIP_DNS_RULE_TAG="fakeip-dns-rule-tag"
SB_INVERT_FAKEIP_DNS_RULE_TAG="invert-fakeip-dns-rule-tag"
# Inbounds
SB_TPROXY_INBOUND_TAG="tproxy-in"
SB_TPROXY_INBOUND_ADDRESS="127.0.0.1"
SB_TPROXY_INBOUND_PORT=1602
SB_DNS_INBOUND_TAG="dns-in"
SB_DNS_INBOUND_ADDRESS="127.0.0.42"
SB_DNS_INBOUND_PORT=53
SB_MIXED_INBOUND_TAG="mixed-in"
SB_MIXED_INBOUND_ADDRESS="0.0.0.0" # TODO(ampetelin): maybe to determine address?
SB_MIXED_INBOUND_PORT=2080
SB_SERVICE_MIXED_INBOUND_TAG="service-mixed-in"
SB_SERVICE_MIXED_INBOUND_ADDRESS="127.0.0.1"
SB_SERVICE_MIXED_INBOUND_PORT=4534
# Outbounds
SB_DIRECT_OUTBOUND_TAG="direct-out"
SB_MAIN_OUTBOUND_TAG="main-out"
# Route
SB_REJECT_RULE_TAG="reject-rule-tag"
## Lists
GITHUB_RAW_URL="https://raw.githubusercontent.com/itdoginfo/allow-domains/main"
SRS_MAIN_URL="https://github.com/itdoginfo/allow-domains/releases/latest/download"
DOMAINS_RU_INSIDE="${GITHUB_RAW_URL}/Russia/inside-dnsmasq-nfset.lst"
DOMAINS_RU_OUTSIDE="${GITHUB_RAW_URL}/Russia/outside-dnsmasq-nfset.lst"
DOMAINS_UA="${GITHUB_RAW_URL}/Ukraine/inside-dnsmasq-nfset.lst"
DOMAINS_YOUTUBE="${GITHUB_RAW_URL}/Services/youtube.lst"
SUBNETS_TWITTER="${GITHUB_RAW_URL}/Subnets/IPv4/twitter.lst"
SUBNETS_META="${GITHUB_RAW_URL}/Subnets/IPv4/meta.lst"
SUBNETS_DISCORD="${GITHUB_RAW_URL}/Subnets/IPv4/discord.lst"
SUBNETS_TELERAM="${GITHUB_RAW_URL}/Subnets/IPv4/telegram.lst"
SUBNETS_CLOUDFLARE="${GITHUB_RAW_URL}/Subnets/IPv4/cloudflare.lst"
SUBNETS_HETZNER="${GITHUB_RAW_URL}/Subnets/IPv4/hetzner.lst"
SUBNETS_OVH="${GITHUB_RAW_URL}/Subnets/IPv4/ovh.lst"
SUBNETS_DIGITALOCEAN="${GITHUB_RAW_URL}/Subnets/IPv4/digitalocean.lst"
SUBNETS_CLOUDFRONT="${GITHUB_RAW_URL}/Subnets/IPv4/cloudfront.lst"
COMMUNITY_SERVICES="russia_inside russia_outside ukraine_inside geoblock block porn news anime youtube hdrezka tiktok google_ai google_play hodca discord meta twitter cloudflare cloudfront digitalocean hetzner ovh telegram"

View File

@@ -0,0 +1,14 @@
def extend_key_value(current_value; new_value):
if (current_value | type) == "array" then
if (new_value | type) == "array" then
current_value + new_value
else
current_value + [new_value]
end
else
if (new_value | type) == "array" then
[current_value] + new_value
else
[current_value, new_value]
end
end;

View File

@@ -0,0 +1,402 @@
# Check if string is valid IPv4
is_ipv4() {
local ip="$1"
local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$"
[[ "$ip" =~ $regex ]]
}
# Check if string is valid IPv4 with CIDR mask
is_ipv4_cidr() {
local ip="$1"
local regex="^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}(\/(3[0-2]|2[0-9]|1[0-9]|[0-9]))$"
[[ "$ip" =~ $regex ]]
}
is_ipv4_ip_or_ipv4_cidr() {
is_ipv4 "$1" || is_ipv4_cidr "$1"
}
is_domain() {
local str="$1"
local regex='^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$'
[[ "$str" =~ $regex ]]
}
is_domain_suffix() {
local str="$1"
local normalized="${str#.}"
is_domain "$normalized"
}
# Checks if the given string is a valid base64-encoded sequence
is_base64() {
local str="$1"
if echo "$str" | base64 -d > /dev/null 2>&1; then
return 0
fi
return 1
}
# Checks if the given string looks like a Shadowsocks userinfo
is_shadowsocks_userinfo_format() {
local str="$1"
local regex='^[^:]+:[^:]+(:[^:]+)?$'
[[ "$str" =~ $regex ]]
}
# Compares the current package version with the required minimum
is_min_package_version() {
local current="$1"
local required="$2"
local lowest
lowest="$(printf '%s\n' "$current" "$required" | sort -V | head -n1)"
[ "$lowest" = "$required" ]
}
# Checks if the given file exists
file_exists() {
local filepath="$1"
if [[ -f "$filepath" ]]; then
return 0
else
return 1
fi
}
# Checks if a service script exists in /etc/init.d
service_exists() {
local service="$1"
if [ -x "/etc/init.d/$service" ]; then
return 0
else
return 1
fi
}
# Returns the inbound tag name by appending the postfix to the given section
get_inbound_tag_by_section() {
local section="$1"
local postfix="in"
echo "$section-$postfix"
}
# Returns the outbound tag name by appending the postfix to the given section
get_outbound_tag_by_section() {
local section="$1"
local postfix="out"
echo "$section-$postfix"
}
# Constructs and returns a domain resolver tag by appending a fixed postfix to the given section
get_domain_resolver_tag() {
local section="$1"
local postfix="domain-resolver"
echo "$section-$postfix"
}
# Constructs and returns a ruleset tag using section, name, optional type, and a fixed postfix
get_ruleset_tag() {
local section="$1"
local name="$2"
local type="$3"
local postfix="ruleset"
if [ -n "$type" ]; then
echo "$section-$name-$type-$postfix"
else
echo "$section-$name-$postfix"
fi
}
# Determines the ruleset format based on the file extension (json → source, srs → binary)
get_ruleset_format_by_file_extension() {
local file_extension="$1"
local format
case "$file_extension" in
json) format="source" ;;
srs) format="binary" ;;
*)
log "Unsupported file extension: .$file_extension"
return 1
;;
esac
echo "$format"
}
# Converts a comma-separated string into a JSON array string
comma_string_to_json_array() {
local input="$1"
if [ -z "$input" ]; then
echo "[]"
return
fi
local replaced="${input//,/\",\"}"
echo "[\"$replaced\"]"
}
# Decodes a URL-encoded string
url_decode() {
local encoded="$1"
printf '%b' "$(echo "$encoded" | sed 's/+/ /g; s/%/\\x/g')"
}
# Extracts the userinfo (username[:password]) part from a URL
url_get_userinfo() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e '/@/!d' -e 's/@.*//p'
}
# Extracts the host part from a URL
url_get_host() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#\([:/].*\|$\)##p'
}
# Extracts the port number from a URL
url_get_port() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*@##' -e 's#^[^/]*:\([0-9][0-9]*\).*#\1#p'
}
# Extracts the path from a URL (without query or fragment; returns "/" if empty)
url_get_path() {
local url="$1"
echo "$url" | sed -n -e 's#^[^:/?]*://##' -e 's#^[^/]*##' -e 's#\([^?]*\).*#\1#p'
}
# Extracts the value of a specific query parameter from a URL
url_get_query_param() {
local url="$1"
local param="$2"
local raw
raw=$(echo "$url" | sed -n "s/.*[?&]$param=\([^&?#]*\).*/\1/p")
[ -z "$raw" ] && echo "" && return
echo "$raw"
}
# Extracts the basename (filename without extension) from a URL
url_get_basename() {
local url="$1"
local filename="${url##*/}"
local basename="${filename%%.*}"
echo "$basename"
}
# Extracts and returns the file extension from the given URL
url_get_file_extension() {
local url="$1"
local basename="${url##*/}"
case "$basename" in
*.*) echo "${basename##*.}" ;;
*) echo "" ;;
esac
}
# Remove url fragment (everything after the first '#')
url_strip_fragment() {
local url="$1"
echo "${url%%#*}"
}
# Decodes and returns a base64-encoded string
base64_decode() {
local str="$1"
local decoded_url
decoded_url="$(echo "$str" | base64 -d 2> /dev/null)"
echo "$decoded_url"
}
# Generates a unique 16-character ID based on the current timestamp and a random number
gen_id() {
printf '%s%s' "$(date +%s)" "$RANDOM" | md5sum | cut -c1-16
}
# Adds a missing UCI option with the given value if it does not exist
migration_add_new_option() {
local package="$1"
local section="$2"
local option="$3"
local value="$4"
local current
current="$(uci -q get "$package.$section.$option")"
if [ -z "$current" ]; then
log "Adding missing option '$option' with value '$value'"
uci set "$package.$section.$option=$value"
uci commit "$package"
return 0
else
return 1
fi
}
# Migrates a configuration key in an OpenWrt config file from old_key_name to new_key_name
migration_rename_config_key() {
local config="$1"
local key_type="$2"
local old_key_name="$3"
local new_key_name="$4"
if grep -q "$key_type $old_key_name" "$config"; then
log "Deprecated $key_type found: $old_key_name migrating to $new_key_name"
sed -i "s/$key_type $old_key_name/$key_type $new_key_name/g" "$config"
fi
}
# Download URL content directly
download_to_stream() {
local url="$1"
local http_proxy_address="$2"
local retries="${3:-3}"
local wait="${4:-2}"
for attempt in $(seq 1 "$retries"); do
if [ -n "$http_proxy_address" ]; then
http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -qO- "$url" | sed 's/\r$//' && break
else
wget -qO- "$url" | sed 's/\r$//' && break
fi
log "Attempt $attempt/$retries to download $url failed" "warn"
sleep "$wait"
done
}
# Download URL to file
download_to_file() {
local url="$1"
local filepath="$2"
local http_proxy_address="$3"
local retries="${4:-3}"
local wait="${5:-2}"
for attempt in $(seq 1 "$retries"); do
if [ -n "$http_proxy_address" ]; then
http_proxy="http://$http_proxy_address" https_proxy="http://$http_proxy_address" wget -O "$filepath" "$url" && break
else
wget -O "$filepath" "$url" && break
fi
log "Attempt $attempt/$retries to download $url failed" "warn"
sleep "$wait"
done
if grep -q $'\r' "$filepath"; then
log "Downloaded file has Windows line endings (CRLF). Converting to Unix (LF)"
sed -i 's/\r$//' "$filepath"
fi
}
# Decompiles a sing-box SRS binary file into a JSON ruleset file
decompile_srs_file() {
local binary_filepath="$1"
local output_filepath="$2"
log "Decompiling $binary_filepath to $output_filepath" "debug"
if ! file_exists "$binary_filepath"; then
log "File $binary_filepath not found" "error"
return 1
fi
sing-box rule-set decompile "$binary_filepath" -o "$output_filepath"
if [[ $? -ne 0 ]]; then
log "Decompilation command failed for $binary_filepath" "error"
return 1
fi
}
#######################################
# Parses a whitespace-separated string, validates items as either domains
# or IPv4 addresses/subnets, and returns a comma-separated string of valid items.
# Arguments:
# $1 - Input string (space-separated list of items)
# $2 - Type of validation ("domains" or "subnets")
# Outputs:
# Comma-separated string of valid domains or subnets
#######################################
parse_domain_or_subnet_string_to_commas_string() {
local string="$1"
local type="$2"
tmpfile=$(mktemp)
printf "%s\n" "$string" | sed 's/\/\/.*//' | tr ', ' '\n' | grep -v '^$' > "$tmpfile"
result="$(parse_domain_or_subnet_file_to_comma_string "$tmpfile" "$type")"
rm -f "$tmpfile"
echo "$result"
}
#######################################
# Parses a file line by line, validates entries as either domains or subnets,
# and returns a single comma-separated string of valid items.
# Arguments:
# $1 - Path to the input file
# $2 - Type of validation ("domains" or "subnets")
# Outputs:
# Comma-separated string of valid domains or subnets
#######################################
parse_domain_or_subnet_file_to_comma_string() {
local filepath="$1"
local type="$2"
local result
while IFS= read -r line; do
line=$(echo "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
[ -z "$line" ] && continue
case "$type" in
domains)
if ! is_domain_suffix "$line"; then
log "'$line' is not a valid domain" "debug"
continue
fi
;;
subnets)
if ! is_ipv4 "$line" && ! is_ipv4_cidr "$line"; then
log "'$line' is not IPv4 or IPv4 CIDR" "debug"
continue
fi
;;
*)
log "Unknown type: $type" "error"
return 1
;;
esac
if [ -z "$result" ]; then
result="$line"
else
result="$result,$line"
fi
done < "$filepath"
echo "$result"
}

View File

@@ -0,0 +1,30 @@
COLOR_CYAN="\033[0;36m"
COLOR_GREEN="\033[0;32m"
COLOR_RESET="\033[0m"
log() {
local message="$1"
local level="$2"
if [ "$level" == "" ]; then
level="info"
fi
logger -t "podkop" "[$level] $message"
}
nolog() {
local message="$1"
local timestamp
timestamp=$(date +"%Y-%m-%d %H:%M:%S")
echo -e "${COLOR_CYAN}[$timestamp]${COLOR_RESET} ${COLOR_GREEN}$message${COLOR_RESET}"
}
echolog() {
local message="$1"
local level="$2"
log "$message" "$level"
nolog "$message"
}

View File

@@ -0,0 +1,30 @@
# Create an nftables table in the inet family
nft_create_table() {
local name="$1"
nft add table inet "$name"
}
# Create a set within a table for storing IPv4 addresses
nft_create_ipv4_set() {
local table="$1"
local name="$2"
nft add set inet "$table" "$name" '{ type ipv4_addr; flags interval; auto-merge; }'
}
nft_create_ifname_set() {
local table="$1"
local name="$2"
nft add set inet "$table" "$name" '{ type ifname; flags interval; }'
}
# Add one or more elements to a set
nft_add_set_elements() {
local table="$1"
local set="$2"
local elements="$3"
nft add element inet "$table" "$set" "{ $elements }"
}

View File

@@ -0,0 +1,264 @@
PODKOP_LIB="/usr/lib/podkop"
. "$PODKOP_LIB/helpers.sh"
. "$PODKOP_LIB/sing_box_config_manager.sh"
sing_box_cf_add_dns_server() {
local config="$1"
local type="$2"
local tag="$3"
local server="$4"
local domain_resolver="$5"
local detour="$6"
local server_address server_port
server_address=$(url_get_host "$server")
server_port=$(url_get_port "$server")
case "$type" in
udp)
[ -z "$server_port" ] && server_port=53
config=$(sing_box_cm_add_udp_dns_server "$config" "$tag" "$server_address" "$server_port" "$domain_resolver" \
"$detour")
;;
dot)
[ -z "$server_port" ] && server_port=853
config=$(sing_box_cm_add_tls_dns_server "$config" "$tag" "$server_address" "$server_port" "$domain_resolver" \
"$detour")
;;
doh)
[ -z "$server_port" ] && server_port=443
local path headers
path=$(url_get_path "$server")
headers="" # TODO(ampetelin): implement it if necessary
config=$(sing_box_cm_add_https_dns_server "$config" "$tag" "$server_address" "$server_port" "$path" "$headers" \
"$domain_resolver" "$detour")
;;
*)
log "Unsupported DNS server type: $type"
exit 1
;;
esac
echo "$config"
}
sing_box_cf_add_mixed_inbound_and_route_rule() {
local config="$1"
local tag="$2"
local listen_address="$3"
local listen_port="$4"
local outbound="$5"
config=$(sing_box_cm_add_mixed_inbound "$config" "$tag" "$listen_address" "$listen_port")
config=$(sing_box_cm_add_route_rule "$config" "" "$tag" "$outbound")
echo "$config"
}
sing_box_cf_add_proxy_outbound() {
local config="$1"
local section="$2"
local url="$3"
local udp_over_tcp="$4"
url=$(url_decode "$url")
url=$(url_strip_fragment "$url")
local scheme="${url%%://*}"
case "$scheme" in
vless)
local tag host port uuid flow packet_encoding
tag=$(get_outbound_tag_by_section "$section")
host=$(url_get_host "$url")
port=$(url_get_port "$url")
uuid=$(url_get_userinfo "$url")
flow=$(url_get_query_param "$url" "flow")
packet_encoding=$(url_get_query_param "$url" "packetEncoding")
config=$(sing_box_cm_add_vless_outbound "$config" "$tag" "$host" "$port" "$uuid" "$flow" "" "$packet_encoding")
config=$(_add_outbound_security "$config" "$tag" "$url")
config=$(_add_outbound_transport "$config" "$tag" "$url")
;;
ss)
local userinfo tag host port method password udp_over_tcp
userinfo=$(url_get_userinfo "$url")
if ! is_shadowsocks_userinfo_format "$userinfo"; then
userinfo=$(base64_decode "$userinfo")
if [ $? -ne 0 ]; then
log "Cannot decode shadowsocks userinfo or it does not match the expected format. Aborted." "fatal"
exit 1
fi
fi
tag=$(get_outbound_tag_by_section "$section")
host=$(url_get_host "$url")
port=$(url_get_port "$url")
method="${userinfo%%:*}"
password="${userinfo#*:}"
config=$(
sing_box_cm_add_shadowsocks_outbound \
"$config" \
"$tag" \
"$host" \
"$port" \
"$method" \
"$password" \
"" \
"$([ "$udp_over_tcp" == "1" ] && echo 2)" # if udp_over_tcp is enabled, enable version 2
)
;;
trojan)
local tag host port password
tag=$(get_outbound_tag_by_section "$section")
host=$(url_get_host "$url")
port=$(url_get_port "$url")
password=$(url_get_userinfo "$url")
config=$(sing_box_cm_add_trojan_outbound "$config" "$tag" "$host" "$port" "$password")
config=$(_add_outbound_security "$config" "$tag" "$url")
config=$(_add_outbound_transport "$config" "$tag" "$url")
;;
*)
log "Unsupported proxy $scheme type"
exit 1
;;
esac
echo "$config"
}
_add_outbound_security() {
local config="$1"
local outbound_tag="$2"
local url="$3"
local security
security=$(url_get_query_param "$url" "security")
case "$security" in
tls | reality)
local sni insecure alpn fingerprint public_key short_id
sni=$(url_get_query_param "$url" "sni")
insecure=$(url_get_query_param "$url" "allowInsecure")
alpn=$(comma_string_to_json_array "$(url_get_query_param "$url" "alpn")")
fingerprint=$(url_get_query_param "$url" "fp")
public_key=$(url_get_query_param "$url" "pbk")
short_id=$(url_get_query_param "$url" "sid")
config=$(
sing_box_cm_set_tls_for_outbound \
"$config" \
"$outbound_tag" \
"$sni" \
"$([ "$insecure" == "1" ] && echo true)" \
"$([ "$alpn" == "[]" ] && echo null || echo "$alpn")" \
"$fingerprint" \
"$public_key" \
"$short_id"
)
;;
none) ;;
*)
log "Unknown security '$security' detected." "error"
;;
esac
echo "$config"
}
_add_outbound_transport() {
local config="$1"
local outbound_tag="$2"
local url="$3"
local transport
transport=$(url_get_query_param "$url" "type")
case "$transport" in
tcp | raw) ;;
ws)
local ws_path ws_host ws_early_data
ws_path=$(url_get_query_param "$url" "path")
ws_host=$(url_get_query_param "$url" "host")
ws_early_data=$(url_get_query_param "$url" "ed")
config=$(
sing_box_cm_set_ws_transport_for_outbound "$config" "$outbound_tag" "$ws_path" "$ws_host" "$ws_early_data"
)
;;
grpc)
# TODO(ampetelin): Add handling of optional gRPC parameters; example links are needed.
config=$(sing_box_cm_set_grpc_transport_for_outbound "$config" "$outbound_tag")
;;
*)
log "Unknown transport '$transport' detected." "error"
;;
esac
echo "$config"
}
sing_box_cf_add_json_outbound() {
local config="$1"
local section="$2"
local json_outbound="$3"
local tag
tag=$(get_outbound_tag_by_section "$section")
config=$(sing_box_cm_add_raw_outbound "$config" "$tag" "$json_outbound")
echo "$config"
}
sing_box_cf_add_interface_outbound() {
local config="$1"
local section="$2"
local interface_name="$3"
local tag
tag=$(get_outbound_tag_by_section "$section")
config=$(sing_box_cm_add_interface_outbound "$config" "$tag" "$interface_name")
echo "$config"
}
sing_box_cf_proxy_domain() {
local config="$1"
local inbound="$2"
local domain="$3"
local outbound="$4"
tag="$(gen_id)"
config=$(sing_box_cm_add_route_rule "$config" "$tag" "$inbound" "$outbound")
config=$(sing_box_cm_patch_route_rule "$config" "$tag" "domain" "$domain")
echo "$config"
}
sing_box_cf_override_domain_port() {
local config="$1"
local domain="$2"
local port="$3"
tag="$(gen_id)"
config=$(sing_box_cm_add_options_route_rule "$config" "$tag")
config=$(sing_box_cm_patch_route_rule "$config" "$tag" "domain" "$domain")
config=$(sing_box_cm_patch_route_rule "$config" "$tag" "override_port" "$port")
echo "$config"
}
sing_box_cf_add_single_key_reject_rule() {
local config="$1"
local inbound="$2"
local key="$3"
local value="$4"
tag="$(gen_id)"
config=$(sing_box_cm_add_reject_route_rule "$config" "$tag" "$inbound")
config=$(sing_box_cm_patch_route_rule "$config" "$tag" "$key" "$value")
echo "$config"
}

File diff suppressed because it is too large Load Diff