├── .github ├── CODEOWNERS └── workflows │ └── build.yml ├── Dockerfile ├── Dockerfile-SDK ├── LICENSE ├── README.md ├── String-example.md ├── install.sh ├── luci-app-podkop ├── Makefile ├── htdocs │ └── luci-static │ │ └── resources │ │ └── view │ │ └── podkop │ │ ├── additionalTab.js │ │ ├── configSection.js │ │ ├── constants.js │ │ ├── diagnosticTab.js │ │ ├── podkop.js │ │ └── utils.js ├── po │ ├── ru │ │ └── podkop.po │ └── templates │ │ └── podkop.pot └── root │ ├── etc │ └── uci-defaults │ │ └── 50_luci-podkop │ └── usr │ └── share │ ├── luci │ └── menu.d │ │ └── luci-app-podkop.json │ └── rpcd │ └── acl.d │ └── luci-app-podkop.json └── podkop ├── Makefile └── files ├── etc ├── config │ └── podkop └── init.d │ └── podkop └── usr └── bin └── podkop /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @itdoginfo -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build packages 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | build: 9 | name: Build podkop and luci-app-podkop 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4.2.1 13 | 14 | - name: Check version match 15 | run: | 16 | PODKOP_VERSION=$(grep '^PKG_VERSION:=' podkop/Makefile | cut -d '=' -f 2) 17 | LUCI_APP_PODKOP_VERSION=$(grep '^PKG_VERSION:=' luci-app-podkop/Makefile | cut -d '=' -f 2) 18 | 19 | TAG_VERSION=${GITHUB_REF#refs/tags/v} 20 | 21 | echo "Podkop version: $PODKOP_VERSION" 22 | echo "Luci-app-podkop version: $LUCI_APP_PODKOP_VERSION" 23 | echo "Tag version: $TAG_VERSION" 24 | 25 | if [ "$PODKOP_VERSION" != "$TAG_VERSION" ] || [ "$LUCI_APP_PODKOP_VERSION" != "$TAG_VERSION" ]; then 26 | echo "Error: Version mismatch" 27 | exit 1 28 | fi 29 | 30 | - name: Build and push 31 | uses: docker/build-push-action@v6.9.0 32 | with: 33 | context: . 34 | tags: podkop:ci 35 | 36 | - name: Create Docker container 37 | run: docker create --name podkop podkop:ci 38 | 39 | - name: Copy file from Docker container 40 | run: | 41 | docker cp podkop:/builder/bin/packages/x86_64/utilites/. ./bin/ 42 | docker cp podkop:/builder/bin/packages/x86_64/luci/. ./bin/ 43 | 44 | - name: Filter IPK files 45 | run: | 46 | # Извлекаем версию из тега, убирая префикс 'v' 47 | VERSION=${GITHUB_REF#refs/tags/v} 48 | 49 | mkdir -p ./filtered-bin 50 | cp ./bin/luci-i18n-podkop-ru_*.ipk "./filtered-bin/luci-i18n-podkop-ru_${VERSION}.ipk" 51 | cp ./bin/podkop_*.ipk ./filtered-bin/ 52 | cp ./bin/luci-app-podkop_*.ipk ./filtered-bin/ 53 | 54 | - name: Remove Docker container 55 | run: docker rm podkop 56 | 57 | - name: Release 58 | uses: softprops/action-gh-release@v2.0.8 59 | with: 60 | files: ./filtered-bin/*.ipk -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM itdoginfo/openwrt-sdk:24.10.1 2 | 3 | COPY ./podkop /builder/package/feeds/utilites/podkop 4 | COPY ./luci-app-podkop /builder/package/feeds/luci/luci-app-podkop 5 | 6 | RUN make defconfig && make package/podkop/compile && make package/luci-app-podkop/compile V=s -j4 -------------------------------------------------------------------------------- /Dockerfile-SDK: -------------------------------------------------------------------------------- 1 | FROM openwrt/sdk:x86_64-v24.10.1 2 | 3 | RUN ./scripts/feeds update -a && ./scripts/feeds install luci-base && mkdir -p /builder/package/feeds/utilites/ && mkdir -p /builder/package/feeds/luci/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Вещи, которые вам нужно знать перед установкой 2 | 3 | - Это бета-версия, которая находится в активной разработке. Из версии в версию что-то может меняться. 4 | - При возникновении проблем, нужен технически грамотный фидбэк в чат. 5 | - При обновлении **обязательно** [сбрасывайте кэш LuCI](https://podkop.net/docs/clearbrowsercache/). 6 | - Также при обновлении всегда заходите в конфигурацию и проверяйте свои настройки. Конфигурация может измениться. 7 | - Необходимо минимум 15МБ свободного места на роутере. Роутеры с флешками на 16МБ сразу мимо. 8 | - При старте программы редактируется конфиг Dnsmasq. 9 | - Podkop редактирует конфиг sing-box. Обязательно сохраните ваш конфиг sing-box перед установкой, если он вам нужен. 10 | - Информация здесь может быть устаревшей. Все изменения фиксируются в [телеграм-чате](https://t.me/itdogchat/81758/420321). 11 | - [Если у вас не что-то не работает.](https://podkop.net/docs/diagnostics/) 12 | - Если у вас установлен 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). 13 | 14 | # Документация 15 | https://podkop.net/ 16 | 17 | # Установка Podkop 18 | Полная информация в [документации](https://podkop.net/docs/install/) 19 | 20 | Вкратце, достаточно одного скрипта для установки и обновления: 21 | ``` 22 | sh <(wget -O - https://raw.githubusercontent.com/itdoginfo/podkop/refs/heads/main/install.sh) 23 | ``` 24 | 25 | # ToDo 26 | Этот раздел не означает задачи, которые нужно брать и делать. Это общий список хотелок. Если вы хотите помочь, пожалуйста, спросите сначала в телеграмме. 27 | 28 | Основные задачи в issues. 29 | 30 | ## Рефактор 31 | - [ ] Очевидные повторения в `/usr/bin/podkop` загнать в переменые 32 | - [ ] Возможно поменять структуру 33 | 34 | ## Списки 35 | - [ ] Speedtest 36 | - [x] Google AI 37 | - [x] Google PlayMarket. Здесь уточнить, что точно не работает через корректную настройку FakeIP, а не dnsmasq+nft. 38 | - [x] Hetzner ASN (AS24940) 39 | - [x] OVH ASN (AS16276) 40 | 41 | ## Будущее 42 | - [ ] После наполнения вики про туннели, убрать всё что связано с их установкой из скрипта. Только с AWG что-то решить, лучше чтоб был скрипт в сторонем репозитории. 43 | - [ ] Подписка. Здесь нужна реализация, чтоб для каждой секции помимо ручного выбора, был выбор фильтрации по тегу. Например, для main выбираем ключевые слова NL, DE, FI. А для extra секции фильтруем по RU. И создаётся outbound c urltest в которых перечислены outbound из фильтров. 44 | - [ ] Опция, когда все запросы (с роутера в первую очередь), а не только br-lan идут в прокси. С этим связана #95. Требуется много переделать для nftables. 45 | - [ ] Весь трафик в Proxy\VPN. Вопрос, что делать с экстрасекциями в этом случае. FakeIP здесь скорее не нужен, а значит только main секция остаётся. Всё что касается fakeip проверок, придётся выключать в этом режиме. 46 | - [ ] Поддержка Source format. Нужна расшифровка в json и если присуствуют подсети, заносить их в custom subnet nftset. 47 | - [ ] Переделывание функции формирования кастомных списков в JSON. Обрабатывать сразу скопом, а не по одному. 48 | - [ ] При успешном запуске переходит в фоновый режим и следит за состоянием sing-box. Если вдруг идёт exit 1, выполняется dnsmasq restore и снова следит за состоянием. Вопрос в том, как это искусcтвенно провернуть. Попробовать положить прокси и посмотреть, останется ли работать DNS в этом случае. И здесь, вероятно, можно обойтись триггером в init.d. 49 | - [ ] Галочка, которая режет доступ к doh серверам. 50 | - [ ] IPv6. Только после наполнения Wiki. 51 | 52 | ## Тесты 53 | - [ ] Unit тесты (BATS) 54 | - [ ] Интеграционые тесты бекенда (OpenWrt rootfs + BATS) -------------------------------------------------------------------------------- /String-example.md: -------------------------------------------------------------------------------- 1 | # Shadowsocks 2 | Тут всё просто 3 | 4 | ## Shadowsocks-old 5 | ``` 6 | ss://YWVzLTI1Ni1nY206RmJwUDJnSStPczJKK1kzdkVhTnVuOUZ2ZjJZYUhNUlN1L1BBdEVqMks1VT0@example.com:80?type=tcp#example-ss-old 7 | ``` 8 | 9 | ## Shadowsocks-2022 10 | ``` 11 | ss://2022-blake3-aes-128-gcm:5NgF%2B9eM8h4OnrTbHp%2B8UA%3D%3D%3Am8tbs5aKLYG7dN9f3xsiKA%3D%3D@example.com:80#example-ss2022 12 | ``` 13 | 14 | ``` 15 | ss://MjAyMi1ibGFrZTMtYWVzLTEyOC1nY206Y21lZklCdDhwMTJaZm1QWUplMnNCNThRd3R3NXNKeVpUV0Z6ZENKV2taOD06eEJHZUxiMWNPTjFIeE9CenF6UlN0VFdhUUh6YWM2cFhRVFNZd2dVV2R1RT0@example.com:81?type=tcp#example-ss2022 16 | ``` 17 | Может быть без `?type=tcp` 18 | 19 | # VLESS 20 | 21 | ## Reality 22 | ``` 23 | 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 24 | ``` 25 | 26 | ``` 27 | 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 28 | ``` 29 | 30 | ## TLS 31 | 1. 32 | ``` 33 | vless://8100b6eb-3fd1-4e73-8ccf-b4ac961232d6@example.com:443?type=tcp&security=tls&fp=&alpn=h3%2Ch2%2Chttp%2F1.1#vless-tls 34 | ``` 35 | 36 | 2. 37 | ``` 38 | 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 39 | ``` 40 | 3. 41 | ``` 42 | 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 43 | ``` 44 | 45 | 4. 46 | ``` 47 | vless://[someid]@[someserver]?security=tls&sni=[somesni]&type=ws&path=/?ed%3D2560&host=[somesni]&encryption=none#vless-tls-ws-2 48 | ``` 49 | 50 | 5. 51 | ``` 52 | vless://uuid@server:443?security=tls&sni=server&fp=chrome&type=ws&path=/websocket&encryption=none#vless-tls-ws-3 53 | ``` 54 | 55 | 6. 56 | ``` 57 | vless://33333@example.com:443/?type=ws&encryption=none&path=%2Fwebsocket&security=tls&sni=example.com&fp=chrome#vless-tls-ws-4 58 | ``` 59 | 60 | ## No security 61 | ``` 62 | vless://8b60389a-7a01-4365-9244-c87f12bb98cf@example.com:443?type=tcp&security=none#vless-tls-no-encrypt 63 | ``` -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | REPO="https://api.github.com/repos/itdoginfo/podkop/releases/latest" 4 | DOWNLOAD_DIR="/tmp/podkop" 5 | COUNT=3 6 | 7 | rm -rf "$DOWNLOAD_DIR" 8 | mkdir -p "$DOWNLOAD_DIR" 9 | 10 | msg() { 11 | printf "\033[32;1m%s\033[0m\n" "$1" 12 | } 13 | 14 | main() { 15 | check_system 16 | sing_box 17 | 18 | /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 19 | 20 | opkg update || { echo "opkg update failed"; exit 1; } 21 | 22 | if [ -f "/etc/init.d/podkop" ]; then 23 | msg "Podkop is already installed. Upgraded..." 24 | else 25 | msg "Installed podkop..." 26 | fi 27 | 28 | if command -v curl &> /dev/null; then 29 | check_response=$(curl -s "https://api.github.com/repos/itdoginfo/podkop/releases/latest") 30 | 31 | if echo "$check_response" | grep -q 'API rate limit '; then 32 | msg "You've reached rate limit from GitHub. Repeat in five minutes." 33 | exit 1 34 | fi 35 | fi 36 | 37 | download_success=0 38 | while read -r url; do 39 | filename=$(basename "$url") 40 | filepath="$DOWNLOAD_DIR/$filename" 41 | 42 | attempt=0 43 | while [ $attempt -lt $COUNT ]; do 44 | msg "Download $filename (count $((attempt+1)))..." 45 | if wget -q -O "$filepath" "$url"; then 46 | if [ -s "$filepath" ]; then 47 | msg "$filename successfully downloaded" 48 | download_success=1 49 | break 50 | fi 51 | fi 52 | msg "Download error $filename. Retry..." 53 | rm -f "$filepath" 54 | attempt=$((attempt+1)) 55 | done 56 | 57 | if [ $attempt -eq $COUNT ]; then 58 | msg "Failed to download $filename after $COUNT attempts" 59 | fi 60 | done < <(wget -qO- "$REPO" | grep -o 'https://[^"[:space:]]*\.ipk') 61 | 62 | if [ $download_success -eq 0 ]; then 63 | msg "No packages were downloaded successfully" 64 | exit 1 65 | fi 66 | 67 | for pkg in podkop luci-app-podkop; do 68 | file=$(ls "$DOWNLOAD_DIR" | grep "^$pkg" | head -n 1) 69 | if [ -n "$file" ]; then 70 | msg "Installing $file" 71 | opkg install "$DOWNLOAD_DIR/$file" 72 | sleep 3 73 | fi 74 | done 75 | 76 | ru=$(ls "$DOWNLOAD_DIR" | grep "luci-i18n-podkop-ru" | head -n 1) 77 | if [ -n "$ru" ]; then 78 | if opkg list-installed | grep -q luci-i18n-podkop-ru; then 79 | msg "Upgraded ru translation..." 80 | opkg remove luci-i18n-podkop* 81 | opkg install "$DOWNLOAD_DIR/$ru" 82 | else 83 | msg "Русский язык интерфейса ставим? y/n (Need a Russian translation?)" 84 | while true; do 85 | read -r -p '' RUS 86 | case $RUS in 87 | y) 88 | opkg remove luci-i18n-podkop* 89 | opkg install "$DOWNLOAD_DIR/$ru" 90 | break 91 | ;; 92 | n) 93 | break 94 | ;; 95 | *) 96 | echo "Введите y или n" 97 | ;; 98 | esac 99 | done 100 | fi 101 | fi 102 | 103 | find "$DOWNLOAD_DIR" -type f -name '*podkop*' -exec rm {} \; 104 | } 105 | 106 | check_system() { 107 | # Get router model 108 | MODEL=$(cat /tmp/sysinfo/model) 109 | msg "Router model: $MODEL" 110 | 111 | # Check available space 112 | AVAILABLE_SPACE=$(df /overlay | awk 'NR==2 {print $4}') 113 | REQUIRED_SPACE=15360 # 15MB in KB 114 | 115 | if [ "$AVAILABLE_SPACE" -lt "$REQUIRED_SPACE" ]; then 116 | msg "Error: Insufficient space in flash" 117 | msg "Available: $((AVAILABLE_SPACE/1024))MB" 118 | msg "Required: $((REQUIRED_SPACE/1024))MB" 119 | exit 1 120 | fi 121 | 122 | if ! nslookup google.com >/dev/null 2>&1; then 123 | msg "DNS not working" 124 | exit 1 125 | fi 126 | 127 | if opkg list-installed | grep -q https-dns-proxy; then 128 | msg "Сonflicting package detected: https-dns-proxy. Remove?" 129 | 130 | while true; do 131 | read -r -p '' DNSPROXY 132 | case $DNSPROXY in 133 | 134 | yes|y|Y|yes) 135 | opkg remove --force-depends luci-app-https-dns-proxy https-dns-proxy luci-i18n-https-dns-proxy* 136 | break 137 | ;; 138 | *) 139 | msg "Exit" 140 | exit 1 141 | ;; 142 | esac 143 | done 144 | fi 145 | 146 | if opkg list-installed | grep -q "iptables-mod-extra"; then 147 | msg "Found incompatible iptables packages. If you're using FriendlyWrt: https://t.me/itdogchat/44512/181082" 148 | fi 149 | } 150 | 151 | sing_box() { 152 | if ! opkg list-installed | grep -q "^sing-box"; then 153 | return 154 | fi 155 | 156 | sing_box_version=$(sing-box version | head -n 1 | awk '{print $3}') 157 | required_version="1.11.1" 158 | 159 | if [ "$(echo -e "$sing_box_version\n$required_version" | sort -V | head -n 1)" != "$required_version" ]; then 160 | msg "sing-box version $sing_box_version is older than required $required_version" 161 | msg "Removing old version..." 162 | opkg remove sing-box 163 | fi 164 | } 165 | 166 | main 167 | -------------------------------------------------------------------------------- /luci-app-podkop/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | PKG_NAME:=luci-app-podkop 4 | PKG_VERSION:=0.4.4 5 | PKG_RELEASE:=1 6 | 7 | LUCI_TITLE:=LuCI podkop app 8 | LUCI_DEPENDS:=+luci-base +podkop 9 | LUCI_PKGARCH:=all 10 | LUCI_LANG.ru:=Русский (Russian) 11 | LUCI_LANG.en:=English 12 | 13 | PKG_LICENSE:=GPL-2.0-or-later 14 | PKG_MAINTAINER:=ITDog 15 | 16 | LUCI_LANGUAGES:=en ru 17 | 18 | include $(TOPDIR)/feeds/luci/luci.mk 19 | 20 | # call BuildPackage - OpenWrt buildroot signature -------------------------------------------------------------------------------- /luci-app-podkop/htdocs/luci-static/resources/view/podkop/additionalTab.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require form'; 3 | 'require baseclass'; 4 | 'require view.podkop.constants as constants'; 5 | 'require tools.widgets as widgets'; 6 | 7 | function createAdditionalSection(mainSection, network) { 8 | let o = mainSection.tab('additional', _('Additional Settings')); 9 | 10 | o = mainSection.taboption('additional', form.Flag, 'yacd', _('Yacd enable'), _('openwrt.lan:9090/ui')); 11 | o.default = '0'; 12 | o.rmempty = false; 13 | o.ucisection = 'main'; 14 | 15 | o = mainSection.taboption('additional', form.Flag, 'exclude_ntp', _('Exclude NTP'), _('For issues with open connections sing-box')); 16 | o.default = '0'; 17 | o.rmempty = false; 18 | o.ucisection = 'main'; 19 | 20 | o = mainSection.taboption('additional', form.Flag, 'quic_disable', _('QUIC disable'), _('For issues with the video stream')); 21 | o.default = '0'; 22 | o.rmempty = false; 23 | o.ucisection = 'main'; 24 | 25 | o = mainSection.taboption('additional', form.ListValue, 'update_interval', _('List Update Frequency'), _('Select how often the lists will be updated')); 26 | Object.entries(constants.UPDATE_INTERVAL_OPTIONS).forEach(([key, label]) => { 27 | o.value(key, _(label)); 28 | }); 29 | o.default = '1d'; 30 | o.rmempty = false; 31 | o.ucisection = 'main'; 32 | 33 | o = mainSection.taboption('additional', form.ListValue, 'dns_type', _('DNS Protocol Type'), _('Select DNS protocol to use')); 34 | o.value('doh', _('DNS over HTTPS (DoH)')); 35 | o.value('dot', _('DNS over TLS (DoT)')); 36 | o.value('udp', _('UDP (Unprotected DNS)')); 37 | o.default = 'doh'; 38 | o.rmempty = false; 39 | o.ucisection = 'main'; 40 | 41 | o = mainSection.taboption('additional', form.Value, 'dns_server', _('DNS Server'), _('Select or enter DNS server address')); 42 | Object.entries(constants.DNS_SERVER_OPTIONS).forEach(([key, label]) => { 43 | o.value(key, _(label)); 44 | }); 45 | o.default = '8.8.8.8'; 46 | o.rmempty = false; 47 | o.ucisection = 'main'; 48 | o.validate = function (section_id, value) { 49 | if (!value) { 50 | return _('DNS server address cannot be empty'); 51 | } 52 | 53 | const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; 54 | if (ipRegex.test(value)) { 55 | const parts = value.split('.'); 56 | for (const part of parts) { 57 | const num = parseInt(part); 58 | if (num < 0 || num > 255) { 59 | return _('IP address parts must be between 0 and 255'); 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | const domainRegex = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(\/[^\s]*)?$/; 66 | if (!domainRegex.test(value)) { 67 | return _('Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com or dns.example.com/nicedns for DoH'); 68 | } 69 | 70 | return true; 71 | }; 72 | 73 | o = mainSection.taboption('additional', form.Value, 'dns_rewrite_ttl', _('DNS Rewrite TTL'), _('Time in seconds for DNS record caching (default: 60)')); 74 | o.default = '60'; 75 | o.rmempty = false; 76 | o.ucisection = 'main'; 77 | o.validate = function (section_id, value) { 78 | if (!value) { 79 | return _('TTL value cannot be empty'); 80 | } 81 | 82 | const ttl = parseInt(value); 83 | if (isNaN(ttl) || ttl < 0) { 84 | return _('TTL must be a positive number'); 85 | } 86 | 87 | return true; 88 | }; 89 | 90 | o = mainSection.taboption('additional', form.Value, 'cache_file', _('Cache File Path'), _('Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing')); 91 | o.value('/tmp/cache.db', 'RAM (/tmp/cache.db)'); 92 | o.value('/usr/share/sing-box/cache.db', 'Flash (/usr/share/sing-box/cache.db)'); 93 | o.default = '/tmp/cache.db'; 94 | o.rmempty = false; 95 | o.ucisection = 'main'; 96 | o.validate = function (section_id, value) { 97 | if (!value) { 98 | return _('Cache file path cannot be empty'); 99 | } 100 | 101 | if (!value.startsWith('/')) { 102 | return _('Path must be absolute (start with /)'); 103 | } 104 | 105 | if (!value.endsWith('cache.db')) { 106 | return _('Path must end with cache.db'); 107 | } 108 | 109 | const parts = value.split('/').filter(Boolean); 110 | if (parts.length < 2) { 111 | return _('Path must contain at least one directory (like /tmp/cache.db)'); 112 | } 113 | 114 | return true; 115 | }; 116 | 117 | o = mainSection.taboption('additional', widgets.DeviceSelect, 'iface', _('Source Network Interface'), _('Select the network interface from which the traffic will originate')); 118 | o.ucisection = 'main'; 119 | o.default = 'br-lan'; 120 | o.noaliases = true; 121 | o.nobridges = false; 122 | o.noinactive = false; 123 | o.multiple = true; 124 | o.filter = function (section_id, value) { 125 | if (['wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan'].indexOf(value) !== -1) { 126 | return false; 127 | } 128 | 129 | var device = this.devices.filter(function (dev) { 130 | return dev.getName() === value; 131 | })[0]; 132 | 133 | if (device) { 134 | var type = device.getType(); 135 | return type !== 'wifi' && type !== 'wireless' && !type.includes('wlan'); 136 | } 137 | 138 | return true; 139 | }; 140 | 141 | o = mainSection.taboption('additional', form.Flag, 'mon_restart_ifaces', _('Interface monitoring'), _('Interface monitoring for bad WAN')); 142 | o.default = '0'; 143 | o.rmempty = false; 144 | o.ucisection = 'main'; 145 | 146 | o = mainSection.taboption('additional', widgets.NetworkSelect, 'restart_ifaces', _('Interface for monitoring'), _('Select the WAN interfaces to be monitored')); 147 | o.ucisection = 'main'; 148 | o.depends('mon_restart_ifaces', '1'); 149 | o.multiple = true; 150 | o.filter = function (section_id, value) { 151 | return ['lan', 'loopback'].indexOf(value) === -1 && !value.startsWith('@'); 152 | }; 153 | 154 | o = mainSection.taboption('additional', form.Flag, 'dont_touch_dhcp', _('Dont touch my DHCP!'), _('Podkop will not change the DHCP config')); 155 | o.default = '0'; 156 | o.rmempty = false; 157 | o.ucisection = 'main'; 158 | 159 | o = mainSection.taboption('additional', form.Flag, 'detour', _('Proxy download of lists'), _('Downloading all lists via main Proxy/VPN')); 160 | o.default = '0'; 161 | o.rmempty = false; 162 | o.ucisection = 'main'; 163 | 164 | // Extra IPs and exclusions (main section) 165 | o = mainSection.taboption('basic', form.Flag, 'exclude_from_ip_enabled', _('IP for exclusion'), _('Specify local IP addresses that will never use the configured route')); 166 | o.default = '0'; 167 | o.rmempty = false; 168 | o.ucisection = 'main'; 169 | 170 | o = mainSection.taboption('basic', form.DynamicList, 'exclude_traffic_ip', _('Local IPs'), _('Enter valid IPv4 addresses')); 171 | o.placeholder = 'IP'; 172 | o.depends('exclude_from_ip_enabled', '1'); 173 | o.rmempty = false; 174 | o.ucisection = 'main'; 175 | o.validate = function (section_id, value) { 176 | if (!value || value.length === 0) return true; 177 | const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; 178 | if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)'); 179 | const ipParts = value.split('.'); 180 | for (const part of ipParts) { 181 | const num = parseInt(part); 182 | if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255'); 183 | } 184 | return true; 185 | }; 186 | 187 | o = mainSection.taboption('basic', form.Flag, 'socks5', _('Mixed enable'), _('Browser port: 2080')); 188 | o.default = '0'; 189 | o.rmempty = false; 190 | o.ucisection = 'main'; 191 | } 192 | 193 | return baseclass.extend({ 194 | createAdditionalSection 195 | }); -------------------------------------------------------------------------------- /luci-app-podkop/htdocs/luci-static/resources/view/podkop/configSection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require baseclass'; 3 | 'require form'; 4 | 'require ui'; 5 | 'require network'; 6 | 'require view.podkop.constants as constants'; 7 | 'require tools.widgets as widgets'; 8 | 9 | function validateUrl(url, protocols = ['http:', 'https:']) { 10 | try { 11 | const parsedUrl = new URL(url); 12 | if (!protocols.includes(parsedUrl.protocol)) { 13 | return _('URL must use one of the following protocols: ') + protocols.join(', '); 14 | } 15 | return true; 16 | } catch (e) { 17 | return _('Invalid URL format'); 18 | } 19 | } 20 | 21 | function createConfigSection(section, map, network) { 22 | const s = section; 23 | 24 | let o = s.tab('basic', _('Basic Settings')); 25 | 26 | o = s.taboption('basic', form.ListValue, 'mode', _('Connection Type'), _('Select between VPN and Proxy connection methods for traffic routing')); 27 | o.value('proxy', ('Proxy')); 28 | o.value('vpn', ('VPN')); 29 | o.value('block', ('Block')); 30 | o.ucisection = s.section; 31 | 32 | o = s.taboption('basic', form.ListValue, 'proxy_config_type', _('Configuration Type'), _('Select how to configure the proxy')); 33 | o.value('url', _('Connection URL')); 34 | o.value('outbound', _('Outbound Config')); 35 | o.default = 'url'; 36 | o.depends('mode', 'proxy'); 37 | o.ucisection = s.section; 38 | 39 | o = s.taboption('basic', form.TextValue, 'proxy_string', _('Proxy Configuration URL'), _('')); 40 | o.depends('proxy_config_type', 'url'); 41 | o.rows = 5; 42 | o.rmempty = false; 43 | o.ucisection = s.section; 44 | o.sectionDescriptions = new Map(); 45 | 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'; 46 | 47 | o.renderWidget = function (section_id, option_index, cfgvalue) { 48 | const original = form.TextValue.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]); 49 | const container = E('div', {}); 50 | container.appendChild(original); 51 | 52 | if (cfgvalue) { 53 | try { 54 | const activeConfig = cfgvalue.split('\n') 55 | .map(line => line.trim()) 56 | .find(line => line && !line.startsWith('//')); 57 | 58 | if (activeConfig) { 59 | if (activeConfig.includes('#')) { 60 | const label = activeConfig.split('#').pop(); 61 | if (label && label.trim()) { 62 | const decodedLabel = decodeURIComponent(label); 63 | const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Current config: ') + decodedLabel); 64 | container.appendChild(descDiv); 65 | } else { 66 | const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); 67 | container.appendChild(descDiv); 68 | } 69 | } else { 70 | const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); 71 | container.appendChild(descDiv); 72 | } 73 | } 74 | } catch (e) { 75 | console.error('Error parsing config label:', e); 76 | const descDiv = E('div', { 'class': 'cbi-value-description' }, _('Config without description')); 77 | container.appendChild(descDiv); 78 | } 79 | } else { 80 | const defaultDesc = E('div', { 'class': 'cbi-value-description' }, 81 | _('Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs')); 82 | container.appendChild(defaultDesc); 83 | } 84 | 85 | return container; 86 | }; 87 | 88 | o.validate = function (section_id, value) { 89 | if (!value || value.length === 0) { 90 | return true; 91 | } 92 | 93 | try { 94 | const activeConfig = value.split('\n') 95 | .map(line => line.trim()) 96 | .find(line => line && !line.startsWith('//')); 97 | 98 | if (!activeConfig) { 99 | return _('No active configuration found. At least one non-commented line is required.'); 100 | } 101 | 102 | if (!activeConfig.startsWith('vless://') && !activeConfig.startsWith('ss://')) { 103 | return _('URL must start with vless:// or ss://'); 104 | } 105 | 106 | if (activeConfig.startsWith('ss://')) { 107 | let encrypted_part; 108 | try { 109 | let mainPart = activeConfig.includes('?') ? activeConfig.split('?')[0] : activeConfig.split('#')[0]; 110 | encrypted_part = mainPart.split('/')[2].split('@')[0]; 111 | try { 112 | let decoded = atob(encrypted_part); 113 | if (!decoded.includes(':')) { 114 | if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) { 115 | return _('Invalid Shadowsocks URL format: missing method and password separator ":"'); 116 | } 117 | } 118 | } catch (e) { 119 | if (!encrypted_part.includes(':') && !encrypted_part.includes('-')) { 120 | return _('Invalid Shadowsocks URL format: missing method and password separator ":"'); 121 | } 122 | } 123 | } catch (e) { 124 | return _('Invalid Shadowsocks URL format'); 125 | } 126 | 127 | try { 128 | let serverPart = activeConfig.split('@')[1]; 129 | if (!serverPart) return _('Invalid Shadowsocks URL: missing server address'); 130 | let [server, portAndRest] = serverPart.split(':'); 131 | if (!server) return _('Invalid Shadowsocks URL: missing server'); 132 | let port = portAndRest ? portAndRest.split(/[?#]/)[0] : null; 133 | if (!port) return _('Invalid Shadowsocks URL: missing port'); 134 | let portNum = parseInt(port); 135 | if (isNaN(portNum) || portNum < 1 || portNum > 65535) { 136 | return _('Invalid port number. Must be between 1 and 65535'); 137 | } 138 | } catch (e) { 139 | return _('Invalid Shadowsocks URL: missing or invalid server/port format'); 140 | } 141 | } 142 | 143 | if (activeConfig.startsWith('vless://')) { 144 | let uuid = activeConfig.split('/')[2].split('@')[0]; 145 | if (!uuid || uuid.length === 0) return _('Invalid VLESS URL: missing UUID'); 146 | 147 | try { 148 | let serverPart = activeConfig.split('@')[1]; 149 | if (!serverPart) return _('Invalid VLESS URL: missing server address'); 150 | let [server, portAndRest] = serverPart.split(':'); 151 | if (!server) return _('Invalid VLESS URL: missing server'); 152 | let port = portAndRest ? portAndRest.split(/[/?#]/)[0] : null; 153 | if (!port) return _('Invalid VLESS URL: missing port'); 154 | let portNum = parseInt(port); 155 | if (isNaN(portNum) || portNum < 1 || portNum > 65535) { 156 | return _('Invalid port number. Must be between 1 and 65535'); 157 | } 158 | } catch (e) { 159 | return _('Invalid VLESS URL: missing or invalid server/port format'); 160 | } 161 | 162 | let queryString = activeConfig.split('?')[1]; 163 | if (!queryString) return _('Invalid VLESS URL: missing query parameters'); 164 | 165 | let params = new URLSearchParams(queryString.split('#')[0]); 166 | let type = params.get('type'); 167 | const validTypes = ['tcp', 'raw', 'udp', 'grpc', 'http', 'ws']; 168 | if (!type || !validTypes.includes(type)) { 169 | return _('Invalid VLESS URL: type must be one of tcp, raw, udp, grpc, http, ws'); 170 | } 171 | 172 | let security = params.get('security'); 173 | const validSecurities = ['tls', 'reality', 'none']; 174 | if (!security || !validSecurities.includes(security)) { 175 | return _('Invalid VLESS URL: security must be one of tls, reality, none'); 176 | } 177 | 178 | if (security === 'reality') { 179 | if (!params.get('pbk')) return _('Invalid VLESS URL: missing pbk parameter for reality security'); 180 | if (!params.get('fp')) return _('Invalid VLESS URL: missing fp parameter for reality security'); 181 | } 182 | 183 | if (security === 'tls' && type !== 'tcp' && !params.get('sni')) { 184 | return _('Invalid VLESS URL: missing sni parameter for tls security'); 185 | } 186 | } 187 | 188 | return true; 189 | } catch (e) { 190 | console.error('Validation error:', e); 191 | return _('Invalid URL format: ') + e.message; 192 | } 193 | }; 194 | 195 | o = s.taboption('basic', form.TextValue, 'outbound_json', _('Outbound Configuration'), _('Enter complete outbound configuration in JSON format')); 196 | o.depends('proxy_config_type', 'outbound'); 197 | o.rows = 10; 198 | o.ucisection = s.section; 199 | o.validate = function (section_id, value) { 200 | if (!value || value.length === 0) return true; 201 | try { 202 | const parsed = JSON.parse(value); 203 | if (!parsed.type || !parsed.server || !parsed.server_port) { 204 | return _('JSON must contain at least type, server and server_port fields'); 205 | } 206 | return true; 207 | } catch (e) { 208 | return _('Invalid JSON format'); 209 | } 210 | }; 211 | 212 | o = s.taboption('basic', form.Flag, 'ss_uot', _('Shadowsocks UDP over TCP'), _('Apply for SS2022')); 213 | o.default = '0'; 214 | o.depends('mode', 'proxy'); 215 | o.rmempty = false; 216 | o.ucisection = 'main'; 217 | 218 | o = s.taboption('basic', widgets.DeviceSelect, 'interface', _('Network Interface'), _('Select network interface for VPN connection')); 219 | o.depends('mode', 'vpn'); 220 | o.ucisection = s.section; 221 | o.noaliases = true; 222 | o.nobridges = false; 223 | o.noinactive = false; 224 | o.filter = function (section_id, value) { 225 | if (['br-lan', 'eth0', 'eth1', 'wan', 'phy0-ap0', 'phy1-ap0', 'pppoe-wan', 'lan'].indexOf(value) !== -1) { 226 | return false; 227 | } 228 | 229 | var device = this.devices.filter(function (dev) { 230 | return dev.getName() === value; 231 | })[0]; 232 | 233 | if (device) { 234 | var type = device.getType(); 235 | return type !== 'wifi' && type !== 'wireless' && !type.includes('wlan'); 236 | } 237 | 238 | return true; 239 | }; 240 | 241 | o = s.taboption('basic', form.Flag, 'domain_list_enabled', _('Community Lists')); 242 | o.default = '0'; 243 | o.rmempty = false; 244 | o.ucisection = s.section; 245 | 246 | o = s.taboption('basic', form.DynamicList, 'domain_list', _('Service List'), _('Select predefined service for routing') + ' github.com/itdoginfo/allow-domains'); 247 | o.placeholder = 'Service list'; 248 | Object.entries(constants.DOMAIN_LIST_OPTIONS).forEach(([key, label]) => { 249 | o.value(key, _(label)); 250 | }); 251 | 252 | o.depends('domain_list_enabled', '1'); 253 | o.rmempty = false; 254 | o.ucisection = s.section; 255 | 256 | let lastValues = []; 257 | let isProcessing = false; 258 | 259 | o.onchange = function (ev, section_id, value) { 260 | if (isProcessing) return; 261 | isProcessing = true; 262 | 263 | try { 264 | const values = Array.isArray(value) ? value : [value]; 265 | let newValues = [...values]; 266 | let notifications = []; 267 | 268 | const selectedRegionalOptions = constants.REGIONAL_OPTIONS.filter(opt => newValues.includes(opt)); 269 | 270 | if (selectedRegionalOptions.length > 1) { 271 | const lastSelected = selectedRegionalOptions[selectedRegionalOptions.length - 1]; 272 | const removedRegions = selectedRegionalOptions.slice(0, -1); 273 | newValues = newValues.filter(v => v === lastSelected || !constants.REGIONAL_OPTIONS.includes(v)); 274 | notifications.push(E('p', { class: 'alert-message warning' }, [ 275 | E('strong', {}, _('Regional options cannot be used together')), E('br'), 276 | _('Warning: %s cannot be used together with %s. Previous selections have been removed.') 277 | .format(removedRegions.join(', '), lastSelected) 278 | ])); 279 | } 280 | 281 | if (newValues.includes('russia_inside')) { 282 | const removedServices = newValues.filter(v => !constants.ALLOWED_WITH_RUSSIA_INSIDE.includes(v)); 283 | if (removedServices.length > 0) { 284 | newValues = newValues.filter(v => constants.ALLOWED_WITH_RUSSIA_INSIDE.includes(v)); 285 | notifications.push(E('p', { class: 'alert-message warning' }, [ 286 | E('strong', {}, _('Russia inside restrictions')), E('br'), 287 | _('Warning: Russia inside can only be used with %s. %s already in Russia inside and have been removed from selection.') 288 | .format( 289 | constants.ALLOWED_WITH_RUSSIA_INSIDE.map(key => constants.DOMAIN_LIST_OPTIONS[key]).filter(label => label !== 'Russia inside').join(', '), 290 | removedServices.join(', ') 291 | ) 292 | ])); 293 | } 294 | } 295 | 296 | if (JSON.stringify(newValues.sort()) !== JSON.stringify(values.sort())) { 297 | this.getUIElement(section_id).setValue(newValues); 298 | } 299 | 300 | notifications.forEach(notification => ui.addNotification(null, notification)); 301 | lastValues = newValues; 302 | } catch (e) { 303 | console.error('Error in onchange handler:', e); 304 | } finally { 305 | isProcessing = false; 306 | } 307 | }; 308 | 309 | o = s.taboption('basic', form.ListValue, 'custom_domains_list_type', _('User Domain List Type'), _('Select how to add your custom domains')); 310 | o.value('disabled', _('Disabled')); 311 | o.value('dynamic', _('Dynamic List')); 312 | o.value('text', _('Text List')); 313 | o.default = 'disabled'; 314 | o.rmempty = false; 315 | o.ucisection = s.section; 316 | 317 | o = s.taboption('basic', form.DynamicList, 'custom_domains', _('User Domains'), _('Enter domain names without protocols (example: sub.example.com or example.com)')); 318 | o.placeholder = 'Domains list'; 319 | o.depends('custom_domains_list_type', 'dynamic'); 320 | o.rmempty = false; 321 | o.ucisection = s.section; 322 | o.validate = function (section_id, value) { 323 | if (!value || value.length === 0) return true; 324 | const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/; 325 | if (!domainRegex.test(value)) { 326 | return _('Invalid domain format. Enter domain without protocol (example: sub.example.com or ru)'); 327 | } 328 | return true; 329 | }; 330 | 331 | o = s.taboption('basic', form.TextValue, 'custom_domains_text', _('User Domains List'), _('Enter domain names separated by comma, space or newline. You can add comments after //')); 332 | o.placeholder = 'example.com, sub.example.com\n// Social networks\ndomain.com test.com // personal domains'; 333 | o.depends('custom_domains_list_type', 'text'); 334 | o.rows = 8; 335 | o.rmempty = false; 336 | o.ucisection = s.section; 337 | o.validate = function (section_id, value) { 338 | if (!value || value.length === 0) return true; 339 | 340 | const domainRegex = /^(?!-)[A-Za-z0-9-]+([-.][A-Za-z0-9-]+)*(\.[A-Za-z]{2,})?$/; 341 | const lines = value.split(/\n/).map(line => line.trim()); 342 | let hasValidDomain = false; 343 | 344 | for (const line of lines) { 345 | // Skip empty lines 346 | if (!line) continue; 347 | 348 | // Extract domain part (before any //) 349 | const domainPart = line.split('//')[0].trim(); 350 | 351 | // Skip if line is empty after removing comments 352 | if (!domainPart) continue; 353 | 354 | // Process each domain in the line (separated by comma or space) 355 | const domains = domainPart.split(/[,\s]+/).map(d => d.trim()).filter(d => d.length > 0); 356 | 357 | for (const domain of domains) { 358 | if (!domainRegex.test(domain)) { 359 | return _('Invalid domain format: %s. Enter domain without protocol').format(domain); 360 | } 361 | hasValidDomain = true; 362 | } 363 | } 364 | 365 | if (!hasValidDomain) { 366 | return _('At least one valid domain must be specified. Comments-only content is not allowed.'); 367 | } 368 | 369 | return true; 370 | }; 371 | 372 | o = s.taboption('basic', form.Flag, 'custom_local_domains_list_enabled', _('Local Domain Lists'), _('Use the list from the router filesystem')); 373 | o.default = '0'; 374 | o.rmempty = false; 375 | o.ucisection = s.section; 376 | 377 | o = s.taboption('basic', form.DynamicList, 'custom_local_domains', _('Local Domain Lists Path'), _('Enter the list file path')); 378 | o.placeholder = '/path/file.lst'; 379 | o.depends('custom_local_domains_list_enabled', '1'); 380 | o.rmempty = false; 381 | o.ucisection = s.section; 382 | o.validate = function (section_id, value) { 383 | if (!value || value.length === 0) return true; 384 | const pathRegex = /^\/[a-zA-Z0-9_\-\/\.]+$/; 385 | if (!pathRegex.test(value)) { 386 | return _('Invalid path format. Path must start with "/" and contain valid characters'); 387 | } 388 | return true; 389 | }; 390 | 391 | o = s.taboption('basic', form.Flag, 'custom_download_domains_list_enabled', _('Remote Domain Lists'), _('Download and use domain lists from remote URLs')); 392 | o.default = '0'; 393 | o.rmempty = false; 394 | o.ucisection = s.section; 395 | 396 | o = s.taboption('basic', form.DynamicList, 'custom_download_domains', _('Remote Domain URLs'), _('Enter full URLs starting with http:// or https://')); 397 | o.placeholder = 'URL'; 398 | o.depends('custom_download_domains_list_enabled', '1'); 399 | o.rmempty = false; 400 | o.ucisection = s.section; 401 | o.validate = function (section_id, value) { 402 | if (!value || value.length === 0) return true; 403 | return validateUrl(value); 404 | }; 405 | 406 | o = s.taboption('basic', form.ListValue, 'custom_subnets_list_enabled', _('User Subnet List Type'), _('Select how to add your custom subnets')); 407 | o.value('disabled', _('Disabled')); 408 | o.value('dynamic', _('Dynamic List')); 409 | o.value('text', _('Text List (comma/space/newline separated)')); 410 | o.default = 'disabled'; 411 | o.rmempty = false; 412 | o.ucisection = s.section; 413 | 414 | o = s.taboption('basic', form.DynamicList, 'custom_subnets', _('User Subnets'), _('Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses')); 415 | o.placeholder = 'IP or subnet'; 416 | o.depends('custom_subnets_list_enabled', 'dynamic'); 417 | o.rmempty = false; 418 | o.ucisection = s.section; 419 | o.validate = function (section_id, value) { 420 | if (!value || value.length === 0) return true; 421 | const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; 422 | if (!subnetRegex.test(value)) return _('Invalid format. Use format: X.X.X.X or X.X.X.X/Y'); 423 | const [ip, cidr] = value.split('/'); 424 | const ipParts = ip.split('.'); 425 | for (const part of ipParts) { 426 | const num = parseInt(part); 427 | if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255'); 428 | } 429 | if (cidr !== undefined) { 430 | const cidrNum = parseInt(cidr); 431 | if (cidrNum < 0 || cidrNum > 32) return _('CIDR must be between 0 and 32'); 432 | } 433 | return true; 434 | }; 435 | 436 | o = s.taboption('basic', form.TextValue, 'custom_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 //')); 437 | 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'; 438 | o.depends('custom_subnets_list_enabled', 'text'); 439 | o.rows = 10; 440 | o.rmempty = false; 441 | o.ucisection = s.section; 442 | o.validate = function (section_id, value) { 443 | if (!value || value.length === 0) return true; 444 | 445 | const subnetRegex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; 446 | const lines = value.split(/\n/).map(line => line.trim()); 447 | let hasValidSubnet = false; 448 | 449 | for (const line of lines) { 450 | // Skip empty lines 451 | if (!line) continue; 452 | 453 | // Extract subnet part (before any //) 454 | const subnetPart = line.split('//')[0].trim(); 455 | 456 | // Skip if line is empty after removing comments 457 | if (!subnetPart) continue; 458 | 459 | // Process each subnet in the line (separated by comma or space) 460 | const subnets = subnetPart.split(/[,\s]+/).map(s => s.trim()).filter(s => s.length > 0); 461 | 462 | for (const subnet of subnets) { 463 | if (!subnetRegex.test(subnet)) { 464 | return _('Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y').format(subnet); 465 | } 466 | 467 | const [ip, cidr] = subnet.split('/'); 468 | const ipParts = ip.split('.'); 469 | for (const part of ipParts) { 470 | const num = parseInt(part); 471 | if (num < 0 || num > 255) { 472 | return _('IP parts must be between 0 and 255 in: %s').format(subnet); 473 | } 474 | } 475 | 476 | if (cidr !== undefined) { 477 | const cidrNum = parseInt(cidr); 478 | if (cidrNum < 0 || cidrNum > 32) { 479 | return _('CIDR must be between 0 and 32 in: %s').format(subnet); 480 | } 481 | } 482 | hasValidSubnet = true; 483 | } 484 | } 485 | 486 | if (!hasValidSubnet) { 487 | return _('At least one valid subnet or IP must be specified. Comments-only content is not allowed.'); 488 | } 489 | 490 | return true; 491 | }; 492 | 493 | o = s.taboption('basic', form.Flag, 'custom_download_subnets_list_enabled', _('Remote Subnet Lists'), _('Download and use subnet lists from remote URLs')); 494 | o.default = '0'; 495 | o.rmempty = false; 496 | o.ucisection = s.section; 497 | 498 | o = s.taboption('basic', form.DynamicList, 'custom_download_subnets', _('Remote Subnet URLs'), _('Enter full URLs starting with http:// or https://')); 499 | o.placeholder = 'URL'; 500 | o.depends('custom_download_subnets_list_enabled', '1'); 501 | o.rmempty = false; 502 | o.ucisection = s.section; 503 | o.validate = function (section_id, value) { 504 | if (!value || value.length === 0) return true; 505 | return validateUrl(value); 506 | }; 507 | 508 | 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')); 509 | o.default = '0'; 510 | o.rmempty = false; 511 | o.ucisection = s.section; 512 | 513 | o = s.taboption('basic', form.DynamicList, 'all_traffic_ip', _('Local IPs'), _('Enter valid IPv4 addresses')); 514 | o.placeholder = 'IP'; 515 | o.depends('all_traffic_from_ip_enabled', '1'); 516 | o.rmempty = false; 517 | o.ucisection = s.section; 518 | o.validate = function (section_id, value) { 519 | if (!value || value.length === 0) return true; 520 | const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/; 521 | if (!ipRegex.test(value)) return _('Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)'); 522 | const ipParts = value.split('.'); 523 | for (const part of ipParts) { 524 | const num = parseInt(part); 525 | if (num < 0 || num > 255) return _('IP address parts must be between 0 and 255'); 526 | } 527 | return true; 528 | }; 529 | } 530 | 531 | return baseclass.extend({ 532 | createConfigSection 533 | }); -------------------------------------------------------------------------------- /luci-app-podkop/htdocs/luci-static/resources/view/podkop/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require baseclass'; 3 | 4 | const STATUS_COLORS = { 5 | SUCCESS: '#4caf50', 6 | ERROR: '#f44336', 7 | WARNING: '#ff9800' 8 | }; 9 | 10 | const FAKEIP_CHECK_DOMAIN = 'fakeip.podkop.fyi'; 11 | const IP_CHECK_DOMAIN = 'ip.podkop.fyi'; 12 | 13 | const REGIONAL_OPTIONS = ['russia_inside', 'russia_outside', 'ukraine_inside']; 14 | const ALLOWED_WITH_RUSSIA_INSIDE = [ 15 | 'russia_inside', 16 | 'meta', 17 | 'twitter', 18 | 'discord', 19 | 'telegram', 20 | 'cloudflare', 21 | 'google_ai', 22 | 'google_play', 23 | 'hetzner', 24 | 'ovh' 25 | ]; 26 | 27 | const DOMAIN_LIST_OPTIONS = { 28 | russia_inside: 'Russia inside', 29 | russia_outside: 'Russia outside', 30 | ukraine_inside: 'Ukraine', 31 | geoblock: 'Geo Block', 32 | block: 'Block', 33 | porn: 'Porn', 34 | news: 'News', 35 | anime: 'Anime', 36 | youtube: 'Youtube', 37 | discord: 'Discord', 38 | meta: 'Meta', 39 | twitter: 'Twitter (X)', 40 | hdrezka: 'HDRezka', 41 | tiktok: 'Tik-Tok', 42 | telegram: 'Telegram', 43 | cloudflare: 'Cloudflare', 44 | google_ai: 'Google AI', 45 | google_play: 'Google Play', 46 | hetzner: 'Hetzner ASN', 47 | ovh: 'OVH ASN' 48 | }; 49 | 50 | const UPDATE_INTERVAL_OPTIONS = { 51 | '1h': 'Every hour', 52 | '3h': 'Every 3 hours', 53 | '12h': 'Every 12 hours', 54 | '1d': 'Every day', 55 | '3d': 'Every 3 days' 56 | }; 57 | 58 | const DNS_SERVER_OPTIONS = { 59 | '1.1.1.1': 'Cloudflare (1.1.1.1)', 60 | '8.8.8.8': 'Google (8.8.8.8)', 61 | '9.9.9.9': 'Quad9 (9.9.9.9)', 62 | 'dns.adguard-dns.com': 'AdGuard Default (dns.adguard-dns.com)', 63 | 'unfiltered.adguard-dns.com': 'AdGuard Unfiltered (unfiltered.adguard-dns.com)', 64 | 'family.adguard-dns.com': 'AdGuard Family (family.adguard-dns.com)' 65 | }; 66 | 67 | const DIAGNOSTICS_UPDATE_INTERVAL = 10000; // 10 seconds 68 | const CACHE_TIMEOUT = DIAGNOSTICS_UPDATE_INTERVAL - 1000; // 9 seconds 69 | const ERROR_POLL_INTERVAL = 10000; // 10 seconds 70 | const COMMAND_TIMEOUT = 10000; // 10 seconds 71 | const FETCH_TIMEOUT = 10000; // 10 seconds 72 | const BUTTON_FEEDBACK_TIMEOUT = 1000; // 1 second 73 | const DIAGNOSTICS_INITIAL_DELAY = 100; // 100 milliseconds 74 | 75 | // Интервалы планирования команд в диагностике (в миллисекундах) 76 | const COMMAND_SCHEDULING = { 77 | P0_PRIORITY: 0, // Наивысший приоритет (без задержки) 78 | P1_PRIORITY: 100, // Очень высокий приоритет 79 | P2_PRIORITY: 300, // Высокий приоритет 80 | P3_PRIORITY: 500, // Выше среднего 81 | P4_PRIORITY: 700, // Стандартный приоритет 82 | P5_PRIORITY: 900, // Ниже среднего 83 | P6_PRIORITY: 1100, // Низкий приоритет 84 | P7_PRIORITY: 1300, // Очень низкий приоритет 85 | P8_PRIORITY: 1500, // Фоновое выполнение 86 | P9_PRIORITY: 1700, // Выполнение в режиме простоя 87 | P10_PRIORITY: 1900 // Наименьший приоритет 88 | }; 89 | 90 | return baseclass.extend({ 91 | STATUS_COLORS, 92 | FAKEIP_CHECK_DOMAIN, 93 | IP_CHECK_DOMAIN, 94 | REGIONAL_OPTIONS, 95 | ALLOWED_WITH_RUSSIA_INSIDE, 96 | DOMAIN_LIST_OPTIONS, 97 | UPDATE_INTERVAL_OPTIONS, 98 | DNS_SERVER_OPTIONS, 99 | DIAGNOSTICS_UPDATE_INTERVAL, 100 | ERROR_POLL_INTERVAL, 101 | COMMAND_TIMEOUT, 102 | FETCH_TIMEOUT, 103 | BUTTON_FEEDBACK_TIMEOUT, 104 | DIAGNOSTICS_INITIAL_DELAY, 105 | COMMAND_SCHEDULING, 106 | CACHE_TIMEOUT 107 | }); 108 | -------------------------------------------------------------------------------- /luci-app-podkop/htdocs/luci-static/resources/view/podkop/diagnosticTab.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require baseclass'; 3 | 'require form'; 4 | 'require ui'; 5 | 'require uci'; 6 | 'require fs'; 7 | 'require view.podkop.constants as constants'; 8 | 'require view.podkop.utils as utils'; 9 | 10 | // Cache system for network requests 11 | const fetchCache = {}; 12 | 13 | // Helper function to fetch with cache 14 | async function cachedFetch(url, options = {}) { 15 | const cacheKey = url; 16 | const currentTime = Date.now(); 17 | 18 | // If we have a valid cached response, return it 19 | if (fetchCache[cacheKey] && currentTime - fetchCache[cacheKey].timestamp < constants.CACHE_TIMEOUT) { 20 | console.log(`Using cached response for ${url}`); 21 | return Promise.resolve(fetchCache[cacheKey].response.clone()); 22 | } 23 | 24 | // Otherwise, make a new request 25 | try { 26 | const response = await fetch(url, options); 27 | 28 | // Cache the response 29 | fetchCache[cacheKey] = { 30 | response: response.clone(), 31 | timestamp: currentTime 32 | }; 33 | 34 | return response; 35 | } catch (error) { 36 | throw error; 37 | } 38 | } 39 | 40 | // Helper functions for command execution with prioritization - Using from utils.js now 41 | function safeExec(command, args, priority, callback, timeout = constants.COMMAND_TIMEOUT) { 42 | return utils.safeExec(command, args, priority, callback, timeout); 43 | } 44 | 45 | // Helper functions for handling checks 46 | function runCheck(checkFunction, priority, callback) { 47 | // Default to highest priority execution if priority is not provided or invalid 48 | let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY; 49 | 50 | // If priority is a string, try to get the corresponding delay value 51 | if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) { 52 | schedulingDelay = constants.COMMAND_SCHEDULING[priority]; 53 | } 54 | 55 | const executeCheck = async () => { 56 | try { 57 | const result = await checkFunction(); 58 | if (callback && typeof callback === 'function') { 59 | callback(result); 60 | } 61 | return result; 62 | } catch (error) { 63 | if (callback && typeof callback === 'function') { 64 | callback({ error }); 65 | } 66 | return { error }; 67 | } 68 | }; 69 | 70 | if (callback && typeof callback === 'function') { 71 | setTimeout(executeCheck, schedulingDelay); 72 | return; 73 | } else { 74 | return executeCheck(); 75 | } 76 | } 77 | 78 | function runAsyncTask(taskFunction, priority) { 79 | // Default to highest priority execution if priority is not provided or invalid 80 | let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY; 81 | 82 | // If priority is a string, try to get the corresponding delay value 83 | if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) { 84 | schedulingDelay = constants.COMMAND_SCHEDULING[priority]; 85 | } 86 | 87 | setTimeout(async () => { 88 | try { 89 | await taskFunction(); 90 | } catch (error) { 91 | console.error('Async task error:', error); 92 | } 93 | }, schedulingDelay); 94 | } 95 | 96 | // Helper Functions for UI and formatting 97 | function createStatus(state, message, color) { 98 | return { 99 | state, 100 | message: _(message), 101 | color: constants.STATUS_COLORS[color] 102 | }; 103 | } 104 | 105 | function formatDiagnosticOutput(output) { 106 | if (typeof output !== 'string') return ''; 107 | return output.trim() 108 | .replace(/\x1b\[[0-9;]*m/g, '') 109 | .replace(/\r\n/g, '\n') 110 | .replace(/\r/g, '\n'); 111 | } 112 | 113 | function copyToClipboard(text, button) { 114 | const textarea = document.createElement('textarea'); 115 | textarea.value = text; 116 | document.body.appendChild(textarea); 117 | textarea.select(); 118 | try { 119 | document.execCommand('copy'); 120 | const originalText = button.textContent; 121 | button.textContent = _('Copied!'); 122 | setTimeout(() => button.textContent = originalText, constants.BUTTON_FEEDBACK_TIMEOUT); 123 | } catch (err) { 124 | ui.addNotification(null, E('p', {}, _('Failed to copy: ') + err.message)); 125 | } 126 | document.body.removeChild(textarea); 127 | } 128 | 129 | // IP masking function 130 | function maskIP(ip) { 131 | if (!ip) return ''; 132 | const parts = ip.split('.'); 133 | if (parts.length !== 4) return ip; 134 | return ['XX', 'XX', 'XX', parts[3]].join('.'); 135 | } 136 | 137 | // Status Check Functions 138 | async function checkFakeIP() { 139 | try { 140 | const controller = new AbortController(); 141 | const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT); 142 | 143 | try { 144 | const response = await cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal }); 145 | const data = await response.json(); 146 | clearTimeout(timeoutId); 147 | 148 | if (data.fakeip === true) { 149 | return createStatus('working', 'working', 'SUCCESS'); 150 | } else { 151 | return createStatus('not_working', 'not working', 'ERROR'); 152 | } 153 | } catch (fetchError) { 154 | clearTimeout(timeoutId); 155 | const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error'; 156 | return createStatus('error', message, 'WARNING'); 157 | } 158 | } catch (error) { 159 | return createStatus('error', 'check error', 'WARNING'); 160 | } 161 | } 162 | 163 | async function checkFakeIPCLI() { 164 | try { 165 | return new Promise((resolve) => { 166 | safeExec('nslookup', ['-timeout=2', constants.FAKEIP_CHECK_DOMAIN, '127.0.0.42'], 'P0_PRIORITY', result => { 167 | if (result.stdout && result.stdout.includes('198.18')) { 168 | resolve(createStatus('working', 'working on router', 'SUCCESS')); 169 | } else { 170 | resolve(createStatus('not_working', 'not working on router', 'ERROR')); 171 | } 172 | }); 173 | }); 174 | } catch (error) { 175 | return createStatus('error', 'CLI check error', 'WARNING'); 176 | } 177 | } 178 | 179 | function checkDNSAvailability() { 180 | return new Promise(async (resolve) => { 181 | try { 182 | safeExec('/usr/bin/podkop', ['check_dns_available'], 'P0_PRIORITY', dnsStatusResult => { 183 | if (!dnsStatusResult || !dnsStatusResult.stdout) { 184 | return resolve({ 185 | remote: createStatus('error', 'DNS check timeout', 'WARNING'), 186 | local: createStatus('error', 'DNS check timeout', 'WARNING') 187 | }); 188 | } 189 | 190 | try { 191 | const dnsStatus = JSON.parse(dnsStatusResult.stdout); 192 | 193 | const remoteStatus = dnsStatus.is_available ? 194 | createStatus('available', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) available`, 'SUCCESS') : 195 | createStatus('unavailable', `${dnsStatus.dns_type.toUpperCase()} (${dnsStatus.dns_server}) unavailable`, 'ERROR'); 196 | 197 | const localStatus = dnsStatus.local_dns_working ? 198 | createStatus('available', 'Router DNS working', 'SUCCESS') : 199 | createStatus('unavailable', 'Router DNS not working', 'ERROR'); 200 | 201 | return resolve({ 202 | remote: remoteStatus, 203 | local: localStatus 204 | }); 205 | } catch (parseError) { 206 | return resolve({ 207 | remote: createStatus('error', 'DNS check parse error', 'WARNING'), 208 | local: createStatus('error', 'DNS check parse error', 'WARNING') 209 | }); 210 | } 211 | }); 212 | } catch (error) { 213 | return resolve({ 214 | remote: createStatus('error', 'DNS check error', 'WARNING'), 215 | local: createStatus('error', 'DNS check error', 'WARNING') 216 | }); 217 | } 218 | }); 219 | } 220 | 221 | async function checkBypass() { 222 | try { 223 | const controller = new AbortController(); 224 | const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT); 225 | 226 | try { 227 | const response1 = await cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal }); 228 | const data1 = await response1.json(); 229 | 230 | const response2 = await cachedFetch(`https://${constants.IP_CHECK_DOMAIN}/check`, { signal: controller.signal }); 231 | const data2 = await response2.json(); 232 | 233 | clearTimeout(timeoutId); 234 | 235 | if (data1.IP && data2.IP) { 236 | if (data1.IP !== data2.IP) { 237 | return createStatus('working', 'working', 'SUCCESS'); 238 | } else { 239 | return createStatus('not_working', 'same IP for both domains', 'ERROR'); 240 | } 241 | } else { 242 | return createStatus('error', 'check error (no IP)', 'WARNING'); 243 | } 244 | } catch (fetchError) { 245 | clearTimeout(timeoutId); 246 | const message = fetchError.name === 'AbortError' ? 'timeout' : 'check error'; 247 | return createStatus('error', message, 'WARNING'); 248 | } 249 | } catch (error) { 250 | return createStatus('error', 'check error', 'WARNING'); 251 | } 252 | } 253 | 254 | // Modal Functions 255 | function createModalContent(title, content) { 256 | return [ 257 | E('div', { 258 | 'class': 'panel-body', 259 | style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' + 260 | 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' + 261 | 'line-height: 1.5; font-size: 14px;' 262 | }, [ 263 | E('pre', { style: 'margin: 0;' }, content) 264 | ]), 265 | E('div', { 266 | 'class': 'right', 267 | style: 'margin-top: 1em;' 268 | }, [ 269 | E('button', { 270 | 'class': 'btn', 271 | 'click': ev => copyToClipboard('```txt\n' + content + '\n```', ev.target) 272 | }, _('Copy to Clipboard')), 273 | E('button', { 274 | 'class': 'btn', 275 | 'click': ui.hideModal 276 | }, _('Close')) 277 | ]) 278 | ]; 279 | } 280 | 281 | function showConfigModal(command, title) { 282 | // Create and show modal immediately with loading state 283 | const modalContent = E('div', { 'class': 'panel-body' }, [ 284 | E('div', { 285 | 'class': 'panel-body', 286 | style: 'max-height: 70vh; overflow-y: auto; margin: 1em 0; padding: 1.5em; ' + 287 | 'font-family: monospace; white-space: pre-wrap; word-wrap: break-word; ' + 288 | 'line-height: 1.5; font-size: 14px;' 289 | }, [ 290 | E('pre', { 291 | 'id': 'modal-content-pre', 292 | style: 'margin: 0;' 293 | }, _('Loading...')) 294 | ]), 295 | E('div', { 296 | 'class': 'right', 297 | style: 'margin-top: 1em;' 298 | }, [ 299 | E('button', { 300 | 'class': 'btn', 301 | 'id': 'copy-button', 302 | 'click': ev => copyToClipboard('```txt\n' + document.getElementById('modal-content-pre').innerText + '\n```', ev.target) 303 | }, _('Copy to Clipboard')), 304 | E('button', { 305 | 'class': 'btn', 306 | 'click': ui.hideModal 307 | }, _('Close')) 308 | ]) 309 | ]); 310 | 311 | ui.showModal(_(title), modalContent); 312 | 313 | // Function to update modal content 314 | const updateModalContent = (content) => { 315 | const pre = document.getElementById('modal-content-pre'); 316 | if (pre) { 317 | pre.textContent = content; 318 | } 319 | }; 320 | 321 | try { 322 | let formattedOutput = ''; 323 | 324 | if (command === 'global_check') { 325 | safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => { 326 | formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); 327 | 328 | try { 329 | const controller = new AbortController(); 330 | const timeoutId = setTimeout(() => controller.abort(), constants.FETCH_TIMEOUT); 331 | 332 | cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal }) 333 | .then(response => response.json()) 334 | .then(data => { 335 | clearTimeout(timeoutId); 336 | 337 | if (data.fakeip === true) { 338 | formattedOutput += '\n✅ ' + _('FakeIP is working in browser!') + '\n'; 339 | } else { 340 | formattedOutput += '\n❌ ' + _('FakeIP is not working in browser') + '\n'; 341 | formattedOutput += _('Check DNS server on current device (PC, phone)') + '\n'; 342 | formattedOutput += _('Its must be router!') + '\n'; 343 | } 344 | 345 | // Bypass check 346 | cachedFetch(`https://${constants.FAKEIP_CHECK_DOMAIN}/check`, { signal: controller.signal }) 347 | .then(bypassResponse => bypassResponse.json()) 348 | .then(bypassData => { 349 | cachedFetch(`https://${constants.IP_CHECK_DOMAIN}/check`, { signal: controller.signal }) 350 | .then(bypassResponse2 => bypassResponse2.json()) 351 | .then(bypassData2 => { 352 | formattedOutput += '━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'; 353 | 354 | if (bypassData.IP && bypassData2.IP && bypassData.IP !== bypassData2.IP) { 355 | formattedOutput += '✅ ' + _('Proxy working correctly') + '\n'; 356 | formattedOutput += _('Direct IP: ') + maskIP(bypassData.IP) + '\n'; 357 | formattedOutput += _('Proxy IP: ') + maskIP(bypassData2.IP) + '\n'; 358 | } else if (bypassData.IP === bypassData2.IP) { 359 | formattedOutput += '❌ ' + _('Proxy is not working - same IP for both domains') + '\n'; 360 | formattedOutput += _('IP: ') + maskIP(bypassData.IP) + '\n'; 361 | } else { 362 | formattedOutput += '❌ ' + _('Proxy check failed') + '\n'; 363 | } 364 | 365 | updateModalContent(formattedOutput); 366 | }) 367 | .catch(error => { 368 | formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n'; 369 | updateModalContent(formattedOutput); 370 | }); 371 | }) 372 | .catch(error => { 373 | formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n'; 374 | updateModalContent(formattedOutput); 375 | }); 376 | }) 377 | .catch(error => { 378 | formattedOutput += '\n❌ ' + _('Check failed: ') + (error.name === 'AbortError' ? _('timeout') : error.message) + '\n'; 379 | updateModalContent(formattedOutput); 380 | }); 381 | } catch (error) { 382 | formattedOutput += '\n❌ ' + _('Check failed: ') + error.message + '\n'; 383 | updateModalContent(formattedOutput); 384 | } 385 | }); 386 | } else { 387 | safeExec('/usr/bin/podkop', [command], 'P0_PRIORITY', res => { 388 | formattedOutput = formatDiagnosticOutput(res.stdout || _('No output')); 389 | updateModalContent(formattedOutput); 390 | }); 391 | } 392 | } catch (error) { 393 | updateModalContent(_('Error: ') + error.message); 394 | } 395 | } 396 | 397 | // Button Factory 398 | const ButtonFactory = { 399 | createButton: function (config) { 400 | return E('button', { 401 | 'class': `btn ${config.additionalClass || ''}`.trim(), 402 | 'click': config.onClick, 403 | 'style': config.style || '' 404 | }, _(config.label)); 405 | }, 406 | 407 | createActionButton: function (config) { 408 | return this.createButton({ 409 | label: config.label, 410 | additionalClass: `cbi-button-${config.type || ''}`, 411 | onClick: () => safeExec('/usr/bin/podkop', [config.action], 'P0_PRIORITY') 412 | .then(() => config.reload && location.reload()), 413 | style: config.style 414 | }); 415 | }, 416 | 417 | createInitActionButton: function (config) { 418 | return this.createButton({ 419 | label: config.label, 420 | additionalClass: `cbi-button-${config.type || ''}`, 421 | onClick: () => safeExec('/etc/init.d/podkop', [config.action], 'P0_PRIORITY') 422 | .then(() => config.reload && location.reload()), 423 | style: config.style 424 | }); 425 | }, 426 | 427 | createModalButton: function (config) { 428 | return this.createButton({ 429 | label: config.label, 430 | onClick: () => showConfigModal(config.command, config.title), 431 | additionalClass: `cbi-button-${config.type || ''}`, 432 | style: config.style 433 | }); 434 | } 435 | }; 436 | 437 | // Create a loading placeholder for status text 438 | function createLoadingStatusText() { 439 | return E('span', { 'class': 'loading-indicator' }, _('Loading...')); 440 | } 441 | 442 | // Create the status section with buttons loaded immediately but status indicators loading asynchronously 443 | let createStatusSection = async function () { 444 | // Get initial podkop status 445 | let initialPodkopStatus = { enabled: false }; 446 | try { 447 | const result = await fs.exec('/usr/bin/podkop', ['get_status']); 448 | if (result && result.stdout) { 449 | const status = JSON.parse(result.stdout); 450 | initialPodkopStatus.enabled = status.enabled === 1; 451 | } 452 | } catch (e) { 453 | console.error('Error getting initial podkop status:', e); 454 | } 455 | 456 | return E('div', { 'class': 'cbi-section' }, [ 457 | E('div', { 'class': 'table', style: 'display: flex; gap: 20px;' }, [ 458 | // Podkop Status Panel 459 | E('div', { 'id': 'podkop-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [ 460 | E('div', { 'class': 'panel-heading' }, [ 461 | E('strong', {}, _('Podkop Status')), 462 | E('br'), 463 | E('span', { 'id': 'podkop-status-text' }, createLoadingStatusText()) 464 | ]), 465 | E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ 466 | ButtonFactory.createActionButton({ 467 | label: 'Restart Podkop', 468 | type: 'apply', 469 | action: 'restart', 470 | reload: true 471 | }), 472 | ButtonFactory.createActionButton({ 473 | label: 'Stop Podkop', 474 | type: 'apply', 475 | action: 'stop', 476 | reload: true 477 | }), 478 | // Autostart button - create with initial state 479 | ButtonFactory.createInitActionButton({ 480 | label: initialPodkopStatus.enabled ? 'Disable Autostart' : 'Enable Autostart', 481 | type: initialPodkopStatus.enabled ? 'remove' : 'apply', 482 | action: initialPodkopStatus.enabled ? 'disable' : 'enable', 483 | reload: true 484 | }), 485 | ButtonFactory.createModalButton({ 486 | label: _('Global check'), 487 | command: 'global_check', 488 | title: _('Click here for all the info') 489 | }), 490 | ButtonFactory.createModalButton({ 491 | label: 'View Logs', 492 | command: 'check_logs', 493 | title: 'Podkop Logs' 494 | }), 495 | ButtonFactory.createModalButton({ 496 | label: _('Update Lists'), 497 | command: 'list_update', 498 | title: _('Lists Update Results') 499 | }) 500 | ]) 501 | ]), 502 | 503 | // Sing-box Status Panel 504 | E('div', { 'id': 'singbox-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [ 505 | E('div', { 'class': 'panel-heading' }, [ 506 | E('strong', {}, _('Sing-box Status')), 507 | E('br'), 508 | E('span', { 'id': 'singbox-status-text' }, createLoadingStatusText()) 509 | ]), 510 | E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ 511 | ButtonFactory.createModalButton({ 512 | label: 'Show Config', 513 | command: 'show_sing_box_config', 514 | title: 'Sing-box Configuration' 515 | }), 516 | ButtonFactory.createModalButton({ 517 | label: 'View Logs', 518 | command: 'check_sing_box_logs', 519 | title: 'Sing-box Logs' 520 | }), 521 | ButtonFactory.createModalButton({ 522 | label: 'Check Connections', 523 | command: 'check_sing_box_connections', 524 | title: 'Active Connections' 525 | }), 526 | ButtonFactory.createModalButton({ 527 | label: _('Check NFT Rules'), 528 | command: 'check_nft', 529 | title: _('NFT Rules') 530 | }), 531 | ButtonFactory.createModalButton({ 532 | label: _('Check DNSMasq'), 533 | command: 'check_dnsmasq', 534 | title: _('DNSMasq Configuration') 535 | }) 536 | ]) 537 | ]), 538 | 539 | // FakeIP Status Panel 540 | E('div', { 'id': 'fakeip-status-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [ 541 | E('div', { 'class': 'panel-heading' }, [ 542 | E('strong', {}, _('FakeIP Status')) 543 | ]), 544 | E('div', { 'class': 'panel-body', 'style': 'display: flex; flex-direction: column; gap: 8px;' }, [ 545 | E('div', { style: 'margin-bottom: 5px;' }, [ 546 | E('div', {}, [ 547 | E('span', { 'id': 'fakeip-browser-status' }, createLoadingStatusText()) 548 | ]), 549 | E('div', {}, [ 550 | E('span', { 'id': 'fakeip-router-status' }, createLoadingStatusText()) 551 | ]) 552 | ]), 553 | E('div', { style: 'margin-bottom: 5px;' }, [ 554 | E('div', {}, [ 555 | E('strong', {}, _('DNS Status')), 556 | E('br'), 557 | E('span', { 'id': 'dns-remote-status' }, createLoadingStatusText()), 558 | E('br'), 559 | E('span', { 'id': 'dns-local-status' }, createLoadingStatusText()) 560 | ]) 561 | ]), 562 | E('div', { style: 'margin-bottom: 5px;' }, [ 563 | E('div', {}, [ 564 | E('strong', { 'id': 'config-name-text' }, _('Main config')), 565 | E('br'), 566 | E('span', { 'id': 'bypass-status' }, createLoadingStatusText()) 567 | ]) 568 | ]) 569 | ]) 570 | ]), 571 | 572 | // Version Information Panel 573 | E('div', { 'id': 'version-info-panel', 'class': 'panel', 'style': 'flex: 1; padding: 15px;' }, [ 574 | E('div', { 'class': 'panel-heading' }, [ 575 | E('strong', {}, _('Version Information')) 576 | ]), 577 | E('div', { 'class': 'panel-body' }, [ 578 | E('div', { 'style': 'margin-top: 10px; font-family: monospace; white-space: pre-wrap;' }, [ 579 | E('strong', {}, _('Podkop: ')), E('span', { 'id': 'podkop-version' }, _('Loading...')), '\n', 580 | E('strong', {}, _('LuCI App: ')), E('span', { 'id': 'luci-version' }, _('Loading...')), '\n', 581 | E('strong', {}, _('Sing-box: ')), E('span', { 'id': 'singbox-version' }, _('Loading...')), '\n', 582 | E('strong', {}, _('OpenWrt Version: ')), E('span', { 'id': 'openwrt-version' }, _('Loading...')), '\n', 583 | E('strong', {}, _('Device Model: ')), E('span', { 'id': 'device-model' }, _('Loading...')) 584 | ]) 585 | ]) 586 | ]) 587 | ]) 588 | ]); 589 | }; 590 | 591 | // Global variables for tracking state 592 | let diagnosticsUpdateTimer = null; 593 | let isInitialCheck = true; 594 | showConfigModal.busy = false; 595 | 596 | function startDiagnosticsUpdates() { 597 | if (diagnosticsUpdateTimer) { 598 | clearInterval(diagnosticsUpdateTimer); 599 | } 600 | 601 | // Immediately update when started 602 | updateDiagnostics(); 603 | 604 | // Then set up periodic updates 605 | diagnosticsUpdateTimer = setInterval(updateDiagnostics, constants.DIAGNOSTICS_UPDATE_INTERVAL); 606 | } 607 | 608 | function stopDiagnosticsUpdates() { 609 | if (diagnosticsUpdateTimer) { 610 | clearInterval(diagnosticsUpdateTimer); 611 | diagnosticsUpdateTimer = null; 612 | } 613 | } 614 | 615 | // Update individual text element with new content 616 | function updateTextElement(elementId, content) { 617 | const element = document.getElementById(elementId); 618 | if (element) { 619 | element.innerHTML = ''; 620 | element.appendChild(content); 621 | } 622 | } 623 | 624 | async function updateDiagnostics() { 625 | // Podkop Status check 626 | safeExec('/usr/bin/podkop', ['get_status'], 'P0_PRIORITY', result => { 627 | try { 628 | const parsedPodkopStatus = JSON.parse(result.stdout || '{"enabled":0,"status":"error"}'); 629 | 630 | // Update Podkop status text 631 | updateTextElement('podkop-status-text', 632 | E('span', { 633 | 'style': `color: ${parsedPodkopStatus.enabled ? constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR}` 634 | }, [ 635 | parsedPodkopStatus.enabled ? '✔ Autostart enabled' : '✘ Autostart disabled' 636 | ]) 637 | ); 638 | 639 | // Update autostart button 640 | const autostartButton = parsedPodkopStatus.enabled ? 641 | ButtonFactory.createInitActionButton({ 642 | label: 'Disable Autostart', 643 | type: 'remove', 644 | action: 'disable', 645 | reload: true 646 | }) : 647 | ButtonFactory.createInitActionButton({ 648 | label: 'Enable Autostart', 649 | type: 'apply', 650 | action: 'enable', 651 | reload: true 652 | }); 653 | 654 | // Find the autostart button and replace it 655 | const panel = document.getElementById('podkop-status-panel'); 656 | if (panel) { 657 | const buttons = panel.querySelectorAll('.cbi-button'); 658 | if (buttons.length >= 3) { 659 | buttons[2].parentNode.replaceChild(autostartButton, buttons[2]); 660 | } 661 | } 662 | } catch (error) { 663 | updateTextElement('podkop-status-text', 664 | E('span', { 'style': `color: ${constants.STATUS_COLORS.ERROR}` }, '✘ Error') 665 | ); 666 | } 667 | }); 668 | 669 | // Sing-box Status check 670 | safeExec('/usr/bin/podkop', ['get_sing_box_status'], 'P0_PRIORITY', result => { 671 | try { 672 | const parsedSingboxStatus = JSON.parse(result.stdout || '{"running":0,"enabled":0,"status":"error"}'); 673 | 674 | // Update Sing-box status text 675 | updateTextElement('singbox-status-text', 676 | E('span', { 677 | 'style': `color: ${parsedSingboxStatus.running && !parsedSingboxStatus.enabled ? 678 | constants.STATUS_COLORS.SUCCESS : constants.STATUS_COLORS.ERROR}` 679 | }, [ 680 | parsedSingboxStatus.running && !parsedSingboxStatus.enabled ? 681 | '✔ running' : '✘ ' + parsedSingboxStatus.status 682 | ]) 683 | ); 684 | } catch (error) { 685 | updateTextElement('singbox-status-text', 686 | E('span', { 'style': `color: ${constants.STATUS_COLORS.ERROR}` }, '✘ Error') 687 | ); 688 | } 689 | }); 690 | 691 | // Version Information checks 692 | safeExec('/usr/bin/podkop', ['show_version'], 'P2_PRIORITY', result => { 693 | updateTextElement('podkop-version', 694 | document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown')) 695 | ); 696 | }); 697 | 698 | safeExec('/usr/bin/podkop', ['show_luci_version'], 'P2_PRIORITY', result => { 699 | updateTextElement('luci-version', 700 | document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown')) 701 | ); 702 | }); 703 | 704 | safeExec('/usr/bin/podkop', ['show_sing_box_version'], 'P2_PRIORITY', result => { 705 | updateTextElement('singbox-version', 706 | document.createTextNode(result.stdout ? result.stdout.trim() : _('Unknown')) 707 | ); 708 | }); 709 | 710 | safeExec('/usr/bin/podkop', ['show_system_info'], 'P2_PRIORITY', result => { 711 | if (result.stdout) { 712 | updateTextElement('openwrt-version', 713 | document.createTextNode(result.stdout.split('\n')[1].trim()) 714 | ); 715 | updateTextElement('device-model', 716 | document.createTextNode(result.stdout.split('\n')[4].trim()) 717 | ); 718 | } else { 719 | updateTextElement('openwrt-version', document.createTextNode(_('Unknown'))); 720 | updateTextElement('device-model', document.createTextNode(_('Unknown'))); 721 | } 722 | }); 723 | 724 | // FakeIP and DNS status checks 725 | runCheck(checkFakeIP, 'P3_PRIORITY', result => { 726 | updateTextElement('fakeip-browser-status', 727 | E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [ 728 | result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ', 729 | result.error ? 'check error' : result.state === 'working' ? _('works in browser') : _('not works in browser') 730 | ]) 731 | ); 732 | }); 733 | 734 | runCheck(checkFakeIPCLI, 'P8_PRIORITY', result => { 735 | updateTextElement('fakeip-router-status', 736 | E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [ 737 | result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ', 738 | result.error ? 'check error' : result.state === 'working' ? _('works on router') : _('not works on router') 739 | ]) 740 | ); 741 | }); 742 | 743 | runCheck(checkDNSAvailability, 'P4_PRIORITY', result => { 744 | if (result.error) { 745 | updateTextElement('dns-remote-status', 746 | E('span', { style: `color: ${constants.STATUS_COLORS.WARNING}` }, '! DNS check error') 747 | ); 748 | updateTextElement('dns-local-status', 749 | E('span', { style: `color: ${constants.STATUS_COLORS.WARNING}` }, '! DNS check error') 750 | ); 751 | } else { 752 | updateTextElement('dns-remote-status', 753 | E('span', { style: `color: ${result.remote.color}` }, [ 754 | result.remote.state === 'available' ? '✔ ' : result.remote.state === 'unavailable' ? '✘ ' : '! ', 755 | result.remote.message 756 | ]) 757 | ); 758 | 759 | updateTextElement('dns-local-status', 760 | E('span', { style: `color: ${result.local.color}` }, [ 761 | result.local.state === 'available' ? '✔ ' : result.local.state === 'unavailable' ? '✘ ' : '! ', 762 | result.local.message 763 | ]) 764 | ); 765 | } 766 | }); 767 | 768 | runCheck(checkBypass, 'P1_PRIORITY', result => { 769 | updateTextElement('bypass-status', 770 | E('span', { style: `color: ${result.error ? constants.STATUS_COLORS.WARNING : result.color}` }, [ 771 | result.error ? '! ' : result.state === 'working' ? '✔ ' : result.state === 'not_working' ? '✘ ' : '! ', 772 | result.error ? 'check error' : result.message 773 | ]) 774 | ); 775 | }, 'P1_PRIORITY'); 776 | 777 | // Config name 778 | runAsyncTask(async () => { 779 | try { 780 | let configName = _('Main config'); 781 | const data = await uci.load('podkop'); 782 | const proxyString = uci.get('podkop', 'main', 'proxy_string'); 783 | 784 | if (proxyString) { 785 | const activeConfig = proxyString.split('\n') 786 | .map(line => line.trim()) 787 | .find(line => line && !line.startsWith('//')); 788 | 789 | if (activeConfig) { 790 | if (activeConfig.includes('#')) { 791 | const label = activeConfig.split('#').pop(); 792 | if (label && label.trim()) { 793 | configName = _('Config: ') + decodeURIComponent(label); 794 | } 795 | } 796 | } 797 | } 798 | 799 | updateTextElement('config-name-text', document.createTextNode(configName)); 800 | } catch (e) { 801 | console.error('Error getting config name from UCI:', e); 802 | } 803 | }, 'P1_PRIORITY'); 804 | } 805 | 806 | function createDiagnosticsSection(mainSection) { 807 | let o = mainSection.tab('diagnostics', _('Diagnostics')); 808 | 809 | o = mainSection.taboption('diagnostics', form.DummyValue, '_status'); 810 | o.rawhtml = true; 811 | o.cfgvalue = () => E('div', { 812 | id: 'diagnostics-status', 813 | 'data-loading': 'true' 814 | }); 815 | } 816 | 817 | function setupDiagnosticsEventHandlers(node) { 818 | const titleDiv = E('h2', { 'class': 'cbi-map-title' }, _('Podkop')); 819 | node.insertBefore(titleDiv, node.firstChild); 820 | 821 | // Function to initialize diagnostics 822 | function initDiagnostics(container) { 823 | if (container && container.hasAttribute('data-loading')) { 824 | container.innerHTML = ''; 825 | showConfigModal.busy = false; 826 | createStatusSection().then(section => { 827 | container.appendChild(section); 828 | startDiagnosticsUpdates(); 829 | // Start error polling when diagnostics tab is active 830 | utils.startErrorPolling(); 831 | }); 832 | } 833 | } 834 | 835 | document.addEventListener('visibilitychange', function () { 836 | const diagnosticsContainer = document.getElementById('diagnostics-status'); 837 | const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]'); 838 | 839 | if (document.hidden || !diagnosticsTab || !diagnosticsTab.classList.contains('cbi-tab-active')) { 840 | stopDiagnosticsUpdates(); 841 | // Don't stop error polling here - it's managed in podkop.js for all tabs 842 | } else if (diagnosticsContainer && diagnosticsContainer.hasAttribute('data-loading')) { 843 | startDiagnosticsUpdates(); 844 | // Ensure error polling is running when diagnostics tab is active 845 | utils.startErrorPolling(); 846 | } 847 | }); 848 | 849 | setTimeout(() => { 850 | const diagnosticsContainer = document.getElementById('diagnostics-status'); 851 | const diagnosticsTab = document.querySelector('.cbi-tab[data-tab="diagnostics"]'); 852 | const otherTabs = document.querySelectorAll('.cbi-tab:not([data-tab="diagnostics"])'); 853 | 854 | // Check for direct page load case 855 | const noActiveTabsExist = !Array.from(otherTabs).some(tab => tab.classList.contains('cbi-tab-active')); 856 | 857 | if (diagnosticsContainer && diagnosticsTab && (diagnosticsTab.classList.contains('cbi-tab-active') || noActiveTabsExist)) { 858 | initDiagnostics(diagnosticsContainer); 859 | } 860 | 861 | const tabs = node.querySelectorAll('.cbi-tabmenu'); 862 | if (tabs.length > 0) { 863 | tabs[0].addEventListener('click', function (e) { 864 | const tab = e.target.closest('.cbi-tab'); 865 | if (tab) { 866 | const tabName = tab.getAttribute('data-tab'); 867 | if (tabName === 'diagnostics') { 868 | const container = document.getElementById('diagnostics-status'); 869 | container.setAttribute('data-loading', 'true'); 870 | initDiagnostics(container); 871 | } else { 872 | stopDiagnosticsUpdates(); 873 | // Don't stop error polling - it should continue on all tabs 874 | } 875 | } 876 | }); 877 | } 878 | }, constants.DIAGNOSTICS_INITIAL_DELAY); 879 | 880 | node.classList.add('fade-in'); 881 | return node; 882 | } 883 | 884 | return baseclass.extend({ 885 | createDiagnosticsSection, 886 | setupDiagnosticsEventHandlers 887 | }); -------------------------------------------------------------------------------- /luci-app-podkop/htdocs/luci-static/resources/view/podkop/podkop.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require view'; 3 | 'require form'; 4 | 'require network'; 5 | 'require view.podkop.configSection as configSection'; 6 | 'require view.podkop.diagnosticTab as diagnosticTab'; 7 | 'require view.podkop.additionalTab as additionalTab'; 8 | 'require view.podkop.utils as utils'; 9 | 10 | return view.extend({ 11 | async render() { 12 | document.head.insertAdjacentHTML('beforeend', ` 13 | 34 | `); 35 | 36 | const m = new form.Map('podkop', _(''), null, ['main', 'extra']); 37 | 38 | // Main Section 39 | const mainSection = m.section(form.TypedSection, 'main'); 40 | mainSection.anonymous = true; 41 | configSection.createConfigSection(mainSection, m, network); 42 | 43 | // Additional Settings Tab (main section) 44 | additionalTab.createAdditionalSection(mainSection, network); 45 | 46 | // Diagnostics Tab (main section) 47 | diagnosticTab.createDiagnosticsSection(mainSection); 48 | const map_promise = m.render().then(node => { 49 | // Set up diagnostics event handlers 50 | diagnosticTab.setupDiagnosticsEventHandlers(node); 51 | 52 | // Start critical error polling for all tabs 53 | utils.startErrorPolling(); 54 | 55 | // Add event listener to keep error polling active when switching tabs 56 | const tabs = node.querySelectorAll('.cbi-tabmenu'); 57 | if (tabs.length > 0) { 58 | tabs[0].addEventListener('click', function (e) { 59 | const tab = e.target.closest('.cbi-tab'); 60 | if (tab) { 61 | // Ensure error polling continues when switching tabs 62 | utils.startErrorPolling(); 63 | } 64 | }); 65 | } 66 | 67 | // Add visibility change handler to manage error polling 68 | document.addEventListener('visibilitychange', function () { 69 | if (document.hidden) { 70 | utils.stopErrorPolling(); 71 | } else { 72 | utils.startErrorPolling(); 73 | } 74 | }); 75 | 76 | return node; 77 | }); 78 | 79 | // Extra Section 80 | const extraSection = m.section(form.TypedSection, 'extra', _('Extra configurations')); 81 | extraSection.anonymous = false; 82 | extraSection.addremove = true; 83 | extraSection.addbtntitle = _('Add Section'); 84 | extraSection.multiple = true; 85 | configSection.createConfigSection(extraSection, m, network); 86 | 87 | return map_promise; 88 | } 89 | }); -------------------------------------------------------------------------------- /luci-app-podkop/htdocs/luci-static/resources/view/podkop/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require baseclass'; 3 | 'require ui'; 4 | 'require fs'; 5 | 'require view.podkop.constants as constants'; 6 | 7 | // Flag to track if this is the first error check 8 | let isInitialCheck = true; 9 | 10 | // Set to track which errors we've already seen 11 | const lastErrorsSet = new Set(); 12 | 13 | // Timer for periodic error polling 14 | let errorPollTimer = null; 15 | 16 | // Helper function to fetch errors from the podkop command 17 | async function getPodkopErrors() { 18 | return new Promise(resolve => { 19 | safeExec('/usr/bin/podkop', ['check_logs'], 'P0_PRIORITY', result => { 20 | if (!result || !result.stdout) return resolve([]); 21 | 22 | const logs = result.stdout.split('\n'); 23 | const errors = logs.filter(log => 24 | log.includes('[critical]') 25 | ); 26 | 27 | resolve(errors); 28 | }); 29 | }); 30 | } 31 | 32 | // Show error notification to the user 33 | function showErrorNotification(error, isMultiple = false) { 34 | const notificationContent = E('div', { 'class': 'alert-message error' }, [ 35 | E('pre', { 'class': 'error-log' }, error) 36 | ]); 37 | 38 | ui.addNotification(null, notificationContent); 39 | } 40 | 41 | // Helper function for command execution with prioritization 42 | function safeExec(command, args, priority, callback, timeout = constants.COMMAND_TIMEOUT) { 43 | // Default to highest priority execution if priority is not provided or invalid 44 | let schedulingDelay = constants.COMMAND_SCHEDULING.P0_PRIORITY; 45 | 46 | // If priority is a string, try to get the corresponding delay value 47 | if (typeof priority === 'string' && constants.COMMAND_SCHEDULING[priority] !== undefined) { 48 | schedulingDelay = constants.COMMAND_SCHEDULING[priority]; 49 | } 50 | 51 | const executeCommand = async () => { 52 | try { 53 | const controller = new AbortController(); 54 | const timeoutId = setTimeout(() => controller.abort(), timeout); 55 | 56 | const result = await Promise.race([ 57 | fs.exec(command, args), 58 | new Promise((_, reject) => { 59 | controller.signal.addEventListener('abort', () => { 60 | reject(new Error('Command execution timed out')); 61 | }); 62 | }) 63 | ]); 64 | 65 | clearTimeout(timeoutId); 66 | 67 | if (callback && typeof callback === 'function') { 68 | callback(result); 69 | } 70 | 71 | return result; 72 | } catch (error) { 73 | console.warn(`Command execution failed or timed out: ${command} ${args.join(' ')}`); 74 | const errorResult = { stdout: '', stderr: error.message, error: error }; 75 | 76 | if (callback && typeof callback === 'function') { 77 | callback(errorResult); 78 | } 79 | 80 | return errorResult; 81 | } 82 | }; 83 | 84 | if (callback && typeof callback === 'function') { 85 | setTimeout(executeCommand, schedulingDelay); 86 | return; 87 | } 88 | else { 89 | return executeCommand(); 90 | } 91 | } 92 | 93 | // Check for critical errors and show notifications 94 | async function checkForCriticalErrors() { 95 | try { 96 | const errors = await getPodkopErrors(); 97 | 98 | if (errors && errors.length > 0) { 99 | // Filter out errors we've already seen 100 | const newErrors = errors.filter(error => !lastErrorsSet.has(error)); 101 | 102 | if (newErrors.length > 0) { 103 | // On initial check, just store errors without showing notifications 104 | if (!isInitialCheck) { 105 | // Show each new error as a notification 106 | newErrors.forEach(error => { 107 | showErrorNotification(error, newErrors.length > 1); 108 | }); 109 | } 110 | 111 | // Add new errors to our set of seen errors 112 | newErrors.forEach(error => lastErrorsSet.add(error)); 113 | } 114 | } 115 | 116 | // After first check, mark as no longer initial 117 | isInitialCheck = false; 118 | } catch (error) { 119 | console.error('Error checking for critical messages:', error); 120 | } 121 | } 122 | 123 | // Start polling for errors at regular intervals 124 | function startErrorPolling() { 125 | if (errorPollTimer) { 126 | clearInterval(errorPollTimer); 127 | } 128 | 129 | // Reset initial check flag to make sure we show errors 130 | isInitialCheck = false; 131 | 132 | // Immediately check for errors on start 133 | checkForCriticalErrors(); 134 | 135 | // Then set up periodic checks 136 | errorPollTimer = setInterval(checkForCriticalErrors, constants.ERROR_POLL_INTERVAL); 137 | } 138 | 139 | // Stop polling for errors 140 | function stopErrorPolling() { 141 | if (errorPollTimer) { 142 | clearInterval(errorPollTimer); 143 | errorPollTimer = null; 144 | } 145 | } 146 | 147 | return baseclass.extend({ 148 | startErrorPolling, 149 | stopErrorPolling, 150 | checkForCriticalErrors, 151 | safeExec 152 | }); -------------------------------------------------------------------------------- /luci-app-podkop/po/ru/podkop.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "Content-Type: text/plain; charset=UTF-8" 3 | 4 | msgid "Podkop configuration" 5 | msgstr "Настройка Podkop" 6 | 7 | msgid "Basic Settings" 8 | msgstr "Основные настройки" 9 | 10 | msgid "Additional Settings" 11 | msgstr "Дополнительные настройки" 12 | 13 | msgid "Secondary Config" 14 | msgstr "Второй маршрут" 15 | 16 | msgid "Secondary VPN/Proxy Enable" 17 | msgstr "Включить второй VPN/Proxy" 18 | 19 | msgid "Enable secondary VPN/Proxy configuration" 20 | msgstr "Включить конфигурацию второго VPN/Proxy" 21 | 22 | msgid "Connection Type" 23 | msgstr "Тип подключения" 24 | 25 | msgid "Select between VPN and Proxy connection methods for traffic routing" 26 | msgstr "Выберите между VPN и Proxy методами для маршрутизации трафика" 27 | 28 | msgid "Configuration Type" 29 | msgstr "Тип конфигурации" 30 | 31 | msgid "Select how to configure the proxy" 32 | msgstr "Выберите способ настройки прокси" 33 | 34 | msgid "Connection URL" 35 | msgstr "URL подключения" 36 | 37 | msgid "Outbound Config" 38 | msgstr "Конфигурация Outbound" 39 | 40 | msgid "Proxy Configuration URL" 41 | msgstr "URL конфигурации прокси" 42 | 43 | msgid "Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for saving other configs" 44 | msgstr "Введите строку подключения, начинающуюся с vless:// или ss:// для настройки прокси. Добавляйте комментарии с // для сохранения других конфигураций" 45 | 46 | msgid "Outbound Configuration" 47 | msgstr "Конфигурация исходящего соединения" 48 | 49 | msgid "Enter complete outbound configuration in JSON format" 50 | msgstr "Введите полную конфигурацию исходящего соединения в формате JSON" 51 | 52 | msgid "Network Interface" 53 | msgstr "Сетевой интерфейс" 54 | 55 | msgid "Select network interface for VPN connection" 56 | msgstr "Выберите сетевой интерфейс для VPN подключения" 57 | 58 | msgid "Community Lists" 59 | msgstr "Предустановленные списки" 60 | 61 | msgid "Service List" 62 | msgstr "Список сервисов" 63 | 64 | msgid "Select predefined service for routing" 65 | msgstr "Выберите предустановленные сервисы для маршрутизации" 66 | 67 | msgid "User Domain List Type" 68 | msgstr "Тип пользовательского списка доменов" 69 | 70 | msgid "Select how to add your custom domains" 71 | msgstr "Выберите способ добавления пользовательских доменов" 72 | 73 | msgid "Disabled" 74 | msgstr "Отключено" 75 | 76 | msgid "Dynamic List" 77 | msgstr "Динамический список" 78 | 79 | msgid "Text List" 80 | msgstr "Текстовый список" 81 | 82 | msgid "User Domains" 83 | msgstr "Пользовательские домены" 84 | 85 | msgid "Enter domain names without protocols (example: sub.example.com or example.com)" 86 | msgstr "Введите имена доменов без протоколов (пример: sub.example.com или example.com)" 87 | 88 | msgid "User Domains List" 89 | msgstr "Список пользовательских доменов" 90 | 91 | msgid "Enter domain names separated by comma, space or newline. You can add comments after //" 92 | msgstr "Введите имена доменов, разделяя их запятой, пробелом или с новой строки. Вы можете добавлять комментарии после //" 93 | 94 | msgid "Local Domain Lists" 95 | msgstr "Локальные списки доменов" 96 | 97 | msgid "Use the list from the router filesystem" 98 | msgstr "Использовать список из файловой системы роутера" 99 | 100 | msgid "Local Domain Lists Path" 101 | msgstr "Путь к локальным спискам доменов" 102 | 103 | msgid "Enter to the list file path" 104 | msgstr "Введите путь к файлу списка" 105 | 106 | msgid "Remote Domain Lists" 107 | msgstr "Удаленные списки доменов" 108 | 109 | msgid "Download and use domain lists from remote URLs" 110 | msgstr "Загрузка и использование списков доменов с удаленных URL" 111 | 112 | msgid "Remote Domain URLs" 113 | msgstr "URL удаленных доменов" 114 | 115 | msgid "Enter full URLs starting with http:// or https://" 116 | msgstr "Введите полные URL, начинающиеся с http:// или https://" 117 | 118 | msgid "User Subnet List Type" 119 | msgstr "Тип пользовательского списка подсетей" 120 | 121 | msgid "Select how to add your custom subnets" 122 | msgstr "Выберите способ добавления пользовательских подсетей" 123 | 124 | msgid "Text List (comma/space/newline separated)" 125 | msgstr "Текстовый список (разделенный запятыми/пробелами/новыми строками)" 126 | 127 | msgid "User Subnets" 128 | msgstr "Пользовательские подсети" 129 | 130 | msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses" 131 | msgstr "Введите подсети в нотации CIDR (пример: 103.21.244.0/22) или отдельные IP-адреса" 132 | 133 | msgid "User Subnets List" 134 | msgstr "Список пользовательских подсетей" 135 | 136 | msgid "Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline" 137 | msgstr "Введите подсети в нотации CIDR или отдельные IP-адреса через запятую, пробел или новую строку" 138 | 139 | msgid "Remote Subnet Lists" 140 | msgstr "Удаленные списки подсетей" 141 | 142 | msgid "Download and use subnet lists from remote URLs" 143 | msgstr "Загрузка и использование списков подсетей с удаленных URL" 144 | 145 | msgid "Remote Subnet URLs" 146 | msgstr "URL удаленных подсетей" 147 | 148 | msgid "IP for full redirection" 149 | msgstr "Принудительные прокси IP" 150 | 151 | msgid "Specify local IP addresses whose traffic will always use the configured route" 152 | msgstr "Укажите локальные IP-адреса, трафик которых всегда будет использовать настроенный маршрут" 153 | 154 | msgid "Local IPs" 155 | msgstr "Локальные IP" 156 | 157 | msgid "Enter valid IPv4 addresses" 158 | msgstr "Введите действительные IPv4 адреса" 159 | 160 | msgid "IP for exclusion" 161 | msgstr "Исключения прокси IP" 162 | 163 | msgid "Specify local IP addresses that will never use the configured route" 164 | msgstr "Укажите локальные IP-адреса, которые никогда не будут использовать настроенный маршрут" 165 | 166 | msgid "Mixed enable" 167 | msgstr "Включить смешанный режим" 168 | 169 | msgid "Browser port: 2080" 170 | msgstr "Порт браузера: 2080" 171 | 172 | msgid "Yacd enable" 173 | msgstr "Включить Yacd" 174 | 175 | msgid "Exclude NTP" 176 | msgstr "Исключить NTP" 177 | 178 | msgid "For issues with open connections sing-box" 179 | msgstr "Для проблем с открытыми соединениями sing-box" 180 | 181 | msgid "QUIC disable" 182 | msgstr "Отключить QUIC" 183 | 184 | msgid "For issues with the video stream" 185 | msgstr "Для проблем с видеопотоком" 186 | 187 | msgid "List Update Frequency" 188 | msgstr "Частота обновления списков" 189 | 190 | msgid "Select how often the lists will be updated" 191 | msgstr "Выберите, как часто будут обновляться списки" 192 | 193 | msgid "Every hour" 194 | msgstr "Каждый час" 195 | 196 | msgid "Every 2 hours" 197 | msgstr "Каждые 2 часа" 198 | 199 | msgid "Every 3 hours" 200 | msgstr "Каждые 3 часа" 201 | 202 | msgid "Every 4 hours" 203 | msgstr "Каждые 4 часа" 204 | 205 | msgid "Every 6 hours" 206 | msgstr "Каждые 6 часов" 207 | 208 | msgid "Every 12 hours" 209 | msgstr "Каждые 12 часов" 210 | 211 | msgid "Every day" 212 | msgstr "Каждый день" 213 | 214 | msgid "Every 3 days" 215 | msgstr "Каждые 3 дня" 216 | 217 | msgid "Once a day at 04:00" 218 | msgstr "Раз в день в 04:00" 219 | 220 | msgid "Once a week on Sunday at 04:00" 221 | msgstr "Раз в неделю в воскресенье в 04:00" 222 | 223 | msgid "Invalid domain format. Enter domain without protocol (example: sub.example.com)" 224 | msgstr "Неверный формат домена. Введите домен без протокола (пример: sub.example.com)" 225 | 226 | msgid "URL must use http:// or https:// protocol" 227 | msgstr "URL должен использовать протокол http:// или https://" 228 | 229 | msgid "Invalid URL format. URL must start with http:// or https://" 230 | msgstr "Неверный формат URL. URL должен начинаться с http:// или https://" 231 | 232 | msgid "Invalid format. Use format: X.X.X.X or X.X.X.X/Y" 233 | msgstr "Неверный формат. Используйте формат: X.X.X.X или X.X.X.X/Y" 234 | 235 | msgid "IP address parts must be between 0 and 255" 236 | msgstr "Части IP-адреса должны быть между 0 и 255" 237 | 238 | msgid "CIDR must be between 0 and 32" 239 | msgstr "CIDR должен быть между 0 и 32" 240 | 241 | msgid "Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)" 242 | msgstr "Неверный формат IP. Используйте формат: X.X.X.X (например: 192.168.1.1)" 243 | 244 | msgid "Invalid domain format: %s. Enter domain without protocol" 245 | msgstr "Неверный формат домена: %s. Введите домен без протокола" 246 | 247 | msgid "Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y" 248 | msgstr "Неверный формат: %s. Используйте формат: X.X.X.X или X.X.X.X/Y" 249 | 250 | msgid "IP parts must be between 0 and 255 in: %s" 251 | msgstr "Части IP-адреса должны быть между 0 и 255 в: %s" 252 | 253 | msgid "CIDR must be between 0 and 32 in: %s" 254 | msgstr "CIDR должен быть между 0 и 32 в: %s" 255 | 256 | msgid "Invalid path format. Path must start with \"/\" and contain only valid characters (letters, numbers, \"-\", \"_\", \"/\", \".\")" 257 | msgstr "Неверный формат пути. Путь должен начинаться с \"/\" и содержать только допустимые символы (буквы, цифры, \"-\", \"_\", \"/\", \".\")" 258 | 259 | msgid "Invalid path format" 260 | msgstr "Неверный формат пути" 261 | 262 | msgid "JSON must contain at least type, server and server_port fields" 263 | msgstr "JSON должен содержать как минимум поля type, server и server_port" 264 | 265 | msgid "Invalid JSON format" 266 | msgstr "Неверный формат JSON" 267 | 268 | msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." 269 | msgstr "Предупреждение: %s нельзя использовать вместе с %s. Предыдущие варианты были удалены." 270 | 271 | msgid "Regional options cannot be used together" 272 | msgstr "Нельзя использовать несколько региональных опций" 273 | 274 | msgid "Warning: Russia inside can only be used with Meta, Twitter, Discord, and Telegram. %s already in Russia inside and have been removed from selection." 275 | msgstr "Внимание: Russia inside может использоваться только с Meta, Twitter, Discord и Telegram. %s были удалены из выбора." 276 | 277 | msgid "Russia inside restrictions" 278 | msgstr "Ограничения Russia inside" 279 | 280 | msgid "URL must start with vless:// or ss://" 281 | msgstr "URL должен начинаться с vless:// или ss://" 282 | 283 | msgid "Invalid Shadowsocks URL format: missing method and password separator \":\"" 284 | msgstr "Неверный формат URL Shadowsocks: отсутствует разделитель метода и пароля \":\"" 285 | 286 | msgid "Invalid Shadowsocks URL format" 287 | msgstr "Неверный формат URL Shadowsocks" 288 | 289 | msgid "Invalid Shadowsocks URL: missing server address" 290 | msgstr "Неверный URL Shadowsocks: отсутствует адрес сервера" 291 | 292 | msgid "Invalid Shadowsocks URL: missing server" 293 | msgstr "Неверный URL Shadowsocks: отсутствует сервер" 294 | 295 | msgid "Invalid Shadowsocks URL: missing port" 296 | msgstr "Неверный URL Shadowsocks: отсутствует порт" 297 | 298 | msgid "Invalid port number. Must be between 1 and 65535" 299 | msgstr "Неверный номер порта. Должен быть между 1 и 65535" 300 | 301 | msgid "Invalid Shadowsocks URL: missing or invalid server/port format" 302 | msgstr "Неверный URL Shadowsocks: отсутствует или неверный формат сервера/порта" 303 | 304 | msgid "Invalid VLESS URL: missing UUID" 305 | msgstr "Неверный URL VLESS: отсутствует UUID" 306 | 307 | msgid "Invalid VLESS URL: missing server address" 308 | msgstr "Неверный URL VLESS: отсутствует адрес сервера" 309 | 310 | msgid "Invalid VLESS URL: missing server" 311 | msgstr "Неверный URL VLESS: отсутствует сервер" 312 | 313 | msgid "Invalid VLESS URL: missing port" 314 | msgstr "Неверный URL VLESS: отсутствует порт" 315 | 316 | msgid "Invalid VLESS URL: missing or invalid server/port format" 317 | msgstr "Неверный URL VLESS: отсутствует или неверный формат сервера/порта" 318 | 319 | msgid "Invalid VLESS URL: missing query parameters" 320 | msgstr "Неверный URL VLESS: отсутствуют параметры запроса" 321 | 322 | msgid "Invalid VLESS URL: missing type parameter" 323 | msgstr "Неверный URL VLESS: отсутствует параметр type" 324 | 325 | msgid "Invalid VLESS URL: missing security parameter" 326 | msgstr "Неверный URL VLESS: отсутствует параметр security" 327 | 328 | msgid "Invalid VLESS URL: missing pbk parameter for reality security" 329 | msgstr "Неверный URL VLESS: отсутствует параметр pbk для security reality" 330 | 331 | msgid "Invalid VLESS URL: missing fp parameter for reality security" 332 | msgstr "Неверный URL VLESS: отсутствует параметр fp для security reality" 333 | 334 | msgid "Invalid VLESS URL: missing sni parameter for tls security" 335 | msgstr "Неверный URL VLESS: отсутствует параметр sni для security tls" 336 | 337 | msgid "Invalid URL format: %s" 338 | msgstr "Неверный формат URL: %s" 339 | 340 | msgid "Remote Domain Lists URL" 341 | msgstr "URL удаленных списков доменов" 342 | 343 | msgid "Enter URL to download domain list" 344 | msgstr "Введите URL для загрузки списка доменов" 345 | 346 | msgid "Update Interval" 347 | msgstr "Интервал обновления" 348 | 349 | msgid "Select how often to update the lists" 350 | msgstr "Выберите, как часто обновлять списки" 351 | 352 | msgid "Last Update" 353 | msgstr "Последнее обновление" 354 | 355 | msgid "Last update time" 356 | msgstr "Время последнего обновления" 357 | 358 | msgid "Next Update" 359 | msgstr "Следующее обновление" 360 | 361 | msgid "Next scheduled update time" 362 | msgstr "Время следующего запланированного обновления" 363 | 364 | msgid "Version" 365 | msgstr "Версия" 366 | 367 | msgid "Component version" 368 | msgstr "Версия компонента" 369 | 370 | msgid "Installed" 371 | msgstr "Установлено" 372 | 373 | msgid "Not installed" 374 | msgstr "Не установлено" 375 | 376 | msgid "Unknown version" 377 | msgstr "Неизвестная версия" 378 | 379 | msgid "Error parsing version" 380 | msgstr "Ошибка разбора версии" 381 | 382 | msgid "Error parsing status" 383 | msgstr "Ошибка разбора статуса" 384 | 385 | msgid "Service is running" 386 | msgstr "Сервис запущен" 387 | 388 | msgid "Service is stopped" 389 | msgstr "Сервис остановлен" 390 | 391 | msgid "Service is enabled" 392 | msgstr "Сервис включен" 393 | 394 | msgid "Service is disabled" 395 | msgstr "Сервис отключен" 396 | 397 | msgid "Service Status" 398 | msgstr "Статус сервиса" 399 | 400 | msgid "working" 401 | msgstr "работает" 402 | 403 | msgid "not working" 404 | msgstr "не работает" 405 | 406 | msgid "check error" 407 | msgstr "ошибка проверки" 408 | 409 | msgid "Diagnostic check in progress..." 410 | msgstr "Выполняется диагностическая проверка..." 411 | 412 | msgid "Diagnostic check completed" 413 | msgstr "Диагностическая проверка завершена" 414 | 415 | msgid "Diagnostic check failed" 416 | msgstr "Диагностическая проверка не удалась" 417 | 418 | msgid "Update in progress..." 419 | msgstr "Выполняется обновление..." 420 | 421 | msgid "Update completed" 422 | msgstr "Обновление завершено" 423 | 424 | msgid "Update failed" 425 | msgstr "Обновление не удалось" 426 | 427 | msgid "Check in progress..." 428 | msgstr "Выполняется проверка..." 429 | 430 | msgid "Check completed" 431 | msgstr "Проверка завершена" 432 | 433 | msgid "Check failed" 434 | msgstr "Проверка не удалась" 435 | 436 | msgid "Version Information" 437 | msgstr "Информация о версии" 438 | 439 | msgid "Copied!" 440 | msgstr "Скопировано!" 441 | 442 | msgid "Podkop Status" 443 | msgstr "Статус Podkop" 444 | 445 | msgid "Start Podkop" 446 | msgstr "Запустить Podkop" 447 | 448 | msgid "Stop Podkop" 449 | msgstr "Остановить Podkop" 450 | 451 | msgid "Restart Podkop" 452 | msgstr "Перезапустить Podkop" 453 | 454 | msgid "Enable Podkop" 455 | msgstr "Включить Podkop" 456 | 457 | msgid "Disable Podkop" 458 | msgstr "Отключить Podkop" 459 | 460 | msgid "Loading diagnostics..." 461 | msgstr "Загрузка диагностики..." 462 | 463 | msgid "Error loading diagnostics" 464 | msgstr "Ошибка загрузки диагностики" 465 | 466 | msgid "Sing-box Status" 467 | msgstr "Статус Sing-box" 468 | 469 | msgid "Diagnostic Tools" 470 | msgstr "Инструменты диагностики" 471 | 472 | msgid "Unknown" 473 | msgstr "Неизвестно" 474 | 475 | msgid "Device Model: " 476 | msgstr "Модель устройства: " 477 | 478 | msgid "OpenWrt Version: " 479 | msgstr "Версия OpenWrt: " 480 | 481 | msgid "Sing-box: " 482 | msgstr "Sing-box: " 483 | 484 | msgid "LuCI App: " 485 | msgstr "LuCI App: " 486 | 487 | msgid "Podkop: " 488 | msgstr "Podkop: " 489 | 490 | msgid "Check NFT Rules" 491 | msgstr "Проверить правила NFT" 492 | 493 | msgid "Update Lists" 494 | msgstr "Обновить списки" 495 | 496 | msgid "Lists Update Results" 497 | msgstr "Результаты обновления списков" 498 | 499 | msgid "DNS Protocol Type" 500 | msgstr "Тип DNS протокола" 501 | 502 | msgid "Select DNS protocol to use" 503 | msgstr "Выберите протокол DNS" 504 | 505 | msgid "DNS over HTTPS (DoH)" 506 | msgstr "DNS через HTTPS (DoH)" 507 | 508 | msgid "DNS over TLS (DoT)" 509 | msgstr "DNS через TLS (DoT)" 510 | 511 | msgid "UDP (Unprotected DNS)" 512 | msgstr "UDP (Незащищённый DNS)" 513 | 514 | msgid "DNS Server" 515 | msgstr "DNS сервер" 516 | 517 | msgid "Select or enter DNS server address" 518 | msgstr "Выберите или введите адрес DNS сервера" 519 | 520 | msgid "DNS server address cannot be empty" 521 | msgstr "Адрес DNS сервера не может быть пустым" 522 | 523 | msgid "Invalid DNS server format. Examples: 8.8.8.8 or dns.example.com" 524 | msgstr "Неверный формат DNS сервера. Примеры: 8.8.8.8 или dns.example.com" 525 | 526 | msgid "DNS Rewrite TTL" 527 | msgstr "Перезапись TTL для DNS" 528 | 529 | msgid "Time in seconds for DNS record caching (default: 600)" 530 | msgstr "Время в секундах для кэширования DNS записей (по умолчанию: 600)" 531 | 532 | msgid "TTL value cannot be empty" 533 | msgstr "Значение TTL не может быть пустым" 534 | 535 | msgid "TTL must be a positive number" 536 | msgstr "TTL должно быть положительным числом" 537 | 538 | msgid "Cache File Path" 539 | msgstr "Путь к файлу кэша" 540 | 541 | msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" 542 | msgstr "Выберите или введите путь к файлу кэша sing-box. Меняйте ТОЛЬКО если знаете, что делаете" 543 | 544 | msgid "Cache file path cannot be empty" 545 | msgstr "Путь к файлу кэша не может быть пустым" 546 | 547 | msgid "Path must be absolute (start with /)" 548 | msgstr "Путь должен быть абсолютным (начинаться с /)" 549 | 550 | msgid "Path must end with cache.db" 551 | msgstr "Путь должен заканчиваться на cache.db" 552 | 553 | msgid "Path must contain at least one directory (like /tmp/cache.db)" 554 | msgstr "Путь должен содержать хотя бы одну директорию (например /tmp/cache.db)" 555 | 556 | msgid "Invalid path format. Must be like /tmp/cache.db" 557 | msgstr "Неверный формат пути. Пример: /tmp/cache.db" 558 | 559 | msgid "Select the network interface from which the traffic will originate" 560 | msgstr "Выберите сетевой интерфейс, с которого будет исходить трафик" 561 | 562 | msgid "Copy to Clipboard" 563 | msgstr "Копировать в буфер обмена" 564 | 565 | msgid "Close" 566 | msgstr "Закрыть" 567 | 568 | msgid "Loading..." 569 | msgstr "Загрузка..." 570 | 571 | msgid "Loading version information..." 572 | msgstr "Загрузка информации о версии..." 573 | 574 | msgid "Checking FakeIP..." 575 | msgstr "Проверка FakeIP..." 576 | 577 | msgid "timeout" 578 | msgstr "таймаут" 579 | 580 | msgid "Current config: " 581 | msgstr "Текущая конфигурация: " 582 | 583 | msgid "Invalid VLESS URL: type must be one of tcp, udp, grpc, http" 584 | msgstr "Неверный URL VLESS: тип должен быть одним из tcp, udp, grpc, http" 585 | 586 | msgid "Invalid VLESS URL: security must be one of tls, reality, none" 587 | msgstr "Неверный URL VLESS: security должен быть одним из tls, reality, none" 588 | 589 | msgid "Podkop" 590 | msgstr "Podkop" 591 | 592 | msgid "Proxy" 593 | msgstr "Прокси" 594 | 595 | msgid "VPN" 596 | msgstr "VPN" 597 | 598 | msgid "http://openwrt.lan:9090/ui" 599 | msgstr "http://openwrt.lan:9090/ui" 600 | 601 | msgid "Podkop Configuration" 602 | msgstr "Конфигурация Podkop" 603 | 604 | msgid "Active Connections" 605 | msgstr "Активные соединения" 606 | 607 | msgid "DNSMasq Configuration" 608 | msgstr "Конфигурация DNSMasq" 609 | 610 | msgid "Sing-box Configuration" 611 | msgstr "Конфигурация Sing-box" 612 | 613 | msgid "Extra configurations" 614 | msgstr "Дополнительные конфигурации" 615 | 616 | msgid "Add Section" 617 | msgstr "Добавить раздел" 618 | 619 | msgid "No output" 620 | msgstr "Нет вывода" 621 | 622 | msgid "Failed to copy: " 623 | msgstr "Не удалось скопировать: " 624 | 625 | msgid "Show Config" 626 | msgstr "Показать конфигурацию" 627 | 628 | msgid "View Logs" 629 | msgstr "Просмотр логов" 630 | 631 | msgid "Check Connections" 632 | msgstr "Проверить соединения" 633 | 634 | msgid "FakeIP Status" 635 | msgstr "Статус FakeIP" 636 | 637 | msgid "Device Model: " 638 | msgstr "Модель устройства: " 639 | 640 | msgid "OpenWrt Version: " 641 | msgstr "Версия OpenWrt: " 642 | 643 | msgid "Check DNSMasq" 644 | msgstr "Проверить DNSMasq" 645 | 646 | msgid "Check NFT Rules" 647 | msgstr "Проверить правила NFT" 648 | 649 | msgid "Update Lists" 650 | msgstr "Обновить списки" 651 | 652 | msgid "Lists Update Results" 653 | msgstr "Результаты обновления списков" 654 | 655 | msgid "NFT Rules" 656 | msgstr "Правила NFT" 657 | 658 | msgid "GitHub Connectivity" 659 | msgstr "Подключение к GitHub" 660 | 661 | msgid "Check GitHub" 662 | msgstr "Проверить GitHub" 663 | 664 | msgid "GitHub Connectivity Results" 665 | msgstr "Результаты проверки подключения к GitHub" 666 | 667 | msgid "Sing-Box Logs" 668 | msgstr "Логи Sing-Box" 669 | 670 | msgid "View recent sing-box logs from system journal" 671 | msgstr "Просмотр последних логов sing-box из системного журнала" 672 | 673 | msgid "View Sing-Box Logs" 674 | msgstr "Просмотр логов Sing-Box" 675 | 676 | msgid "Podkop Logs" 677 | msgstr "Логи Podkop" 678 | 679 | msgid "View recent podkop logs from system journal" 680 | msgstr "Просмотр последних логов podkop из системного журнала" 681 | 682 | msgid "View Podkop Logs" 683 | msgstr "Просмотр логов Podkop" 684 | 685 | msgid "Active Connections" 686 | msgstr "Активные соединения" 687 | 688 | msgid "View active sing-box network connections" 689 | msgstr "Просмотр активных сетевых подключений sing-box" 690 | 691 | msgid "DNSMasq Configuration" 692 | msgstr "Конфигурация DNSMasq" 693 | 694 | msgid "View current DNSMasq configuration settings" 695 | msgstr "Просмотр текущих настроек конфигурации DNSMasq" 696 | 697 | msgid "Sing-Box Configuration" 698 | msgstr "Конфигурация Sing-Box" 699 | 700 | msgid "Show current sing-box configuration" 701 | msgstr "Показать текущую конфигурацию sing-box" 702 | 703 | msgid "Show Sing-Box Config" 704 | msgstr "Показать конфигурацию Sing-Box" 705 | 706 | msgid "Diagnostic Tools" 707 | msgstr "Инструменты диагностики" 708 | 709 | msgid "Unknown" 710 | msgstr "Неизвестно" 711 | 712 | msgid "sing-box not running" 713 | msgstr "sing-box не запущен" 714 | 715 | msgid "DNS not configured" 716 | msgstr "DNS не настроен" 717 | 718 | msgid "running & enabled" 719 | msgstr "запущен и активирован" 720 | 721 | msgid "running but disabled" 722 | msgstr "запущен, но деактивирован" 723 | 724 | msgid "stopped but enabled" 725 | msgstr "остановлен, но активирован" 726 | 727 | msgid "stopped & disabled" 728 | msgstr "остановлен и деактивирован" 729 | 730 | msgid "works in browser" 731 | msgstr "работает в браузере" 732 | 733 | msgid "works on router" 734 | msgstr "работает на роутере" 735 | 736 | msgid "Check Router FakeIP" 737 | msgstr "Проверить FakeIP на роутере" 738 | 739 | msgid "FakeIP Router Check" 740 | msgstr "Проверка FakeIP на роутере" 741 | 742 | msgid "FakeIP CLI Check" 743 | msgstr "Проверка FakeIP через CLI" 744 | 745 | msgid "FakeIP CLI Check Results" 746 | msgstr "Результаты проверки FakeIP через CLI" 747 | 748 | msgid "not works in browser" 749 | msgstr "не работает в браузере" 750 | 751 | msgid "not works on router" 752 | msgstr "не работает на роутере" 753 | 754 | msgid "Diagnostics" 755 | msgstr "Диагностика" 756 | 757 | msgid "DNS Status" 758 | msgstr "Статус DNS" 759 | 760 | msgid "Bypass Status" 761 | msgstr "Статус обхода" 762 | 763 | msgid "proxy working correctly" 764 | msgstr "прокси работает корректно" 765 | 766 | msgid "vpn working correctly" 767 | msgstr "vpn работает корректно" 768 | 769 | msgid "proxy not working" 770 | msgstr "прокси не работает" 771 | 772 | msgid "vpn not working" 773 | msgstr "vpn не работает" 774 | 775 | msgid "proxy not running" 776 | msgstr "прокси не запущен" 777 | 778 | msgid "vpn not running" 779 | msgstr "vpn не запущен" 780 | 781 | msgid "proxy routing incorrect" 782 | msgstr "маршрутизация прокси некорректна" 783 | 784 | msgid "vpn routing incorrect" 785 | msgstr "маршрутизация vpn некорректна" 786 | 787 | msgid "First endpoint check failed" 788 | msgstr "Проверка первой конечной точки не удалась" 789 | 790 | msgid "IP comparison failed" 791 | msgstr "Сравнение IP-адресов не удалось" 792 | 793 | msgid "Bypass check error" 794 | msgstr "Ошибка проверки обхода" 795 | 796 | msgid "Main config" 797 | msgstr "Основная конфигурация" 798 | 799 | msgid "Config without description" 800 | msgstr "Конфигурация без описания" 801 | 802 | msgid "DNS working" 803 | msgstr "DNS работает" 804 | 805 | msgid "Router DNS working" 806 | msgstr "DNS роутера работает" 807 | 808 | msgid "Router DNS not working" 809 | msgstr "DNS роутера не работает" 810 | 811 | msgid "DNS check error" 812 | msgstr "Ошибка проверки DNS" 813 | 814 | msgid "available" 815 | msgstr "доступен" 816 | 817 | msgid "unavailable" 818 | msgstr "недоступен" 819 | 820 | msgid "Apply for SS2022" 821 | msgstr "Применить для SS2022" 822 | 823 | msgid "PODKOP CONFIGURATION" 824 | msgstr "КОНФИГУРАЦИЯ PODKOP" 825 | 826 | msgid "FAKEIP ROUTER TEST" 827 | msgstr "ПРОВЕРКА FAKEIP НА РОУТЕРЕ" 828 | 829 | msgid "FAKEIP BROWSER TEST" 830 | msgstr "ПРОВЕРКА FAKEIP В БРАУЗЕРЕ" 831 | 832 | msgid "FakeIP is working correctly on router (198.18.x.x)" 833 | msgstr "FakeIP работает корректно на роутере (198.18.x.x)" 834 | 835 | msgid "Click here for all the info" 836 | msgstr "Нажмите для просмотра всей информации" 837 | 838 | msgid "Check DNS server on current device (PC, phone)" 839 | msgstr "Проверьте DNS сервер на текущем устройстве (ПК, телефон)" 840 | 841 | msgid "Its must be router!" 842 | msgstr "Это должен быть роутер!" 843 | 844 | msgid "Global check" 845 | msgstr "Глобальная проверка" 846 | 847 | msgid "Starting lists update..." 848 | msgstr "Начало обновления списков..." 849 | 850 | msgid "DNS check passed" 851 | msgstr "Проверка DNS пройдена" 852 | 853 | msgid "DNS check failed after 60 attempts" 854 | msgstr "Проверка DNS не удалась после 60 попыток" 855 | 856 | msgid "GitHub connection check passed" 857 | msgstr "Проверка подключения к GitHub пройдена" 858 | 859 | msgid "GitHub connection check passed (via proxy)" 860 | msgstr "Проверка подключения к GitHub пройдена (через прокси)" 861 | 862 | msgid "GitHub connection check failed after 60 attempts" 863 | msgstr "Проверка подключения к GitHub не удалась после 60 попыток" 864 | 865 | msgid "Downloading and processing lists..." 866 | msgstr "Загрузка и обработка списков..." 867 | 868 | msgid "Lists update completed successfully" 869 | msgstr "Обновление списков успешно завершено" 870 | 871 | msgid "Lists update failed" 872 | msgstr "Обновление списков не удалось" 873 | 874 | msgid "Error: " 875 | msgstr "Ошибка: " -------------------------------------------------------------------------------- /luci-app-podkop/po/templates/podkop.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "Content-Type: text/plain; charset=UTF-8" 3 | 4 | msgid "Podkop configuration" 5 | msgstr "" 6 | 7 | msgid "Basic Settings" 8 | msgstr "" 9 | 10 | msgid "Additional Settings" 11 | msgstr "" 12 | 13 | msgid "Secondary Config" 14 | msgstr "" 15 | 16 | msgid "Secondary VPN/Proxy Enable" 17 | msgstr "" 18 | 19 | msgid "Enable secondary VPN/Proxy configuration" 20 | msgstr "" 21 | 22 | msgid "Connection Type" 23 | msgstr "" 24 | 25 | msgid "Select between VPN and Proxy connection methods for traffic routing" 26 | msgstr "" 27 | 28 | msgid "Configuration Type" 29 | msgstr "" 30 | 31 | msgid "Select how to configure the proxy" 32 | msgstr "" 33 | 34 | msgid "Connection URL" 35 | msgstr "" 36 | 37 | msgid "Outbound Config" 38 | msgstr "" 39 | 40 | msgid "Proxy Configuration URL" 41 | msgstr "" 42 | 43 | msgid "Enter connection string starting with vless:// or ss:// for proxy configuration" 44 | msgstr "" 45 | 46 | msgid "Outbound Configuration" 47 | msgstr "" 48 | 49 | msgid "Enter complete outbound configuration in JSON format" 50 | msgstr "" 51 | 52 | msgid "Network Interface" 53 | msgstr "" 54 | 55 | msgid "Select network interface for VPN connection" 56 | msgstr "" 57 | 58 | msgid "Community Lists" 59 | msgstr "" 60 | 61 | msgid "Service List" 62 | msgstr "" 63 | 64 | msgid "Select predefined service for routing" 65 | msgstr "" 66 | 67 | msgid "User Domain List Type" 68 | msgstr "" 69 | 70 | msgid "Select how to add your custom domains" 71 | msgstr "" 72 | 73 | msgid "Disabled" 74 | msgstr "" 75 | 76 | msgid "Dynamic List" 77 | msgstr "" 78 | 79 | msgid "Text List" 80 | msgstr "" 81 | 82 | msgid "User Domains" 83 | msgstr "" 84 | 85 | msgid "Enter domain names without protocols (example: sub.example.com or example.com)" 86 | msgstr "" 87 | 88 | msgid "User Domains List" 89 | msgstr "" 90 | 91 | msgid "Enter domain names separated by comma, space or newline (example: sub.example.com, example.com or one domain per line)" 92 | msgstr "" 93 | 94 | msgid "Local Domain Lists" 95 | msgstr "" 96 | 97 | msgid "Use the list from the router filesystem" 98 | msgstr "" 99 | 100 | msgid "Local Domain Lists Path" 101 | msgstr "" 102 | 103 | msgid "Enter to the list file path" 104 | msgstr "" 105 | 106 | msgid "Remote Domain Lists" 107 | msgstr "" 108 | 109 | msgid "Download and use domain lists from remote URLs" 110 | msgstr "" 111 | 112 | msgid "Remote Domain URLs" 113 | msgstr "" 114 | 115 | msgid "Enter full URLs starting with http:// or https://" 116 | msgstr "" 117 | 118 | msgid "User Subnet List Type" 119 | msgstr "" 120 | 121 | msgid "Select how to add your custom subnets" 122 | msgstr "" 123 | 124 | msgid "Text List (comma/space/newline separated)" 125 | msgstr "" 126 | 127 | msgid "User Subnets" 128 | msgstr "" 129 | 130 | msgid "Enter subnets in CIDR notation (example: 103.21.244.0/22) or single IP addresses" 131 | msgstr "" 132 | 133 | msgid "User Subnets List" 134 | msgstr "" 135 | 136 | msgid "Enter subnets in CIDR notation or single IP addresses, separated by comma, space or newline" 137 | msgstr "" 138 | 139 | msgid "Remote Subnet Lists" 140 | msgstr "" 141 | 142 | msgid "Download and use subnet lists from remote URLs" 143 | msgstr "" 144 | 145 | msgid "Remote Subnet URLs" 146 | msgstr "" 147 | 148 | msgid "IP for full redirection" 149 | msgstr "" 150 | 151 | msgid "Specify local IP addresses whose traffic will always use the configured route" 152 | msgstr "" 153 | 154 | msgid "Local IPs" 155 | msgstr "" 156 | 157 | msgid "Enter valid IPv4 addresses" 158 | msgstr "" 159 | 160 | msgid "IP for exclusion" 161 | msgstr "" 162 | 163 | msgid "Specify local IP addresses that will never use the configured route" 164 | msgstr "" 165 | 166 | msgid "Mixed enable" 167 | msgstr "" 168 | 169 | msgid "Browser port: 2080" 170 | msgstr "" 171 | 172 | msgid "Yacd enable" 173 | msgstr "" 174 | 175 | msgid "Exclude NTP" 176 | msgstr "" 177 | 178 | msgid "For issues with open connections sing-box" 179 | msgstr "" 180 | 181 | msgid "QUIC disable" 182 | msgstr "" 183 | 184 | msgid "For issues with the video stream" 185 | msgstr "" 186 | 187 | msgid "List Update Frequency" 188 | msgstr "" 189 | 190 | msgid "Select how often the lists will be updated" 191 | msgstr "" 192 | 193 | msgid "Every hour" 194 | msgstr "" 195 | 196 | msgid "Every 2 hours" 197 | msgstr "" 198 | 199 | msgid "Every 3 hours" 200 | msgstr "" 201 | 202 | msgid "Every 4 hours" 203 | msgstr "" 204 | 205 | msgid "Every 6 hours" 206 | msgstr "" 207 | 208 | msgid "Every 12 hours" 209 | msgstr "" 210 | 211 | msgid "Every day" 212 | msgstr "" 213 | 214 | msgid "Every 3 days" 215 | msgstr "" 216 | 217 | msgid "Once a day at 04:00" 218 | msgstr "" 219 | 220 | msgid "Once a week on Sunday at 04:00" 221 | msgstr "" 222 | 223 | msgid "Invalid domain format. Enter domain without protocol (example: sub.example.com)" 224 | msgstr "" 225 | 226 | msgid "URL must use http:// or https:// protocol" 227 | msgstr "" 228 | 229 | msgid "Invalid URL format. URL must start with http:// or https://" 230 | msgstr "" 231 | 232 | msgid "Invalid format. Use format: X.X.X.X or X.X.X.X/Y" 233 | msgstr "" 234 | 235 | msgid "IP address parts must be between 0 and 255" 236 | msgstr "" 237 | 238 | msgid "CIDR must be between 0 and 32" 239 | msgstr "" 240 | 241 | msgid "Invalid IP format. Use format: X.X.X.X (like 192.168.1.1)" 242 | msgstr "" 243 | 244 | msgid "Invalid domain format: %s. Enter domain without protocol" 245 | msgstr "" 246 | 247 | msgid "Invalid format: %s. Use format: X.X.X.X or X.X.X.X/Y" 248 | msgstr "" 249 | 250 | msgid "IP parts must be between 0 and 255 in: %s" 251 | msgstr "" 252 | 253 | msgid "CIDR must be between 0 and 32 in: %s" 254 | msgstr "" 255 | 256 | msgid "Invalid path format. Path must start with \"/\" and contain only valid characters (letters, numbers, \"-\", \"_\", \"/\", \".\")" 257 | msgstr "" 258 | 259 | msgid "Invalid path format" 260 | msgstr "" 261 | 262 | msgid "JSON must contain at least type, server and server_port fields" 263 | msgstr "" 264 | 265 | msgid "Invalid JSON format" 266 | msgstr "" 267 | 268 | msgid "Warning: %s cannot be used together with %s. Previous selections have been removed." 269 | msgstr "" 270 | 271 | msgid "Regional options cannot be used together" 272 | msgstr "" 273 | 274 | msgid "Warning: Russia inside can only be used with Meta, Twitter, Discord, and Telegram. %s already in Russia inside and have been removed from selection." 275 | msgstr "" 276 | 277 | msgid "Russia inside restrictions" 278 | msgstr "" 279 | 280 | msgid "URL must start with vless:// or ss://" 281 | msgstr "" 282 | 283 | msgid "Invalid Shadowsocks URL format: missing method and password separator \":\"" 284 | msgstr "" 285 | 286 | msgid "Invalid Shadowsocks URL format" 287 | msgstr "" 288 | 289 | msgid "Invalid Shadowsocks URL: missing server address" 290 | msgstr "" 291 | 292 | msgid "Invalid Shadowsocks URL: missing server" 293 | msgstr "" 294 | 295 | msgid "Invalid Shadowsocks URL: missing port" 296 | msgstr "" 297 | 298 | msgid "Invalid port number. Must be between 1 and 65535" 299 | msgstr "" 300 | 301 | msgid "Invalid Shadowsocks URL: missing or invalid server/port format" 302 | msgstr "" 303 | 304 | msgid "Invalid VLESS URL: missing UUID" 305 | msgstr "" 306 | 307 | msgid "Invalid VLESS URL: missing server address" 308 | msgstr "" 309 | 310 | msgid "Invalid VLESS URL: missing server" 311 | msgstr "" 312 | 313 | msgid "Invalid VLESS URL: missing port" 314 | msgstr "" 315 | 316 | msgid "Invalid VLESS URL: missing or invalid server/port format" 317 | msgstr "" 318 | 319 | msgid "Invalid VLESS URL: missing query parameters" 320 | msgstr "" 321 | 322 | msgid "Invalid VLESS URL: missing type parameter" 323 | msgstr "" 324 | 325 | msgid "Invalid VLESS URL: missing security parameter" 326 | msgstr "" 327 | 328 | msgid "Invalid VLESS URL: missing pbk parameter for reality security" 329 | msgstr "" 330 | 331 | msgid "Invalid VLESS URL: missing fp parameter for reality security" 332 | msgstr "" 333 | 334 | msgid "Invalid VLESS URL: missing sni parameter for tls security" 335 | msgstr "" 336 | 337 | msgid "Invalid URL format: %s" 338 | msgstr "" 339 | 340 | msgid "Community Domain Lists" 341 | msgstr "" 342 | 343 | msgid "Domain List" 344 | msgstr "" 345 | 346 | msgid "Select a list" 347 | msgstr "" 348 | 349 | msgid "Community Subnet Lists" 350 | msgstr "" 351 | 352 | msgid "Enable routing for popular services like Twitter, Meta, and Discord" 353 | msgstr "" 354 | 355 | msgid "Service Networks" 356 | msgstr "" 357 | 358 | msgid "Select predefined service networks for routing" 359 | msgstr "" 360 | 361 | msgid "User Domain List" 362 | msgstr "" 363 | 364 | msgid "Enable and manage your custom list of domains for selective routing" 365 | msgstr "" 366 | 367 | msgid "User Domains List" 368 | msgstr "" 369 | 370 | msgid "Enter domain names separated by comma, space or newline (example: sub.example.com, example.com or one domain per line)" 371 | msgstr "" 372 | 373 | msgid "Remote Domain Lists URL" 374 | msgstr "" 375 | 376 | msgid "Enter URL to download domain list" 377 | msgstr "" 378 | 379 | msgid "Update Interval" 380 | msgstr "" 381 | 382 | msgid "Select how often to update the lists" 383 | msgstr "" 384 | 385 | msgid "Last Update" 386 | msgstr "" 387 | 388 | msgid "Last update time" 389 | msgstr "" 390 | 391 | msgid "Next Update" 392 | msgstr "" 393 | 394 | msgid "Next scheduled update time" 395 | msgstr "" 396 | 397 | msgid "Version" 398 | msgstr "" 399 | 400 | msgid "Component version" 401 | msgstr "" 402 | 403 | msgid "Installed" 404 | msgstr "" 405 | 406 | msgid "Not installed" 407 | msgstr "" 408 | 409 | msgid "Unknown version" 410 | msgstr "" 411 | 412 | msgid "Error parsing version" 413 | msgstr "" 414 | 415 | msgid "Error parsing status" 416 | msgstr "" 417 | 418 | msgid "Service is running" 419 | msgstr "" 420 | 421 | msgid "Service is stopped" 422 | msgstr "" 423 | 424 | msgid "Service is enabled" 425 | msgstr "" 426 | 427 | msgid "Service is disabled" 428 | msgstr "" 429 | 430 | msgid "Service Status" 431 | msgstr "" 432 | 433 | msgid "working" 434 | msgstr "" 435 | 436 | msgid "not working" 437 | msgstr "" 438 | 439 | msgid "check error" 440 | msgstr "" 441 | 442 | msgid "Diagnostic check in progress..." 443 | msgstr "" 444 | 445 | msgid "Diagnostic check completed" 446 | msgstr "" 447 | 448 | msgid "Diagnostic check failed" 449 | msgstr "" 450 | 451 | msgid "Update in progress..." 452 | msgstr "" 453 | 454 | msgid "Update completed" 455 | msgstr "" 456 | 457 | msgid "Update failed" 458 | msgstr "" 459 | 460 | msgid "Check in progress..." 461 | msgstr "" 462 | 463 | msgid "Check completed" 464 | msgstr "" 465 | 466 | msgid "Check failed" 467 | msgstr "" 468 | 469 | msgid "Version Information" 470 | msgstr "" 471 | 472 | msgid "Copied!" 473 | msgstr "" 474 | 475 | msgid "Podkop Status" 476 | msgstr "" 477 | 478 | msgid "Start Podkop" 479 | msgstr "" 480 | 481 | msgid "Stop Podkop" 482 | msgstr "" 483 | 484 | msgid "Restart Podkop" 485 | msgstr "" 486 | 487 | msgid "Enable Podkop" 488 | msgstr "" 489 | 490 | msgid "Disable Podkop" 491 | msgstr "" 492 | 493 | msgid "Loading diagnostics..." 494 | msgstr "" 495 | 496 | msgid "Error loading diagnostics" 497 | msgstr "" 498 | 499 | msgid "Sing-box Status" 500 | msgstr "" 501 | 502 | msgid "Diagnostic Tools" 503 | msgstr "" 504 | 505 | msgid "Unknown" 506 | msgstr "" 507 | 508 | msgid "Device Model: " 509 | msgstr "" 510 | 511 | msgid "OpenWrt Version: " 512 | msgstr "" 513 | 514 | msgid "Sing-box: " 515 | msgstr "" 516 | 517 | msgid "LuCI App: " 518 | msgstr "" 519 | 520 | msgid "Podkop: " 521 | msgstr "" 522 | 523 | msgid "Check NFT Rules" 524 | msgstr "" 525 | 526 | msgid "Update Lists" 527 | msgstr "" 528 | 529 | msgid "Lists Update Results" 530 | msgstr "" 531 | 532 | msgid "Extra configurations" 533 | msgstr "" 534 | 535 | msgid "Extra configuration" 536 | msgstr "" 537 | 538 | msgid "Add Section" 539 | msgstr "" 540 | 541 | msgid "Lists Update Results" 542 | msgstr "" 543 | 544 | msgid "Proxy Check" 545 | msgstr "" 546 | 547 | msgid "Check if sing-box proxy works correctly" 548 | msgstr "" 549 | 550 | msgid "Check Proxy" 551 | msgstr "" 552 | 553 | msgid "Proxy Check Results" 554 | msgstr "" 555 | 556 | msgid "NFT Rules" 557 | msgstr "" 558 | 559 | msgid "Show current nftables rules and statistics" 560 | msgstr "" 561 | 562 | msgid "Check Rules" 563 | msgstr "" 564 | 565 | msgid "GitHub Connectivity" 566 | msgstr "" 567 | 568 | msgid "Check GitHub connectivity and lists availability" 569 | msgstr "" 570 | 571 | msgid "Check GitHub" 572 | msgstr "" 573 | 574 | msgid "GitHub Connectivity Results" 575 | msgstr "" 576 | 577 | msgid "Sing-Box Logs" 578 | msgstr "" 579 | 580 | msgid "View recent sing-box logs from system journal" 581 | msgstr "" 582 | 583 | msgid "View Sing-Box Logs" 584 | msgstr "" 585 | 586 | msgid "Podkop Logs" 587 | msgstr "" 588 | 589 | msgid "View recent podkop logs from system journal" 590 | msgstr "" 591 | 592 | msgid "View Podkop Logs" 593 | msgstr "" 594 | 595 | msgid "Active Connections" 596 | msgstr "" 597 | 598 | msgid "View active sing-box network connections" 599 | msgstr "" 600 | 601 | msgid "Check Connections" 602 | msgstr "" 603 | 604 | msgid "DNSMasq Configuration" 605 | msgstr "" 606 | 607 | msgid "View current DNSMasq configuration settings" 608 | msgstr "" 609 | 610 | msgid "Check DNSMasq" 611 | msgstr "" 612 | 613 | msgid "Sing-Box Configuration" 614 | msgstr "" 615 | 616 | msgid "Show current sing-box configuration" 617 | msgstr "" 618 | 619 | msgid "Show Sing-Box Config" 620 | msgstr "" 621 | 622 | msgid "Lists Update Results" 623 | msgstr "" 624 | 625 | msgid "Warning" 626 | msgstr "" 627 | 628 | msgid "Success" 629 | msgstr "" 630 | 631 | msgid "Info" 632 | msgstr "" 633 | 634 | msgid "Error" 635 | msgstr "" 636 | 637 | msgid "Debug" 638 | msgstr "" 639 | 640 | msgid "Trace" 641 | msgstr "" 642 | 643 | msgid "Yes" 644 | msgstr "" 645 | 646 | msgid "No" 647 | msgstr "" 648 | 649 | msgid "OK" 650 | msgstr "" 651 | 652 | msgid "Cancel" 653 | msgstr "" 654 | 655 | msgid "Apply" 656 | msgstr "" 657 | 658 | msgid "Save" 659 | msgstr "" 660 | 661 | msgid "Delete" 662 | msgstr "" 663 | 664 | msgid "Edit" 665 | msgstr "" 666 | 667 | msgid "Add" 668 | msgstr "" 669 | 670 | msgid "Remove" 671 | msgstr "" 672 | 673 | msgid "Move Up" 674 | msgstr "" 675 | 676 | msgid "Move Down" 677 | msgstr "" 678 | 679 | msgid "Expand" 680 | msgstr "" 681 | 682 | msgid "Collapse" 683 | msgstr "" 684 | 685 | msgid "Show" 686 | msgstr "" 687 | 688 | msgid "Hide" 689 | msgstr "" 690 | 691 | msgid "Enable" 692 | msgstr "" 693 | 694 | msgid "Disable" 695 | msgstr "" 696 | 697 | msgid "Start" 698 | msgstr "" 699 | 700 | msgid "Stop" 701 | msgstr "" 702 | 703 | msgid "Restart" 704 | msgstr "" 705 | 706 | msgid "Reset" 707 | msgstr "" 708 | 709 | msgid "Refresh" 710 | msgstr "" 711 | 712 | msgid "Update" 713 | msgstr "" 714 | 715 | msgid "Install" 716 | msgstr "" 717 | 718 | msgid "Uninstall" 719 | msgstr "" 720 | 721 | msgid "Configure" 722 | msgstr "" 723 | 724 | msgid "Settings" 725 | msgstr "" 726 | 727 | msgid "Options" 728 | msgstr "" 729 | 730 | msgid "Advanced" 731 | msgstr "" 732 | 733 | msgid "Basic" 734 | msgstr "" 735 | 736 | msgid "General" 737 | msgstr "" 738 | 739 | msgid "Details" 740 | msgstr "" 741 | 742 | msgid "Status" 743 | msgstr "" 744 | 745 | msgid "Information" 746 | msgstr "" 747 | 748 | msgid "Configuration" 749 | msgstr "" 750 | 751 | msgid "Management" 752 | msgstr "" 753 | 754 | msgid "System" 755 | msgstr "" 756 | 757 | msgid "Network" 758 | msgstr "" 759 | 760 | msgid "Services" 761 | msgstr "" 762 | 763 | msgid "Remote Domain Lists URL" 764 | msgstr "" 765 | 766 | msgid "Enter URL to download domain list" 767 | msgstr "" 768 | 769 | msgid "Update Interval" 770 | msgstr "" 771 | 772 | msgid "Select how often to update the lists" 773 | msgstr "" 774 | 775 | msgid "Last Update" 776 | msgstr "" 777 | 778 | msgid "Last update time" 779 | msgstr "" 780 | 781 | msgid "Next Update" 782 | msgstr "" 783 | 784 | msgid "Next scheduled update time" 785 | msgstr "" 786 | 787 | msgid "Version" 788 | msgstr "" 789 | 790 | msgid "Component version" 791 | msgstr "" 792 | 793 | msgid "Installed" 794 | msgstr "" 795 | 796 | msgid "Not installed" 797 | msgstr "" 798 | 799 | msgid "Unknown version" 800 | msgstr "" 801 | 802 | msgid "Error parsing version" 803 | msgstr "" 804 | 805 | msgid "Error parsing status" 806 | msgstr "" 807 | 808 | msgid "Service is running" 809 | msgstr "" 810 | 811 | msgid "Service is stopped" 812 | msgstr "" 813 | 814 | msgid "Service is enabled" 815 | msgstr "" 816 | 817 | msgid "Service is disabled" 818 | msgstr "" 819 | 820 | msgid "Service Status" 821 | msgstr "" 822 | 823 | msgid "working" 824 | msgstr "" 825 | 826 | msgid "not working" 827 | msgstr "" 828 | 829 | msgid "check error" 830 | msgstr "" 831 | 832 | msgid "Diagnostic check in progress..." 833 | msgstr "" 834 | 835 | msgid "Diagnostic check completed" 836 | msgstr "" 837 | 838 | msgid "Diagnostic check failed" 839 | msgstr "" 840 | 841 | msgid "Update in progress..." 842 | msgstr "" 843 | 844 | msgid "Update completed" 845 | msgstr "" 846 | 847 | msgid "Update failed" 848 | msgstr "" 849 | 850 | msgid "Check in progress..." 851 | msgstr "" 852 | 853 | msgid "Check completed" 854 | msgstr "" 855 | 856 | msgid "Check failed" 857 | msgstr "" 858 | 859 | msgid "DNS Protocol Type" 860 | msgstr "" 861 | 862 | msgid "Select DNS protocol to use" 863 | msgstr "" 864 | 865 | msgid "DNS over HTTPS (DoH)" 866 | msgstr "" 867 | 868 | msgid "DNS over TLS (DoT)" 869 | msgstr "" 870 | 871 | msgid "UDP (Unprotected DNS)" 872 | msgstr "" 873 | 874 | msgid "DNS Server" 875 | msgstr "" 876 | 877 | msgid "Select or enter DNS server address" 878 | msgstr "" 879 | 880 | msgid "DNS Rewrite TTL" 881 | msgstr "" 882 | 883 | msgid "Time in seconds for DNS record caching (default: 600)" 884 | msgstr "" 885 | 886 | msgid "TTL value cannot be empty" 887 | msgstr "" 888 | 889 | msgid "TTL must be a positive number" 890 | msgstr "" 891 | 892 | msgid "Cache File Path" 893 | msgstr "" 894 | 895 | msgid "Select or enter path for sing-box cache file. Change this ONLY if you know what you are doing" 896 | msgstr "" 897 | 898 | msgid "Cache file path cannot be empty" 899 | msgstr "" 900 | 901 | msgid "Path must be absolute (start with /)" 902 | msgstr "" 903 | 904 | msgid "Path must end with cache.db" 905 | msgstr "" 906 | 907 | msgid "Path must contain at least one directory (like /tmp/cache.db)" 908 | msgstr "" 909 | 910 | msgid "Invalid path format. Must be like /tmp/cache.db" 911 | msgstr "" 912 | 913 | msgid "Copy to Clipboard" 914 | msgstr "" 915 | 916 | msgid "Close" 917 | msgstr "" 918 | 919 | msgid "Loading..." 920 | msgstr "" 921 | 922 | msgid "Loading version information..." 923 | msgstr "" 924 | 925 | msgid "Checking FakeIP..." 926 | msgstr "" 927 | 928 | msgid "timeout" 929 | msgstr "" 930 | 931 | msgid "Current config: " 932 | msgstr "" 933 | 934 | msgid "Invalid VLESS URL: type must be one of tcp, udp, grpc, http" 935 | msgstr "" 936 | 937 | msgid "Invalid VLESS URL: security must be one of tls, reality, none" 938 | msgstr "" 939 | 940 | msgid "Podkop" 941 | msgstr "" 942 | 943 | msgid "Proxy" 944 | msgstr "" 945 | 946 | msgid "VPN" 947 | msgstr "" 948 | 949 | msgid "http://openwrt.lan:9090/ui" 950 | msgstr "" 951 | 952 | msgid "Podkop Configuration" 953 | msgstr "" 954 | 955 | msgid "Active Connections" 956 | msgstr "" 957 | 958 | msgid "DNSMasq Configuration" 959 | msgstr "" 960 | 961 | msgid "Sing-box Configuration" 962 | msgstr "" 963 | 964 | msgid "Extra configurations" 965 | msgstr "" 966 | 967 | msgid "Add Section" 968 | msgstr "" 969 | 970 | msgid "No output" 971 | msgstr "" 972 | 973 | msgid "Failed to copy: " 974 | msgstr "" 975 | 976 | msgid "Show Config" 977 | msgstr "" 978 | 979 | msgid "View Logs" 980 | msgstr "" 981 | 982 | msgid "Check Connections" 983 | msgstr "" 984 | 985 | msgid "FakeIP Status" 986 | msgstr "" 987 | 988 | msgid "Device Model: " 989 | msgstr "" 990 | 991 | msgid "OpenWrt Version: " 992 | msgstr "" 993 | 994 | msgid "Check DNSMasq" 995 | msgstr "" 996 | 997 | msgid "Check NFT Rules" 998 | msgstr "" 999 | 1000 | msgid "Update Lists" 1001 | msgstr "" 1002 | 1003 | msgid "Lists Update Results" 1004 | msgstr "" 1005 | 1006 | msgid "NFT Rules" 1007 | msgstr "" 1008 | 1009 | msgid "GitHub Connectivity" 1010 | msgstr "" 1011 | 1012 | msgid "Check GitHub" 1013 | msgstr "" 1014 | 1015 | msgid "GitHub Connectivity Results" 1016 | msgstr "" 1017 | 1018 | msgid "Sing-Box Logs" 1019 | msgstr "" 1020 | 1021 | msgid "View recent sing-box logs from system journal" 1022 | msgstr "" 1023 | 1024 | msgid "View Sing-Box Logs" 1025 | msgstr "" 1026 | 1027 | msgid "Podkop Logs" 1028 | msgstr "" 1029 | 1030 | msgid "View recent podkop logs from system journal" 1031 | msgstr "" 1032 | 1033 | msgid "View Podkop Logs" 1034 | msgstr "" 1035 | 1036 | msgid "Active Connections" 1037 | msgstr "" 1038 | 1039 | msgid "View active sing-box network connections" 1040 | msgstr "" 1041 | 1042 | msgid "DNSMasq Configuration" 1043 | msgstr "" 1044 | 1045 | msgid "View current DNSMasq configuration settings" 1046 | msgstr "" 1047 | 1048 | msgid "Sing-Box Configuration" 1049 | msgstr "" 1050 | 1051 | msgid "Show current sing-box configuration" 1052 | msgstr "" 1053 | 1054 | msgid "Show Sing-Box Config" 1055 | msgstr "" 1056 | 1057 | msgid "Diagnostic Tools" 1058 | msgstr "" 1059 | 1060 | msgid "Unknown" 1061 | msgstr "" 1062 | 1063 | msgid "sing-box not running" 1064 | msgstr "" 1065 | 1066 | msgid "DNS not configured" 1067 | msgstr "" 1068 | 1069 | msgid "running & enabled" 1070 | msgstr "" 1071 | 1072 | msgid "running but disabled" 1073 | msgstr "" 1074 | 1075 | msgid "stopped but enabled" 1076 | msgstr "" 1077 | 1078 | msgid "stopped & disabled" 1079 | msgstr "" 1080 | 1081 | msgid "works in browser" 1082 | msgstr "" 1083 | 1084 | msgid "works on router" 1085 | msgstr "" 1086 | 1087 | msgid "Check Router FakeIP" 1088 | msgstr "" 1089 | 1090 | msgid "FakeIP Router Check" 1091 | msgstr "" 1092 | 1093 | msgid "FakeIP CLI Check" 1094 | msgstr "" 1095 | 1096 | msgid "FakeIP CLI Check Results" 1097 | msgstr "" 1098 | 1099 | msgid "not works in browser" 1100 | msgstr "" 1101 | 1102 | msgid "not works on router" 1103 | msgstr "" 1104 | 1105 | msgid "Diagnostics" 1106 | msgstr "" 1107 | 1108 | msgid "DNS Status" 1109 | msgstr "" 1110 | 1111 | msgid "Bypass Status" 1112 | msgstr "" 1113 | 1114 | msgid "proxy working correctly" 1115 | msgstr "" 1116 | 1117 | msgid "vpn working correctly" 1118 | msgstr "" 1119 | 1120 | msgid "proxy not working" 1121 | msgstr "" 1122 | 1123 | msgid "vpn not working" 1124 | msgstr "" 1125 | 1126 | msgid "proxy not running" 1127 | msgstr "" 1128 | 1129 | msgid "vpn not running" 1130 | msgstr "" 1131 | 1132 | msgid "proxy routing incorrect" 1133 | msgstr "" 1134 | 1135 | msgid "vpn routing incorrect" 1136 | msgstr "" 1137 | 1138 | msgid "First endpoint check failed" 1139 | msgstr "" 1140 | 1141 | msgid "IP comparison failed" 1142 | msgstr "" 1143 | 1144 | msgid "Bypass check error" 1145 | msgstr "" 1146 | 1147 | msgid "Main config" 1148 | msgstr "" 1149 | 1150 | msgid "Enter connection string starting with vless:// or ss:// for proxy configuration. Add comments with // for backup configs" 1151 | msgstr "" 1152 | 1153 | msgid "Config without description" 1154 | msgstr "" 1155 | 1156 | msgid "DNS working" 1157 | msgstr "" 1158 | 1159 | msgid "Router DNS working" 1160 | msgstr "" 1161 | 1162 | msgid "Router DNS not working" 1163 | msgstr "" 1164 | 1165 | msgid "DNS check error" 1166 | msgstr "" 1167 | 1168 | msgid "available" 1169 | msgstr "" 1170 | 1171 | msgid "unavailable" 1172 | msgstr "" 1173 | 1174 | msgid "PODKOP CONFIGURATION" 1175 | msgstr "" 1176 | 1177 | msgid "FAKEIP ROUTER TEST" 1178 | msgstr "" 1179 | 1180 | msgid "FAKEIP BROWSER TEST" 1181 | msgstr "" 1182 | 1183 | msgid "FakeIP is working correctly on router (198.18.x.x)" 1184 | msgstr "" 1185 | 1186 | msgid "Click here for all the info" 1187 | msgstr "" 1188 | 1189 | msgid "Check DNS server on current device (PC, phone)" 1190 | msgstr "" 1191 | 1192 | msgid "Its must be router!" 1193 | msgstr "" 1194 | 1195 | msgid "Global check" 1196 | msgstr "" 1197 | 1198 | msgid "Starting lists update..." 1199 | msgstr "" 1200 | 1201 | msgid "DNS check passed" 1202 | msgstr "" 1203 | 1204 | msgid "DNS check failed after 60 attempts" 1205 | msgstr "" 1206 | 1207 | msgid "GitHub connection check passed" 1208 | msgstr "" 1209 | 1210 | msgid "GitHub connection check passed (via proxy)" 1211 | msgstr "" 1212 | 1213 | msgid "GitHub connection check failed after 60 attempts" 1214 | msgstr "" 1215 | 1216 | msgid "Downloading and processing lists..." 1217 | msgstr "" 1218 | 1219 | msgid "Lists update completed successfully" 1220 | msgstr "" 1221 | 1222 | msgid "Lists update failed" 1223 | msgstr "" 1224 | 1225 | msgid "Loading..." 1226 | msgstr "" 1227 | 1228 | msgid "Error: " 1229 | msgstr "" -------------------------------------------------------------------------------- /luci-app-podkop/root/etc/uci-defaults/50_luci-podkop: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | rm -f /var/luci-indexcache* 4 | rm -f /tmp/luci-indexcache* 5 | 6 | [ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd reload 7 | 8 | logger -t "podkop" "$timestamp uci-defaults script executed" 9 | 10 | exit 0 -------------------------------------------------------------------------------- /luci-app-podkop/root/usr/share/luci/menu.d/luci-app-podkop.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/services/podkop": { 3 | "title": "Podkop", 4 | "order": 42, 5 | "action": { 6 | "type": "view", 7 | "path": "podkop/podkop" 8 | }, 9 | "depends": { 10 | "acl": [ "luci-app-podkop" ], 11 | "uci": { "podkop": true } 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /luci-app-podkop/root/usr/share/rpcd/acl.d/luci-app-podkop.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-podkop": { 3 | "description": "Grant UCI and RPC access to LuCI app podkop", 4 | "read": { 5 | "file": { 6 | "/etc/init.d/podkop": [ 7 | "exec" 8 | ], 9 | "/usr/bin/podkop": [ 10 | "exec" 11 | ] 12 | }, 13 | "ubus": { 14 | "service": [ 15 | "list" 16 | ] 17 | }, 18 | "uci": [ 19 | "podkop" 20 | ] 21 | }, 22 | "write": { 23 | "uci": [ 24 | "podkop" 25 | ] 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /podkop/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | PKG_NAME:=podkop 4 | PKG_VERSION:=0.4.4 5 | PKG_RELEASE:=1 6 | 7 | PKG_MAINTAINER:=ITDog 8 | PKG_LICENSE:=GPL-2.0-or-later 9 | 10 | include $(INCLUDE_DIR)/package.mk 11 | 12 | define Package/podkop 13 | SECTION:=net 14 | CATEGORY:=Network 15 | DEPENDS:=+sing-box +curl +jq +kmod-nft-tproxy +coreutils-base64 16 | CONFLICTS:=https-dns-proxy 17 | TITLE:=Domain routing app 18 | URL:=https://podkop.net 19 | PKGARCH:=all 20 | endef 21 | 22 | define Package/podkop/description 23 | Domain routing. Use of VLESS, Shadowsocks technologies 24 | endef 25 | 26 | define Build/Configure 27 | endef 28 | 29 | define Build/Compile 30 | endef 31 | 32 | define Package/podkop/prerm 33 | #!/bin/sh 34 | 35 | grep -q "105 podkop" /etc/iproute2/rt_tables && sed -i "/105 podkop/d" /etc/iproute2/rt_tables 36 | 37 | /etc/init.d/podkop stop 38 | 39 | exit 0 40 | endef 41 | 42 | define Package/podkop/conffiles 43 | /etc/config/podkop 44 | endef 45 | 46 | define Package/podkop/install 47 | $(INSTALL_DIR) $(1)/etc/init.d 48 | $(INSTALL_BIN) ./files/etc/init.d/podkop $(1)/etc/init.d/podkop 49 | sed -i "s/VERSION_FROM_MAKEFILE/$(PKG_VERSION)/g" $(1)/etc/init.d/podkop 50 | 51 | $(INSTALL_DIR) $(1)/etc/config 52 | $(INSTALL_CONF) ./files/etc/config/podkop $(1)/etc/config/podkop 53 | 54 | $(INSTALL_DIR) $(1)/usr/bin 55 | $(INSTALL_BIN) ./files/usr/bin/podkop $(1)/usr/bin/podkop 56 | endef 57 | 58 | $(eval $(call BuildPackage,podkop)) 59 | -------------------------------------------------------------------------------- /podkop/files/etc/config/podkop: -------------------------------------------------------------------------------- 1 | config main 'main' 2 | option mode 'proxy' 3 | #option interface '' 4 | option proxy_config_type 'url' 5 | #option outbound_json '' 6 | option proxy_string '' 7 | option domain_list_enabled '1' 8 | list domain_list 'russia_inside' 9 | option subnets_list_enabled '0' 10 | option custom_domains_list_type 'disabled' 11 | #list custom_domains '' 12 | #option custom_domains_text '' 13 | option custom_local_domains_list_enabled '0' 14 | #list custom_local_domains '' 15 | option custom_download_domains_list_enabled '0' 16 | #list custom_download_domains '' 17 | option custom_domains_list_type 'disable' 18 | #list custom_subnets '' 19 | #custom_subnets_text '' 20 | option custom_download_subnets_list_enabled '0' 21 | #list custom_download_subnets '' 22 | option all_traffic_from_ip_enabled '0' 23 | #list all_traffic_ip '' 24 | option delist_domains_enabled '0' 25 | #list delist_domains '' 26 | option exclude_from_ip_enabled '0' 27 | #list exclude_traffic_ip '' 28 | option yacd '0' 29 | option socks5 '0' 30 | option exclude_ntp '0' 31 | option quic_disable '0' 32 | option dont_touch_dhcp '0' 33 | option update_interval '1d' 34 | option dns_type 'doh' 35 | option dns_server '8.8.8.8' 36 | option dns_rewrite_ttl '60' 37 | option cache_file '/tmp/cache.db' 38 | list iface 'br-lan' 39 | option mon_restart_ifaces '0' 40 | #list restart_ifaces 'wan' 41 | option ss_uot '0' 42 | option detour '0' -------------------------------------------------------------------------------- /podkop/files/etc/init.d/podkop: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=99 4 | USE_PROCD=1 5 | 6 | script=$(readlink "$initscript") 7 | NAME="$(basename ${script:-$initscript})" 8 | config_load "$NAME" 9 | 10 | start_service() { 11 | echo "Start podkop" 12 | 13 | config_get mon_restart_ifaces "main" "mon_restart_ifaces" 14 | config_get restart_ifaces "main" "restart_ifaces" 15 | 16 | procd_open_instance 17 | procd_set_param command /usr/bin/podkop start 18 | [ "$mon_restart_ifaces" = "1" ] && [ -n "$restart_ifaces" ] && procd_set_param netdev $restart_ifaces 19 | procd_set_param stdout 1 20 | procd_set_param stderr 1 21 | procd_close_instance 22 | } 23 | 24 | stop_service() { 25 | /usr/bin/podkop stop 26 | } 27 | 28 | reload_service() { 29 | /usr/bin/podkop reload > /dev/null 2>&1 30 | } 31 | 32 | service_triggers() { 33 | echo "service_triggers start" 34 | 35 | config_get mon_restart_ifaces "main" "mon_restart_ifaces" 36 | config_get restart_ifaces "main" "restart_ifaces" 37 | 38 | procd_open_trigger 39 | procd_add_config_trigger "config.change" "$NAME" "$initscript" restart 'on_config_change' 40 | 41 | if [ "$mon_restart_ifaces" = "1" ]; then 42 | for iface in $restart_ifaces; do 43 | procd_add_reload_interface_trigger $iface 44 | done 45 | fi 46 | procd_close_trigger 47 | } --------------------------------------------------------------------------------