mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 21:15:13 +08:00
🌈 Sync 2026-07-29 14:47:30
This commit is contained in:
1 parent
ca80299bab
commit
fc21fd8c5e
48 files changed
+13901
-3399
No files matched your search
@@ -0,0 +1,340 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
@@ -1,15 +1,31 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2024 ImmortalWrt.org
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64
|
||||
PKG_NAME:=luci-app-homeproxy
|
||||
PKG_VERSION:=2.0.0
|
||||
PKG_RELEASE:=8
|
||||
|
||||
LUCI_TITLE:=Sing-box proxy management platform for OpenWrt
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:= \
|
||||
+sing-box \
|
||||
+firewall4 \
|
||||
+dnsmasq-full \
|
||||
+ip-full \
|
||||
+kmod-tun \
|
||||
+kmod-nft-tproxy \
|
||||
+ucode-mod-digest
|
||||
+ca-bundle \
|
||||
+curl \
|
||||
+jsonfilter \
|
||||
+unzip \
|
||||
+ucode-mod-digest \
|
||||
+ucode-mod-fs \
|
||||
+ucode-mod-math \
|
||||
+ucode-mod-ubus \
|
||||
+ucode-mod-uci
|
||||
|
||||
define Package/luci-app-homeproxy/conffiles
|
||||
/etc/config/homeproxy
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
适用于 OpenWrt 24.10 的 HomeProxy LuCI 管理界面,按 `官方内核sing-box 1.13.13` 进行二次定制适配,在官方内核基础上增加功能:
|
||||
1、远程规则集本地持久化
|
||||
2、Clash API 代理供应商
|
||||
3、Clash API 中文名称
|
||||
|
||||
## 一、安装
|
||||
上传 zip 到 OpenWrt /tmp目录后解压并执行(文件名homeproxy-2.0.1-x86_64.zip自行替换):
|
||||
```sh
|
||||
cd /tmp
|
||||
unzip homeproxy-2.0.1-x86_64.zip
|
||||
cd homeproxy-2.0.1-x86_64
|
||||
sh install.sh
|
||||
```
|
||||
|
||||
## 二、卸载
|
||||
```sh
|
||||
/etc/init.d/homeproxy stop 2>/dev/null || true
|
||||
/etc/init.d/homeproxy disable 2>/dev/null || true
|
||||
opkg remove luci-i18n-homeproxy-zh-cn 2>/dev/null || true
|
||||
opkg remove luci-app-homeproxy 2>/dev/null || true
|
||||
rm -rf /etc/config/homeproxy
|
||||
rm -rf /etc/homeproxy
|
||||
rm -rf /usr/share/homeproxy
|
||||
rm -rf /www/luci-static/resources/view/homeproxy
|
||||
rm -f /www/luci-static/resources/homeproxy.js
|
||||
rm -f /usr/share/luci/menu.d/luci-app-homeproxy.json
|
||||
rm -f /usr/share/rpcd/acl.d/luci-app-homeproxy.json
|
||||
rm -f /usr/share/rpcd/ucode/luci.homeproxy
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache/*
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart
|
||||
```
|
||||
|
||||
## 三、维护工作流
|
||||
- `Rescan-Translation.yml`:代码变更后自动重新扫描并更新翻译文件,也支持手动运行。
|
||||
- `Update-Geodata.yml`:每周自动更新内置 geodata 资源,也支持手动运行。
|
||||
|
||||
## 四、常见问题
|
||||
1、面版下载失败解决办法
|
||||
第一种:面版设置-UI下载地址,下拉选择需要的UI,点击更新面版手动触发面版下载。
|
||||
第二种:下载面版ZIP包,手动上传面版ZIP,下载地址如下。
|
||||
镜像
|
||||
```sh
|
||||
https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip
|
||||
```
|
||||
或者直连
|
||||
```sh
|
||||
https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip
|
||||
```
|
||||
2、远程规则集是否下载成功判断
|
||||
连接SSH查看/etc/homeproxy/ruleset/目录下是否有规则集文件,有则表示下载成功,反之则没下载成功,检查规则集远程连接。
|
||||
|
||||
3、安装新包后如果页面仍显示旧文字,建议清浏览器缓存,或执行:
|
||||
```sh
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache/*
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart
|
||||
```
|
||||
|
||||
## 五、感谢作者
|
||||
immortalwrt
|
||||
https://github.com/immortalwrt/homeproxy
|
||||
|
||||
VIKINGYFY
|
||||
https://github.com/VIKINGYFY/homeproxy
|
||||
|
||||
@@ -12,6 +12,158 @@
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
function pad2(value) {
|
||||
value = (value == null) ? '' : String(value).trim();
|
||||
return value.length === 1 ? '0' + value : value;
|
||||
}
|
||||
|
||||
function normalizeTimePart(value, min, max) {
|
||||
value = parseInt(String(value || '').trim(), 10);
|
||||
if (isNaN(value))
|
||||
value = min;
|
||||
value = Math.max(min, Math.min(max, value));
|
||||
return pad2(value);
|
||||
}
|
||||
|
||||
function parseCron(value) {
|
||||
const matched = String(value || '').trim().match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+([0-7*])$/);
|
||||
if (!matched)
|
||||
return null;
|
||||
|
||||
const minute = +matched[1],
|
||||
hour = +matched[2];
|
||||
if (minute < 0 || minute > 59 || hour < 0 || hour > 23)
|
||||
return null;
|
||||
|
||||
return {
|
||||
minute: normalizeTimePart(matched[1], 0, 59),
|
||||
hour: normalizeTimePart(matched[2], 0, 23),
|
||||
day: matched[3] === '*' ? '*' : (matched[3] === '7' ? '0' : matched[3])
|
||||
};
|
||||
}
|
||||
|
||||
function buildCron(day, hour, minute) {
|
||||
return [
|
||||
normalizeTimePart(minute, 0, 59),
|
||||
normalizeTimePart(hour, 0, 23),
|
||||
'*',
|
||||
'*',
|
||||
String(day || '*')
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function alignCronEditorRow(wrap) {
|
||||
let field = wrap?.parentNode;
|
||||
|
||||
while (field && !field.classList?.contains('cbi-value-field'))
|
||||
field = field.parentNode;
|
||||
|
||||
let row = field?.parentNode;
|
||||
while (row && !row.classList?.contains('cbi-value'))
|
||||
row = row.parentNode;
|
||||
|
||||
if (!row || !field)
|
||||
return false;
|
||||
|
||||
if (wrap._dailyRow) {
|
||||
wrap._dailyRow.style.display = row.style.display;
|
||||
return true;
|
||||
}
|
||||
|
||||
let dailyRow = E('div', { 'class': 'cbi-value homeproxy-cron-daily-row' }, [
|
||||
E('div', { 'class': 'cbi-value-title' }, _('Update time (daily)')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [ wrap._dailyField ])
|
||||
]);
|
||||
|
||||
wrap._dailyRow = dailyRow;
|
||||
row.parentNode.insertBefore(dailyRow, row.nextSibling);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function watchCronEditorRow(wrap) {
|
||||
const align = () => alignCronEditorRow(wrap);
|
||||
[ 0, 50, 200, 500, 1000, 2000 ].forEach(delay => window.setTimeout(align, delay));
|
||||
|
||||
if (typeof MutationObserver !== 'undefined' && document?.body) {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (align())
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window.setTimeout(() => observer.disconnect(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCronEditor(input) {
|
||||
if (!input)
|
||||
return null;
|
||||
|
||||
const parsed = parseCron(input.value);
|
||||
input.type = 'text';
|
||||
input.readOnly = true;
|
||||
input.tabIndex = -1;
|
||||
input.style.position = 'absolute';
|
||||
input.style.opacity = '0';
|
||||
input.style.pointerEvents = 'none';
|
||||
input.style.width = '1px';
|
||||
input.style.height = '1px';
|
||||
input.style.minWidth = '1px';
|
||||
input.style.maxWidth = '1px';
|
||||
input.style.padding = '0';
|
||||
input.style.border = '0';
|
||||
|
||||
let day = E('select', { 'class': 'cbi-input-select', 'style': 'width: 12em !important; min-width: 12em !important; max-width: 12em !important; box-sizing: border-box;' }),
|
||||
hour = E('select', { 'class': 'cbi-input-select', 'style': 'width: 4.25em !important; min-width: 4.25em !important; max-width: 4.25em !important; box-sizing: border-box;' }),
|
||||
minute = E('select', { 'class': 'cbi-input-select', 'style': 'width: 4.25em !important; min-width: 4.25em !important; max-width: 4.25em !important; box-sizing: border-box;' }),
|
||||
dailyField = E('div', { 'style': 'display: flex; align-items: center; gap: .25em; width: 9.5em;' }, [
|
||||
hour,
|
||||
E('span', ':'),
|
||||
minute
|
||||
]),
|
||||
wrap = E('div', { 'class': 'homeproxy-cron-editor' }, [ day ]);
|
||||
|
||||
wrap._dailyField = dailyField;
|
||||
|
||||
for (let i = 0; i < 24; i++)
|
||||
hour.appendChild(E('option', { value: pad2(i) }, pad2(i)));
|
||||
|
||||
for (let i = 0; i < 60; i++)
|
||||
minute.appendChild(E('option', { value: pad2(i) }, pad2(i)));
|
||||
|
||||
[
|
||||
['*', _('Every day')],
|
||||
['1', _('Every Monday')],
|
||||
['2', _('Every Tuesday')],
|
||||
['3', _('Every Wednesday')],
|
||||
['4', _('Every Thursday')],
|
||||
['5', _('Every Friday')],
|
||||
['6', _('Every Saturday')],
|
||||
['0', _('Every Sunday')]
|
||||
].forEach(([value, label]) => day.appendChild(E('option', { value }, label)));
|
||||
|
||||
day.value = parsed ? parsed.day : '*';
|
||||
hour.value = parsed ? parsed.hour : '00';
|
||||
minute.value = parsed ? parsed.minute : '00';
|
||||
input.value = buildCron(day.value, hour.value, minute.value);
|
||||
input.setAttribute('value', input.value);
|
||||
|
||||
const sync = () => {
|
||||
input.value = buildCron(day.value, hour.value, minute.value);
|
||||
input.setAttribute('value', input.value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
};
|
||||
|
||||
day.addEventListener('change', sync);
|
||||
hour.addEventListener('change', sync);
|
||||
minute.addEventListener('change', sync);
|
||||
wrap.appendChild(input);
|
||||
window.setTimeout(() => watchCronEditorRow(wrap), 0);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
return baseclass.extend({
|
||||
dns_strategy: {
|
||||
'': _('Default'),
|
||||
@@ -86,6 +238,29 @@ return baseclass.extend({
|
||||
}
|
||||
}),
|
||||
|
||||
CBIMultiValue: form.MultiValue.extend({
|
||||
__name__: 'CBI.HomeProxyMultiValue',
|
||||
|
||||
renderWidget(section_id, _option_index, cfgvalue) {
|
||||
let value = (cfgvalue != null) ? cfgvalue : this.default,
|
||||
choices = this.transformChoices() || {},
|
||||
widget = new ui.Dropdown(L.toArray(value), choices, {
|
||||
id: this.cbid(section_id),
|
||||
sort: this.keylist,
|
||||
multiple: true,
|
||||
optional: this.optional || this.rmempty,
|
||||
select_placeholder: this.placeholder,
|
||||
create: this.create,
|
||||
display_items: this.display_size ?? this.size ?? 3,
|
||||
dropdown_items: this.dropdown_size ?? this.size ?? -1,
|
||||
validate: this.getValidator(section_id),
|
||||
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
|
||||
});
|
||||
|
||||
return widget.render();
|
||||
}
|
||||
}),
|
||||
|
||||
calcStringMD5(e) {
|
||||
/* Thanks to https://stackoverflow.com/a/41602636 */
|
||||
let h = (a, b) => {
|
||||
@@ -185,6 +360,49 @@ return baseclass.extend({
|
||||
return L.resolveDefault(callGetSingBoxFeatures(), {});
|
||||
},
|
||||
|
||||
renderCronSelector(/* ... */) {
|
||||
if (!this._homeproxyCronOriginalFormvalue) {
|
||||
this._homeproxyCronOriginalFormvalue = this.formvalue;
|
||||
this.formvalue = function(section_id) {
|
||||
let cbid = this.cbid(section_id),
|
||||
ids = [ cbid, 'widget.' + cbid ],
|
||||
roots = [ document.getElementById('modal_overlay'), document ],
|
||||
input = null;
|
||||
|
||||
for (let root of roots) {
|
||||
if (!root)
|
||||
continue;
|
||||
|
||||
for (let el of root.querySelectorAll('input')) {
|
||||
if (ids.includes(el.id) || ids.includes(el.name)) {
|
||||
input = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (input)
|
||||
break;
|
||||
}
|
||||
|
||||
let value = input?.value ||
|
||||
this._homeproxyCronOriginalFormvalue?.call(this, section_id) ||
|
||||
this.default ||
|
||||
'0 0 * * *';
|
||||
|
||||
return parseCron(value) ? value : (this.default || '0 0 * * *');
|
||||
};
|
||||
}
|
||||
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments),
|
||||
editor = renderCronEditor(node.querySelector('input'));
|
||||
|
||||
return editor || node;
|
||||
},
|
||||
|
||||
renderCronSelectorRow(/* ... */) {
|
||||
return form.Value.prototype.render.apply(this, arguments);
|
||||
},
|
||||
|
||||
generateRand(type, length) {
|
||||
let byteArr;
|
||||
if (['base64', 'hex'].includes(type))
|
||||
@@ -208,6 +426,9 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
loadDefaultLabel(uciconfig, ucisection) {
|
||||
if (!ucisection)
|
||||
return '';
|
||||
|
||||
let label = uci.get(uciconfig, ucisection, 'label');
|
||||
if (label) {
|
||||
return label;
|
||||
@@ -218,32 +439,127 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
loadModalTitle(title, addtitle, uciconfig, ucisection) {
|
||||
if (!ucisection)
|
||||
return addtitle;
|
||||
|
||||
let label = uci.get(uciconfig, ucisection, 'label');
|
||||
return label ? title + ' » ' + label : addtitle;
|
||||
},
|
||||
|
||||
normalizeSectionId(label, prefix) {
|
||||
let section_id = (label || '').trim().replace(/[^A-Za-z0-9_]/g, '_').replace(/^_+|_+$/g, '');
|
||||
return (prefix || 'cfg') + '_' + (section_id || 'section');
|
||||
},
|
||||
|
||||
renderSectionAdd(section, extra_class) {
|
||||
let el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
|
||||
nameEl = el.querySelector('.cbi-section-create-name');
|
||||
ui.addValidator(nameEl, 'uciname', true, (v) => {
|
||||
let button = el.querySelector('.cbi-section-create > .cbi-button-add');
|
||||
let uciconfig = section.uciconfig || section.map.config;
|
||||
nameEl = el.querySelector('.cbi-section-create-name'),
|
||||
button = el.querySelector('.cbi-section-create > .cbi-button-add'),
|
||||
uciconfig = section.uciconfig || section.map.config,
|
||||
sectiontype = section.sectiontype,
|
||||
labelEl = E('input', {
|
||||
'type': 'text',
|
||||
'class': nameEl.className
|
||||
}),
|
||||
hintEl = E('div', { 'class': 'cbi-value-description' });
|
||||
|
||||
if (!v) {
|
||||
nameEl.style.display = 'none';
|
||||
nameEl.parentNode.insertBefore(labelEl, nameEl);
|
||||
nameEl.parentNode.appendChild(hintEl);
|
||||
button.disabled = true;
|
||||
|
||||
const syncSectionName = () => {
|
||||
let label = (labelEl.value || '').trim();
|
||||
hintEl.textContent = '';
|
||||
|
||||
if (!label) {
|
||||
nameEl.value = '';
|
||||
button.disabled = true;
|
||||
return true;
|
||||
} else if (uci.get(uciconfig, v)) {
|
||||
button.disabled = true;
|
||||
return _('Expecting: %s').format(_('unique UCI identifier'));
|
||||
} else {
|
||||
button.disabled = null;
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
}, 'blur', 'keyup');
|
||||
|
||||
let duplicate = false;
|
||||
uci.sections(uciconfig, sectiontype, (res) => {
|
||||
if ((res.label || res['.name']) === label)
|
||||
duplicate = true;
|
||||
});
|
||||
|
||||
if (duplicate) {
|
||||
nameEl.value = '';
|
||||
button.disabled = true;
|
||||
hintEl.textContent = _('Expecting: %s').format(_('unique value'));
|
||||
return;
|
||||
}
|
||||
|
||||
let normalized = (label || '').trim().replace(/[^A-Za-z0-9_]/g, '_').replace(/^_+|_+$/g, '') || 'section',
|
||||
section_id = (sectiontype || 'cfg') + '_' + normalized,
|
||||
suffix = 1;
|
||||
while (uci.get(uciconfig, section_id))
|
||||
section_id = (sectiontype || 'cfg') + '_' + normalized + '_' + suffix++;
|
||||
|
||||
nameEl.value = section_id;
|
||||
nameEl.dataset.sectionId = section_id;
|
||||
nameEl.dataset.sectionLabel = label;
|
||||
button.disabled = null;
|
||||
};
|
||||
|
||||
labelEl.addEventListener('input', syncSectionName);
|
||||
labelEl.addEventListener('blur', syncSectionName);
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
syncSectionName();
|
||||
|
||||
let label = nameEl.dataset.sectionLabel,
|
||||
section_id = nameEl.dataset.sectionId;
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (label && section_id && uci.get(uciconfig, section_id))
|
||||
uci.set(uciconfig, section_id, 'label', label);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
installCloseButtonText() {
|
||||
if (window.__homeproxyCloseButtonText)
|
||||
return;
|
||||
|
||||
window.__homeproxyCloseButtonText = true;
|
||||
|
||||
let fix = (root) => {
|
||||
for (let el of (root || document).querySelectorAll('button, .btn, input[type="button"], input[type="submit"]')) {
|
||||
let text = (el.textContent || el.value || '').trim();
|
||||
if (text !== 'Dismiss' && text !== '\u5ffd\u7565')
|
||||
continue;
|
||||
|
||||
if ('value' in el && !el.textContent.trim())
|
||||
el.value = _('Close');
|
||||
else
|
||||
el.textContent = _('Close');
|
||||
}
|
||||
};
|
||||
|
||||
let observe = () => {
|
||||
if (!document.body)
|
||||
return;
|
||||
|
||||
fix(document);
|
||||
new MutationObserver((mutations) => {
|
||||
for (let mutation of mutations)
|
||||
for (let node of mutation.addedNodes)
|
||||
if (node.nodeType === 1)
|
||||
fix(node);
|
||||
fix(document);
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
};
|
||||
|
||||
if (document.body)
|
||||
observe();
|
||||
else
|
||||
document.addEventListener('DOMContentLoaded', observe, { once: true });
|
||||
},
|
||||
|
||||
uploadCertificate(_option, type, filename, ev) {
|
||||
const callWriteCertificate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
@@ -264,6 +580,44 @@ return baseclass.extend({
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message)) });
|
||||
},
|
||||
|
||||
uploadPanel(_option, ev) {
|
||||
const callInstallPanel = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'clash_api_install_panel',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const normalizeMessage = (message) => {
|
||||
switch (message) {
|
||||
case 'panel_backup_failed':
|
||||
return _('Backup panel failed.');
|
||||
case 'panel_restore_failed':
|
||||
return _('Restore panel failed.');
|
||||
case 'empty_panel_zip':
|
||||
return _('Panel ZIP file is empty.');
|
||||
case 'unzip_unavailable':
|
||||
return _('The unzip command is unavailable.');
|
||||
case 'invalid_panel_zip':
|
||||
return _('Invalid panel ZIP file.');
|
||||
case 'panel_install_failed':
|
||||
return _('Install panel failed.');
|
||||
default:
|
||||
return message || _('unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
return ui.uploadFile('/tmp/homeproxy_panel.tmp', ev.target)
|
||||
.then(L.bind((_btn, res) => {
|
||||
return L.resolveDefault(callInstallPanel(), {}).then((ret) => {
|
||||
if (ret.result === true)
|
||||
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(_('panel ZIP package'), res.size)), 'info');
|
||||
else
|
||||
ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(_('panel ZIP package'), normalizeMessage(ret.error))), 'danger');
|
||||
});
|
||||
}, this, ev.target))
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message || e), 'danger') });
|
||||
},
|
||||
|
||||
validateBase64Key(length, section_id, value) {
|
||||
/* Thanks to luci-proto-wireguard */
|
||||
if (section_id && value)
|
||||
@@ -304,7 +658,7 @@ return baseclass.extend({
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
if (ucioption === 'node' && value === 'urltest')
|
||||
if (ucioption === 'node' && (value === 'urltest' || value === 'selector'))
|
||||
return true;
|
||||
|
||||
let duplicate = false;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -7,6 +7,7 @@
|
||||
'use strict';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
@@ -14,11 +15,207 @@
|
||||
'require homeproxy as hp';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
const callNodeLatencyTest = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'node_latency_test',
|
||||
params: ['node', 'mode'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callRCInit = rpc.declare({
|
||||
object: 'rc',
|
||||
method: 'init',
|
||||
params: ['name', 'action'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function normalizeList(value) {
|
||||
return Array.isArray(value) ? value : (value ? [value] : []);
|
||||
}
|
||||
|
||||
function getSubscriptionInfo(config) {
|
||||
const urls = normalizeList(uci.get(config, 'subscription', 'subscription_url')),
|
||||
names = normalizeList(uci.get(config, 'subscription', 'subscription_name')),
|
||||
info = [];
|
||||
|
||||
for (let i = 0; i < urls.length; i++) {
|
||||
const url = urls[i],
|
||||
cleanUrl = String(url || '').replace(/#.*$/, ''),
|
||||
title = String(names[i] || '').trim();
|
||||
|
||||
if (!url)
|
||||
continue;
|
||||
|
||||
info.push({
|
||||
hash: hp.calcStringMD5(cleanUrl),
|
||||
title,
|
||||
url
|
||||
});
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
const SubscriptionList = form.Value.extend({
|
||||
__name__: 'CBI.HomeProxySubscriptionList',
|
||||
|
||||
cfgvalue(section_id) {
|
||||
return {
|
||||
urls: normalizeList(uci.get('homeproxy', section_id, 'subscription_url')),
|
||||
names: normalizeList(uci.get('homeproxy', section_id, 'subscription_name'))
|
||||
};
|
||||
},
|
||||
|
||||
renderWidget(section_id, _option_index, cfgvalue) {
|
||||
const value = cfgvalue || { urls: [], names: [] },
|
||||
rows = E('div', {
|
||||
id: this.cbid(section_id),
|
||||
'class': 'homeproxy-subscription-list',
|
||||
'style': 'display: flex; flex-direction: column; gap: .45em; width: min(100%, 42em);'
|
||||
});
|
||||
|
||||
rows.appendChild(E('div', {
|
||||
'class': 'homeproxy-subscription-header',
|
||||
'style': 'display: grid; grid-template-columns: minmax(8em, 13em) minmax(16em, 1fr) auto; gap: .35em; color: var(--text-muted, #6b7280); font-size: 90%;'
|
||||
}, [
|
||||
E('span', {}, [ _('\u8ba2\u9605\u540d\u79f0') ]),
|
||||
E('span', {}, [ _('\u8ba2\u9605\u5730\u5740') ]),
|
||||
E('span')
|
||||
]));
|
||||
|
||||
const addRow = (name, url) => {
|
||||
const row = E('div', {
|
||||
'class': 'homeproxy-subscription-row',
|
||||
'style': 'display: grid; grid-template-columns: minmax(8em, 13em) minmax(16em, 1fr) auto; gap: .35em; align-items: stretch;'
|
||||
}, [
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text subscription-name',
|
||||
'placeholder': _('\u8ba2\u9605\u540d\u79f0'),
|
||||
'value': name || '',
|
||||
'style': 'width: 100%; box-sizing: border-box;'
|
||||
}),
|
||||
E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text subscription-url',
|
||||
'placeholder': _('\u8ba2\u9605\u5730\u5740'),
|
||||
'value': url || '',
|
||||
'style': 'width: 100%; box-sizing: border-box;'
|
||||
}),
|
||||
E('button', {
|
||||
'type': 'button',
|
||||
'class': 'cbi-button cbi-button-remove',
|
||||
'click': () => {
|
||||
row.remove();
|
||||
if (!rows.querySelector('.homeproxy-subscription-row'))
|
||||
addRow('', '');
|
||||
}
|
||||
}, [ '-' ])
|
||||
]);
|
||||
|
||||
if (rows._addButton)
|
||||
rows.insertBefore(row, rows._addButton);
|
||||
else
|
||||
rows.appendChild(row);
|
||||
};
|
||||
|
||||
for (let i = 0; i < Math.max(value.urls.length, 1); i++)
|
||||
addRow(value.names[i], value.urls[i]);
|
||||
|
||||
rows._addButton = E('button', {
|
||||
'type': 'button',
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
'style': 'width: 3em;',
|
||||
'click': () => addRow('', '')
|
||||
}, [ '+' ]);
|
||||
rows.appendChild(rows._addButton);
|
||||
|
||||
return rows;
|
||||
},
|
||||
|
||||
formvalue(section_id) {
|
||||
const node = document.getElementById(this.cbid(section_id)),
|
||||
names = [],
|
||||
urls = [];
|
||||
|
||||
node?.querySelectorAll('.homeproxy-subscription-row').forEach((row) => {
|
||||
const name = row.querySelector('.subscription-name')?.value.trim() || '',
|
||||
url = row.querySelector('.subscription-url')?.value.trim() || '';
|
||||
|
||||
if (!name && !url)
|
||||
return;
|
||||
|
||||
names.push(name);
|
||||
urls.push(url);
|
||||
});
|
||||
|
||||
return { names, urls };
|
||||
},
|
||||
|
||||
validate(_section_id, value) {
|
||||
const names = value?.names || [],
|
||||
urls = value?.urls || [],
|
||||
usedNames = {},
|
||||
usedUrls = {};
|
||||
|
||||
if (!names.length || !urls.length)
|
||||
return _('\u8ba2\u9605\u540d\u79f0\u548c\u8ba2\u9605\u5730\u5740\u4e3a\u5fc5\u586b\u9879');
|
||||
|
||||
for (let i = 0; i < Math.max(names.length, urls.length); i++) {
|
||||
const name = String(names[i] || '').trim(),
|
||||
url = String(urls[i] || '').trim(),
|
||||
normalizedUrl = url.replace(/#.*$/, '');
|
||||
|
||||
if (!name || !url)
|
||||
return _('\u8ba2\u9605\u540d\u79f0\u548c\u8ba2\u9605\u5730\u5740\u5fc5\u987b\u6210\u5bf9\u586b\u5199');
|
||||
|
||||
if (usedNames[name])
|
||||
return _('\u8ba2\u9605\u540d\u79f0\u4e0d\u80fd\u91cd\u590d');
|
||||
usedNames[name] = true;
|
||||
|
||||
if (usedUrls[normalizedUrl])
|
||||
return _('\u8ba2\u9605\u5730\u5740\u4e0d\u80fd\u91cd\u590d');
|
||||
usedUrls[normalizedUrl] = true;
|
||||
|
||||
try {
|
||||
let parsed = new URL(url);
|
||||
if (!parsed.hostname)
|
||||
return _('Expecting: %s').format(_('valid URL'));
|
||||
} catch(e) {
|
||||
return _('Expecting: %s').format(_('valid URL'));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
write(section_id, value) {
|
||||
if (value?.urls?.length) {
|
||||
uci.set('homeproxy', section_id, 'subscription_url', value.urls);
|
||||
uci.set('homeproxy', section_id, 'subscription_name', value.names);
|
||||
} else {
|
||||
uci.unset('homeproxy', section_id, 'subscription_url');
|
||||
uci.unset('homeproxy', section_id, 'subscription_name');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function allowInsecureConfirm(ev, _section_id, value) {
|
||||
if (value === '1' && !confirm(_('Are you sure to allow insecure?')))
|
||||
ev.target.firstElementChild.checked = null;
|
||||
}
|
||||
|
||||
function normalizeHysteriaHoppingPort(mport) {
|
||||
if (!mport)
|
||||
return null;
|
||||
|
||||
const ports = mport.split(',')
|
||||
.map((p) => p.trim())
|
||||
.filter((p) => p.length)
|
||||
.map((p) => (/^\d+$/.test(p) ? `${p}:${p}` : p.replace('-', ':')));
|
||||
return ports.length ? ports : null;
|
||||
}
|
||||
|
||||
function parseShareLink(uri, features) {
|
||||
let config, url, params;
|
||||
|
||||
@@ -29,6 +226,11 @@ function parseShareLink(uri, features) {
|
||||
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
|
||||
url = new URL('http://' + uri[1]);
|
||||
params = url.searchParams;
|
||||
let anytls_fp = params.get('fp');
|
||||
if (['none', 'disable', 'disabled'].includes(anytls_fp))
|
||||
anytls_fp = null;
|
||||
else if (!anytls_fp)
|
||||
anytls_fp = 'chrome';
|
||||
|
||||
/* Check if password exists */
|
||||
if (!url.username)
|
||||
@@ -42,7 +244,8 @@ function parseShareLink(uri, features) {
|
||||
password: url.username ? decodeURIComponent(url.username) : null,
|
||||
tls: '1',
|
||||
tls_sni: params.get('sni'),
|
||||
tls_insecure: (params.get('insecure') === '1') ? '1' : '0'
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_utls: features.with_utls ? anytls_fp : null
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -75,6 +278,7 @@ function parseShareLink(uri, features) {
|
||||
type: 'hysteria',
|
||||
address: url.hostname,
|
||||
port: url.port || '80',
|
||||
hysteria_hopping_port: normalizeHysteriaHoppingPort(params.get('mport')),
|
||||
hysteria_protocol: params.get('protocol') || 'udp',
|
||||
hysteria_auth_type: params.get('auth') ? 'string' : null,
|
||||
hysteria_auth_payload: params.get('auth'),
|
||||
@@ -82,9 +286,9 @@ function parseShareLink(uri, features) {
|
||||
hysteria_down_mbps: params.get('downmbps'),
|
||||
hysteria_up_mbps: params.get('upmbps'),
|
||||
tls: '1',
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('peer'),
|
||||
tls_alpn: params.get('alpn'),
|
||||
tls_insecure: (params.get('insecure') === '1') ? '1' : '0'
|
||||
tls_alpn: params.get('alpn')
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -105,11 +309,12 @@ function parseShareLink(uri, features) {
|
||||
password: url.username ? (
|
||||
decodeURIComponent(url.username + (url.password ? (':' + url.password) : ''))
|
||||
) : null,
|
||||
hysteria_hopping_port: normalizeHysteriaHoppingPort(params.get('mport')),
|
||||
hysteria_obfs_type: params.get('obfs'),
|
||||
hysteria_obfs_password: params.get('obfs-password'),
|
||||
tls: '1',
|
||||
tls_sni: params.get('sni'),
|
||||
tls_insecure: params.get('insecure') ? '1' : '0'
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('sni')
|
||||
};
|
||||
|
||||
break;
|
||||
@@ -212,6 +417,7 @@ function parseShareLink(uri, features) {
|
||||
password: decodeURIComponent(url.username),
|
||||
transport: params.get('type') !== 'tcp' ? params.get('type') : null,
|
||||
tls: '1',
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('sni')
|
||||
};
|
||||
switch (params.get('type')) {
|
||||
@@ -249,6 +455,7 @@ function parseShareLink(uri, features) {
|
||||
tuic_congestion_control: params.get('congestion_control'),
|
||||
tuic_udp_relay_mode: params.get('udp_relay_mode'),
|
||||
tls: '1',
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('sni'),
|
||||
tls_alpn: params.get('alpn') ? decodeURIComponent(params.get('alpn')).split(',') : null
|
||||
};
|
||||
@@ -276,6 +483,7 @@ function parseShareLink(uri, features) {
|
||||
uuid: url.username,
|
||||
transport: params.get('type') !== 'tcp' ? params.get('type') : null,
|
||||
tls: ['tls', 'xtls', 'reality'].includes(params.get('security')) ? '1' : '0',
|
||||
tls_insecure: (params.get('insecure') === '1' || params.get('allowInsecure') === '1') ? '1' : '0',
|
||||
tls_sni: params.get('sni'),
|
||||
tls_alpn: params.get('alpn') ? decodeURIComponent(params.get('alpn')).split(',') : null,
|
||||
tls_reality: (params.get('security') === 'reality') ? '1' : '0',
|
||||
@@ -388,13 +596,378 @@ function parseShareLink(uri, features) {
|
||||
return config;
|
||||
}
|
||||
|
||||
function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
const NODE_LATENCY_ROW_STATES = Object.freeze({
|
||||
UNTESTED: 'untested',
|
||||
TESTING: 'testing',
|
||||
SUCCESS: 'success',
|
||||
TIMEOUT: 'timeout',
|
||||
ERROR: 'error'
|
||||
});
|
||||
|
||||
const NODE_LATENCY_ROW_STATE_VALUES = Object.freeze(Object.values(NODE_LATENCY_ROW_STATES));
|
||||
const NODE_LATENCY_TEST_MODES = Object.freeze([ 'icmp', 'rp' ]);
|
||||
|
||||
function remapLegacyNodeLatencyTestMode(mode) {
|
||||
return mode === 'tcp' ? 'icmp' : mode;
|
||||
}
|
||||
|
||||
function normalizeNodeLatencyTestMode(mode) {
|
||||
mode = remapLegacyNodeLatencyTestMode(mode);
|
||||
return NODE_LATENCY_TEST_MODES.includes(mode) ? mode : 'icmp';
|
||||
}
|
||||
|
||||
function resolveNodeLatencyResultByMode(result, mode) {
|
||||
let active_mode = normalizeNodeLatencyTestMode(mode || result?.mode);
|
||||
let active_latency_ms = (result && result.latency_ms != null) ? result.latency_ms : null;
|
||||
let active_error = (result && result.error) ? result.error : null;
|
||||
let active_state = result?.state;
|
||||
|
||||
if (active_mode === 'icmp') {
|
||||
if (result && result.icmp_latency_ms != null)
|
||||
active_latency_ms = result.icmp_latency_ms;
|
||||
if (result && result.icmp_error)
|
||||
active_error = result.icmp_error;
|
||||
}
|
||||
|
||||
if (active_error?.code?.endsWith('_timeout'))
|
||||
active_state = NODE_LATENCY_ROW_STATES.TIMEOUT;
|
||||
else if (active_error)
|
||||
active_state = NODE_LATENCY_ROW_STATES.ERROR;
|
||||
else if (active_latency_ms != null)
|
||||
active_state = NODE_LATENCY_ROW_STATES.SUCCESS;
|
||||
|
||||
return {
|
||||
mode: active_mode,
|
||||
state: active_state,
|
||||
latency_ms: active_latency_ms,
|
||||
error: active_error
|
||||
};
|
||||
}
|
||||
|
||||
function getNodeLatencyErrorText(error_code) {
|
||||
switch (error_code) {
|
||||
case 'backend_execution failed':
|
||||
return _('Backend execution failed');
|
||||
case 'icmp_failed':
|
||||
return _('ICMP Failed');
|
||||
case 'icmp_probe_unavailable':
|
||||
return _('ICMP Probe unavailable');
|
||||
case 'icmp_timeout':
|
||||
return _('ICMP Timeout');
|
||||
case 'invalid_node':
|
||||
return _('Invalid node');
|
||||
case 'invalid_response':
|
||||
return _('Invalid response');
|
||||
case 'probe_failed':
|
||||
return _('Probe failed');
|
||||
case 'rpc_failed':
|
||||
return _('RPC failed');
|
||||
default:
|
||||
return _('Error');
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeLatencyStatusText(row_state) {
|
||||
if (!row_state)
|
||||
return _('Not Tested');
|
||||
|
||||
switch (row_state.state) {
|
||||
case NODE_LATENCY_ROW_STATES.TESTING:
|
||||
return _('Testing');
|
||||
case NODE_LATENCY_ROW_STATES.SUCCESS:
|
||||
return (row_state.latency_ms != null) ? _('%s ms').format(row_state.latency_ms) : _('Success');
|
||||
case NODE_LATENCY_ROW_STATES.TIMEOUT:
|
||||
return _('Timeout');
|
||||
case NODE_LATENCY_ROW_STATES.ERROR:
|
||||
return getNodeLatencyErrorText(row_state.error?.code);
|
||||
case NODE_LATENCY_ROW_STATES.UNTESTED:
|
||||
default:
|
||||
return _('Not Tested');
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeLatencyStatusStyle(row_state) {
|
||||
if (!row_state)
|
||||
return 'color:gray';
|
||||
|
||||
switch (row_state.state) {
|
||||
case NODE_LATENCY_ROW_STATES.TESTING:
|
||||
return 'color:#0a84ff';
|
||||
case NODE_LATENCY_ROW_STATES.SUCCESS:
|
||||
return 'color:green';
|
||||
case NODE_LATENCY_ROW_STATES.TIMEOUT:
|
||||
return 'color:#ff8c00';
|
||||
case NODE_LATENCY_ROW_STATES.ERROR:
|
||||
return 'color:red';
|
||||
case NODE_LATENCY_ROW_STATES.UNTESTED:
|
||||
default:
|
||||
return 'color:gray';
|
||||
}
|
||||
}
|
||||
|
||||
function renderNodeLatencyStatus(row_state) {
|
||||
return '<strong style="%s">%s</strong>'.format(getNodeLatencyStatusStyle(row_state), getNodeLatencyStatusText(row_state));
|
||||
}
|
||||
|
||||
function renderNodeLatencyStatusNode(row_state, attrs) {
|
||||
return E('strong', Object.assign({
|
||||
'style': getNodeLatencyStatusStyle(row_state)
|
||||
}, attrs || {}), [ getNodeLatencyStatusText(row_state) ]);
|
||||
}
|
||||
|
||||
function getNodeLatencyActionTitle(row_state) {
|
||||
return (row_state?.state === NODE_LATENCY_ROW_STATES.TESTING) ? _('Testing') : _('Test');
|
||||
}
|
||||
|
||||
function parseNodeLatencySectionId(widget_id) {
|
||||
let matched = String(widget_id || '').match(/^cbi-homeproxy-(.*)-_test_latency$/);
|
||||
return matched ? matched[1] : null;
|
||||
}
|
||||
|
||||
function parseActiveSubscriptionTabId(value) {
|
||||
let matched = String(value || '').match(/(?:^|[^\w])(sub_[0-9a-f]+)(?:$|[^\w])/i);
|
||||
return matched ? matched[1] : null;
|
||||
}
|
||||
|
||||
function parseActiveNodeListTabId(value) {
|
||||
let matched = String(value || '').match(/(?:^|[^\w])(node|subscription|sub_[0-9a-f]+)(?:$|[^\w])/i);
|
||||
return matched ? matched[1].toLowerCase() : null;
|
||||
}
|
||||
|
||||
function getActiveNodeListTabId(root_el) {
|
||||
let active_tab = null;
|
||||
let active_tab_nodes = root_el.querySelectorAll('.cbi-tabmenu li.cbi-tab-active, .cbi-tabmenu li.active, .cbi-tabmenu [aria-selected="true"]');
|
||||
|
||||
for (let node of active_tab_nodes) {
|
||||
let tab_id = parseActiveNodeListTabId(node?.getAttribute('data-tab'))
|
||||
|| parseActiveNodeListTabId(node?.dataset?.tab)
|
||||
|| parseActiveNodeListTabId(node?.getAttribute('id'));
|
||||
if (!tab_id && node?.querySelector)
|
||||
tab_id = parseActiveNodeListTabId(node.querySelector('a')?.getAttribute('href'))
|
||||
|| parseActiveNodeListTabId(node.querySelector('a')?.getAttribute('data-tab'));
|
||||
|
||||
if (tab_id) {
|
||||
active_tab = tab_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!active_tab) {
|
||||
let fallback_tab_nodes = root_el.querySelectorAll('.cbi-tabmenu li.cbi-tab[data-tab], .cbi-tabmenu li[data-tab]:not(.cbi-tab-disabled)');
|
||||
|
||||
for (let node of fallback_tab_nodes) {
|
||||
let tab_id = parseActiveNodeListTabId(node?.getAttribute('data-tab')) || parseActiveNodeListTabId(node?.dataset?.tab);
|
||||
if (tab_id) {
|
||||
active_tab = tab_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return active_tab;
|
||||
}
|
||||
|
||||
function shouldShowBulkLatencyButton(root_el) {
|
||||
let active_tab = getActiveNodeListTabId(root_el);
|
||||
|
||||
if (!active_tab)
|
||||
return false;
|
||||
|
||||
return !!parseActiveSubscriptionTabId(active_tab);
|
||||
}
|
||||
|
||||
function getActiveSubscriptionTabId(root_el) {
|
||||
let active_tab = getActiveNodeListTabId(root_el);
|
||||
return parseActiveSubscriptionTabId(active_tab);
|
||||
}
|
||||
|
||||
function getActiveSubscriptionTabContainer(root_el, active_tab) {
|
||||
if (!active_tab)
|
||||
return null;
|
||||
|
||||
let candidates = [];
|
||||
for (let selector of [
|
||||
'.cbi-tabcontainer[data-tab="%s"]'.format(active_tab),
|
||||
'[data-tab="%s"]'.format(active_tab),
|
||||
'[id="tab.%s"]'.format(active_tab),
|
||||
'[id="tab-%s"]'.format(active_tab),
|
||||
'[id="%s"]'.format(active_tab)
|
||||
]) {
|
||||
candidates = candidates.concat(Array.from(root_el.querySelectorAll(selector)));
|
||||
}
|
||||
|
||||
return candidates.find((node) => !node.closest('.cbi-tabmenu') && node.querySelector('div[id$="-_test_latency"]')) || null;
|
||||
}
|
||||
|
||||
function getActiveSubscriptionLatencySectionIds(root_el) {
|
||||
let active_tab = getActiveSubscriptionTabId(root_el);
|
||||
let active_container = getActiveSubscriptionTabContainer(root_el, active_tab);
|
||||
if (!active_container)
|
||||
return [];
|
||||
|
||||
let widgets = Array.from(active_container.querySelectorAll('div[id$="-_test_latency"]'));
|
||||
return widgets.map((node) => parseNodeLatencySectionId(node.id)).filter((sid) => !!sid);
|
||||
}
|
||||
|
||||
function createNodeLatencyRowStateModel() {
|
||||
let rows = Object.create(null);
|
||||
|
||||
const buildRowState = (state, result, mode) => {
|
||||
let resolved_result = resolveNodeLatencyResultByMode(result, mode);
|
||||
|
||||
return {
|
||||
state: state,
|
||||
mode: resolved_result.mode,
|
||||
latency_ms: resolved_result.latency_ms,
|
||||
icmp_latency_ms: (result && result.icmp_latency_ms != null) ? result.icmp_latency_ms : null,
|
||||
measured_at: (result && result.measured_at) ? result.measured_at : null,
|
||||
error: resolved_result.error,
|
||||
icmp_error: (result && result.icmp_error) ? result.icmp_error : null
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeState = (state) => {
|
||||
if (NODE_LATENCY_ROW_STATE_VALUES.includes(state))
|
||||
return state;
|
||||
|
||||
return NODE_LATENCY_ROW_STATES.ERROR;
|
||||
};
|
||||
|
||||
const ensure = (section_id) => {
|
||||
if (!section_id)
|
||||
return buildRowState(NODE_LATENCY_ROW_STATES.UNTESTED, null, 'icmp');
|
||||
|
||||
if (!rows[section_id])
|
||||
rows[section_id] = buildRowState(NODE_LATENCY_ROW_STATES.UNTESTED, null, 'icmp');
|
||||
|
||||
return rows[section_id];
|
||||
};
|
||||
|
||||
return {
|
||||
states: NODE_LATENCY_ROW_STATES,
|
||||
|
||||
get: (section_id) => ensure(section_id),
|
||||
|
||||
set: (section_id, state, result, mode) => {
|
||||
if (!section_id)
|
||||
return null;
|
||||
|
||||
rows[section_id] = buildRowState(normalizeState(state), result, mode);
|
||||
return rows[section_id];
|
||||
},
|
||||
|
||||
setFromProbeResult: (section_id, result, mode) => {
|
||||
if (!section_id)
|
||||
return null;
|
||||
|
||||
let resolved_result = resolveNodeLatencyResultByMode(result, mode);
|
||||
let state = resolved_result.state ? normalizeState(resolved_result.state) : NODE_LATENCY_ROW_STATES.ERROR;
|
||||
rows[section_id] = buildRowState(state, result, mode);
|
||||
return rows[section_id];
|
||||
},
|
||||
|
||||
clear: (section_id) => {
|
||||
if (!section_id)
|
||||
return;
|
||||
|
||||
delete rows[section_id];
|
||||
},
|
||||
|
||||
reset: () => {
|
||||
rows = Object.create(null);
|
||||
},
|
||||
|
||||
snapshot: () => rows
|
||||
};
|
||||
}
|
||||
|
||||
function renderNodeSettings(section, data, features, main_node, routing_mode, node_latency_row_state) {
|
||||
let s = section, o;
|
||||
if (typeof globalThis !== 'undefined') {
|
||||
globalThis.__hpNodeLatencySections = globalThis.__hpNodeLatencySections || {};
|
||||
globalThis.__hpNodeLatencyTrigger = function(section_id) {
|
||||
let target = globalThis.__hpNodeLatencySections?.[section_id];
|
||||
return target ? target.handleNodeLatencyTest(section_id) : false;
|
||||
};
|
||||
}
|
||||
s.rowcolors = true;
|
||||
s.sortable = true;
|
||||
s.nodescriptions = true;
|
||||
s.max_cols = 7;
|
||||
s.modaltitle = L.bind(hp.loadModalTitle, this, _('Node'), _('Add a node'), data[0]);
|
||||
s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
s.node_latency_row_state = node_latency_row_state;
|
||||
s.getNodeLatencyRowState = function(section_id) {
|
||||
return node_latency_row_state.get(section_id);
|
||||
}
|
||||
s.getNodeLatencyTestMode = function() {
|
||||
let mode_option = this.map.lookupOption('latency_test_mode', 'subscription')?.[0];
|
||||
let mode_value = mode_option ? mode_option.formvalue('subscription') : uci.get(data[0], 'subscription', 'latency_test_mode');
|
||||
|
||||
return normalizeNodeLatencyTestMode(mode_value);
|
||||
}
|
||||
s.syncNodeLatencyTestMode = function(mode) {
|
||||
let normalized_mode = normalizeNodeLatencyTestMode(mode);
|
||||
let saved_mode_value = uci.get(data[0], 'subscription', 'latency_test_mode');
|
||||
let saved_mode = normalizeNodeLatencyTestMode(saved_mode_value);
|
||||
|
||||
if (saved_mode === normalized_mode && saved_mode_value !== 'tcp')
|
||||
return Promise.resolve(normalized_mode);
|
||||
|
||||
uci.set(data[0], 'subscription', 'latency_test_mode', normalized_mode);
|
||||
return uci.save().then(() => normalized_mode);
|
||||
}
|
||||
s.setNodeLatencyRowState = function(section_id, state, result, mode) {
|
||||
return node_latency_row_state.set(section_id, state, result, mode);
|
||||
}
|
||||
s.setNodeLatencyRowStateFromProbeResult = function(section_id, result, mode) {
|
||||
return node_latency_row_state.setFromProbeResult(section_id, result, mode);
|
||||
}
|
||||
s.refreshNodeLatencyRow = function(section_id) {
|
||||
let row_state = this.getNodeLatencyRowState(section_id);
|
||||
let latency_id = 'cbi-%s-%s-_latency'.format(data[0], section_id);
|
||||
let test_id = 'cbi-%s-%s-_test_latency'.format(data[0], section_id);
|
||||
|
||||
let status_widget = this.map.findElement('id', latency_id);
|
||||
if (status_widget)
|
||||
status_widget.innerHTML = renderNodeLatencyStatus(row_state);
|
||||
|
||||
let test_widget = this.map.findElement('id', test_id);
|
||||
let test_button = test_widget ? test_widget.querySelector('button') : null;
|
||||
if (test_button) {
|
||||
test_button.textContent = getNodeLatencyActionTitle(row_state);
|
||||
test_button.disabled = (row_state.state === NODE_LATENCY_ROW_STATES.TESTING) ? true : null;
|
||||
}
|
||||
}
|
||||
|
||||
s.handleNodeLatencyTest = function(section_id) {
|
||||
let mode = this.getNodeLatencyTestMode();
|
||||
this.setNodeLatencyRowState(section_id, NODE_LATENCY_ROW_STATES.TESTING, null, mode);
|
||||
this.refreshNodeLatencyRow(section_id);
|
||||
|
||||
return this.syncNodeLatencyTestMode(mode).then((mode) => L.resolveDefault(callNodeLatencyTest(section_id, mode), null).then((result) => {
|
||||
if (result?.node === section_id)
|
||||
this.setNodeLatencyRowStateFromProbeResult(section_id, result, mode);
|
||||
else
|
||||
this.setNodeLatencyRowState(section_id, NODE_LATENCY_ROW_STATES.ERROR, {
|
||||
mode: mode,
|
||||
error: {
|
||||
code: 'invalid_response',
|
||||
message: 'unexpected node latency response'
|
||||
}
|
||||
}, mode);
|
||||
})).catch((err) => {
|
||||
this.setNodeLatencyRowState(section_id, NODE_LATENCY_ROW_STATES.ERROR, {
|
||||
mode: mode,
|
||||
error: {
|
||||
code: 'rpc_failed',
|
||||
message: String(err)
|
||||
}
|
||||
}, mode);
|
||||
}).then(() => {
|
||||
this.refreshNodeLatencyRow(section_id);
|
||||
});
|
||||
}
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
o = s.option(form.Button, '_apply', _('Apply'));
|
||||
@@ -419,6 +992,41 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
}
|
||||
}
|
||||
|
||||
o = s.option(form.DummyValue, '_latency', _('Latency'));
|
||||
o.rawhtml = true;
|
||||
o.modalonly = false;
|
||||
o.textvalue = function(section_id) {
|
||||
return E('span', { 'id': 'cbi-%s-%s-_latency'.format(data[0], section_id) }, [
|
||||
renderNodeLatencyStatusNode(this.section.getNodeLatencyRowState(section_id))
|
||||
]);
|
||||
}
|
||||
|
||||
o = s.option(form.Button, '_test_latency', _('Test'));
|
||||
o.editable = true;
|
||||
o.modalonly = false;
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = function(section_id) {
|
||||
return getNodeLatencyActionTitle(this.section.getNodeLatencyRowState(section_id));
|
||||
}
|
||||
o.renderWidget = function(section_id, _option_index, cfgvalue) {
|
||||
let hiddenEl = new ui.Hiddenfield((cfgvalue != null) ? cfgvalue : '', {
|
||||
id: this.cbid(section_id)
|
||||
});
|
||||
let outputEl = E('output', { 'for': this.cbid(section_id) });
|
||||
let row_state = s.getNodeLatencyRowState(section_id);
|
||||
|
||||
if (typeof globalThis !== 'undefined' && globalThis.__hpNodeLatencySections)
|
||||
globalThis.__hpNodeLatencySections[section_id] = s;
|
||||
|
||||
outputEl.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'onclick': 'return globalThis.__hpNodeLatencyTrigger(%s);'.format(JSON.stringify(section_id)),
|
||||
'disabled': (row_state.state === NODE_LATENCY_ROW_STATES.TESTING) ? true : null
|
||||
}, [ getNodeLatencyActionTitle(row_state) ]));
|
||||
|
||||
return E([ outputEl, hiddenEl.render() ]);
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'label', _('Label'));
|
||||
o.load = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'node', 'label');
|
||||
@@ -443,6 +1051,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
o.value('wireguard', _('WireGuard'));
|
||||
o.value('vless', _('VLESS'));
|
||||
o.value('vmess', _('VMess'));
|
||||
o.default = 'direct';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'address', _('Address'));
|
||||
@@ -493,15 +1102,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
/* Direct config */
|
||||
o = s.option(form.ListValue, 'proxy_protocol', _('Proxy protocol'),
|
||||
_('Write proxy protocol in the connection header.'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('1', _('v1'));
|
||||
o.value('2', _('v2'));
|
||||
o.depends('type', 'direct');
|
||||
o.modalonly = true;
|
||||
|
||||
/* AnyTLS config start */
|
||||
o = s.option(form.Value, 'anytls_idle_session_check_interval', _('Idle session check interval'),
|
||||
_('Interval checking for idle sessions, in seconds.'));
|
||||
@@ -746,7 +1346,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'vmess_encrypt', _('Encrypt method'));
|
||||
o.value('auto');
|
||||
o.value('auto', _('Default'));
|
||||
o.value('none');
|
||||
o.value('zero');
|
||||
o.value('aes-128-gcm');
|
||||
@@ -1194,19 +1794,18 @@ return view.extend({
|
||||
},
|
||||
|
||||
render(data) {
|
||||
let m, s, o, ss, so;
|
||||
let m, s, o, ss, so, subscription_list_option;
|
||||
let main_node = uci.get(data[0], 'config', 'main_node');
|
||||
let routing_mode = uci.get(data[0], 'config', 'routing_mode');
|
||||
let features = data[1];
|
||||
let node_latency_row_state = createNodeLatencyRowStateModel();
|
||||
|
||||
hp.installCloseButtonText();
|
||||
|
||||
this.node_latency_row_state = node_latency_row_state;
|
||||
|
||||
/* Cache subscription information, it will be called multiple times */
|
||||
let subinfo = [];
|
||||
for (let suburl of (uci.get(data[0], 'subscription', 'subscription_url') || [])) {
|
||||
const url = new URL(suburl);
|
||||
const urlhash = hp.calcStringMD5(suburl.replace(/#.*$/, ''));
|
||||
const title = url.hash ? decodeURIComponent(url.hash.slice(1)) : url.hostname;
|
||||
subinfo.push({ 'hash': urlhash, 'title': title });
|
||||
}
|
||||
let subinfo = getSubscriptionInfo(data[0]);
|
||||
|
||||
m = new form.Map('homeproxy', _('Edit nodes'));
|
||||
|
||||
@@ -1216,7 +1815,7 @@ return view.extend({
|
||||
/* User nodes start */
|
||||
s.tab('node', _('Nodes'));
|
||||
o = s.taboption('node', form.SectionValue, '_node', form.GridSection, 'node');
|
||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
|
||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode, node_latency_row_state);
|
||||
ss.addremove = true;
|
||||
ss.filter = function(section_id) {
|
||||
for (let info of subinfo)
|
||||
@@ -1287,24 +1886,7 @@ return view.extend({
|
||||
])
|
||||
}
|
||||
ss.renderSectionAdd = function(/* ... */) {
|
||||
let el = form.GridSection.prototype.renderSectionAdd.apply(this, arguments),
|
||||
nameEl = el.querySelector('.cbi-section-create-name');
|
||||
|
||||
ui.addValidator(nameEl, 'uciname', true, (v) => {
|
||||
let button = el.querySelector('.cbi-section-create > .cbi-button-add');
|
||||
let uciconfig = this.uciconfig || this.map.config;
|
||||
|
||||
if (!v) {
|
||||
button.disabled = true;
|
||||
return true;
|
||||
} else if (uci.get(uciconfig, v)) {
|
||||
button.disabled = true;
|
||||
return _('Expecting: %s').format(_('unique UCI identifier'));
|
||||
} else {
|
||||
button.disabled = null;
|
||||
return true;
|
||||
}
|
||||
}, 'blur', 'keyup');
|
||||
let el = hp.renderSectionAdd(this, arguments[0]);
|
||||
|
||||
el.appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-add',
|
||||
@@ -1319,9 +1901,9 @@ return view.extend({
|
||||
|
||||
/* Subscription nodes start */
|
||||
for (const info of subinfo) {
|
||||
s.tab('sub_' + info.hash, _('Sub (%s)').format(info.title));
|
||||
s.tab('sub_' + info.hash, info.title ? _('Sub (%s)').format(info.title) : _('Sub'));
|
||||
o = s.taboption('sub_' + info.hash, form.SectionValue, '_sub_' + info.hash, form.GridSection, 'node');
|
||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
|
||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode, node_latency_row_state);
|
||||
ss.filter = function(section_id) {
|
||||
return (uci.get(data[0], section_id, 'grouphash') === info.hash);
|
||||
}
|
||||
@@ -1336,32 +1918,35 @@ return view.extend({
|
||||
_('Auto update subscriptions and geodata.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('subscription', form.ListValue, 'auto_update_time', _('Update time'));
|
||||
for (let i = 0; i < 24; i++)
|
||||
o.value(i, i + ':00');
|
||||
o.default = '2';
|
||||
o.depends('auto_update', '1');
|
||||
o = s.taboption('subscription', form.Value, 'auto_update_time', _('Update time (weekly)'));
|
||||
o.renderWidget = function() {
|
||||
return hp.renderCronSelector.apply(this, arguments);
|
||||
};
|
||||
o.default = '0 0 * * *';
|
||||
o.rmempty = false;
|
||||
o.validate = function(section_id, value) {
|
||||
value = value || this.formvalue(section_id);
|
||||
const matched = String(value || '').trim().match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+([0-7*])$/);
|
||||
if (!matched || +matched[1] > 59 || +matched[2] > 23)
|
||||
return _('Expecting: %s').format(_('valid cron expression'));
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.taboption('subscription', form.Flag, 'update_via_proxy', _('Update via proxy'),
|
||||
_('Update subscriptions via proxy.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('subscription', form.DynamicList, 'subscription_url', _('Subscription URL-s'),
|
||||
_('Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.'));
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id && value) {
|
||||
try {
|
||||
let url = new URL(value);
|
||||
if (!url.hostname)
|
||||
return _('Expecting: %s').format(_('valid URL'));
|
||||
}
|
||||
catch(e) {
|
||||
return _('Expecting: %s').format(_('valid URL'));
|
||||
}
|
||||
}
|
||||
o = s.taboption('subscription', form.ListValue, 'latency_test_mode', _('Latency test mode'),
|
||||
_('Choose which method node latency tests use.'));
|
||||
o.value('icmp', _('ICMP'));
|
||||
o.value('rp', _('RP'));
|
||||
o.default = 'icmp';
|
||||
o.rmempty = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
o = s.taboption('subscription', SubscriptionList, 'subscription_url', _('Subscriptions'),
|
||||
_('Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.'));
|
||||
subscription_list_option = o;
|
||||
|
||||
o = s.taboption('subscription', form.ListValue, 'filter_nodes', _('Filter nodes'),
|
||||
_('Drop/keep specific nodes from subscriptions.'));
|
||||
@@ -1396,8 +1981,16 @@ return view.extend({
|
||||
o.inputstyle = 'apply';
|
||||
o.inputtitle = _('Save current settings');
|
||||
o.onclick = function() {
|
||||
const validation = subscription_list_option.validate('subscription', subscription_list_option.formvalue('subscription'));
|
||||
if (validation !== true) {
|
||||
ui.addNotification(null, E('p', validation));
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.map.save(null, true).then(() => {
|
||||
ui.changes.apply(true);
|
||||
return ui.changes.apply(true);
|
||||
}).then(() => {
|
||||
return callRCInit('homeproxy', 'reload');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1460,6 +2053,68 @@ return view.extend({
|
||||
}
|
||||
/* Subscriptions settings end */
|
||||
|
||||
return m.render();
|
||||
return m.render().then((el) => {
|
||||
let bulk_testing = false;
|
||||
let tabmenu_observer = null;
|
||||
let bulk_button = E('button', {
|
||||
'class': 'cbi-button cbi-button-action hp-bulk-latency-test',
|
||||
'click': ui.createHandlerFn(this, async () => {
|
||||
if (bulk_testing || typeof globalThis === 'undefined' || !globalThis.__hpNodeLatencyTrigger)
|
||||
return false;
|
||||
|
||||
let section_ids = getActiveSubscriptionLatencySectionIds(el);
|
||||
if (!section_ids.length)
|
||||
return false;
|
||||
|
||||
bulk_testing = true;
|
||||
bulk_button.textContent = _('Testing Current List...');
|
||||
bulk_button.disabled = true;
|
||||
|
||||
for (let sid of section_ids)
|
||||
await globalThis.__hpNodeLatencyTrigger(sid);
|
||||
|
||||
bulk_testing = false;
|
||||
bulk_button.textContent = _('Test Current List');
|
||||
bulk_button.disabled = false;
|
||||
return true;
|
||||
})
|
||||
}, [ _('Test Current List') ]);
|
||||
|
||||
let updateBulkButtonVisibility = () => {
|
||||
bulk_button.style.display = shouldShowBulkLatencyButton(el) ? '' : 'none';
|
||||
};
|
||||
|
||||
let attachBulkButton = () => {
|
||||
let actions = (el.parentElement ? el.parentElement.querySelector('.cbi-page-actions') : null) || document.querySelector('.cbi-page-actions');
|
||||
if (!actions)
|
||||
return false;
|
||||
|
||||
if (!actions.querySelector('.hp-bulk-latency-test'))
|
||||
actions.insertBefore(bulk_button, actions.firstChild);
|
||||
|
||||
updateBulkButtonVisibility();
|
||||
|
||||
if (!tabmenu_observer) {
|
||||
let tabmenu = el.querySelector('.cbi-tabmenu');
|
||||
if (tabmenu) {
|
||||
tabmenu_observer = new MutationObserver(() => updateBulkButtonVisibility());
|
||||
tabmenu_observer.observe(tabmenu, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: [ 'class', 'aria-selected' ]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!attachBulkButton()) {
|
||||
window.setTimeout(attachBulkButton, 0);
|
||||
window.setTimeout(attachBulkButton, 300);
|
||||
}
|
||||
|
||||
return el;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -126,6 +126,8 @@ return view.extend({
|
||||
let m, s, o;
|
||||
let features = data[1];
|
||||
|
||||
hp.installCloseButtonText();
|
||||
|
||||
m = new form.Map('homeproxy', _('HomeProxy Server'),
|
||||
_('The modern ImmortalWrt proxy platform for ARM64/AMD64.'));
|
||||
|
||||
@@ -188,6 +190,7 @@ return view.extend({
|
||||
o.value('tuic', _('Tuic'));
|
||||
o.value('vless', _('VLESS'));
|
||||
o.value('vmess', _('VMess'));
|
||||
o.default = 'mixed';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'address', _('Listen address'));
|
||||
@@ -637,10 +640,10 @@ return view.extend({
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
|
||||
o.value('alidns', _('Alibaba Cloud DNS'));
|
||||
o.value('cloudflare', _('Cloudflare'));
|
||||
o.value('alidns', _('AliYun'));
|
||||
o.value('cfdns', _('CloudFlare'));
|
||||
o.depends('tls_dns01_challenge', '1');
|
||||
o.default = 'cloudflare';
|
||||
o.default = 'cfdns';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
@@ -663,7 +666,7 @@ return view.extend({
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'cloudflare');
|
||||
o.depends('tls_dns01_provider', 'cfdns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
'require homeproxy as hp';
|
||||
|
||||
/* Thanks to luci-app-aria2 */
|
||||
const css = ' \
|
||||
#log_textarea { \
|
||||
@@ -27,6 +29,53 @@ const css = ' \
|
||||
} \
|
||||
.description { \
|
||||
background-color: #33ccff; \
|
||||
} \
|
||||
.homeproxy-connect-grid { \
|
||||
display: grid; \
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));\
|
||||
gap: 12px; \
|
||||
} \
|
||||
.homeproxy-connect-card { \
|
||||
display: grid; \
|
||||
grid-template-columns: 44px 1fr; \
|
||||
gap: 10px; \
|
||||
align-items: center; \
|
||||
min-height: 64px; \
|
||||
padding: 8px 10px; \
|
||||
border-radius: 6px; \
|
||||
background: #fff; \
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .08);\
|
||||
} \
|
||||
.homeproxy-connect-icon { \
|
||||
display: flex; \
|
||||
align-items: center; \
|
||||
justify-content: center; \
|
||||
width: 38px; \
|
||||
height: 38px; \
|
||||
border-radius: 50%; \
|
||||
color: #fff; \
|
||||
font-weight: 700; \
|
||||
font-size: 15px; \
|
||||
} \
|
||||
.homeproxy-connect-info { \
|
||||
min-height: 42px; \
|
||||
padding: 7px 12px; \
|
||||
border-radius: 8px; \
|
||||
background: #e9edf2; \
|
||||
} \
|
||||
.homeproxy-connect-title { \
|
||||
color: #7d8aa0; \
|
||||
font-weight: 700; \
|
||||
font-size: 13px; \
|
||||
line-height: 1.2; \
|
||||
} \
|
||||
.homeproxy-connect-result { \
|
||||
margin-top: 2px; \
|
||||
color: #ff4d35; \
|
||||
font-weight: 700; \
|
||||
font-size: 13px; \
|
||||
cursor: pointer; \
|
||||
line-height: 1.2; \
|
||||
}';
|
||||
|
||||
const hp_dir = '/var/run/homeproxy';
|
||||
@@ -44,13 +93,13 @@ function getConnStat(o, site) {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return L.resolveDefault(callConnStat(site), {}).then((ret) => {
|
||||
let ele = o.default.firstElementChild.nextElementSibling;
|
||||
let ele = o.default.firstElementChild.nextElementSibling;
|
||||
if (ret.result) {
|
||||
ele.style.setProperty('color', 'green');
|
||||
ele.innerHTML = _('passed');
|
||||
ele.innerHTML = _('passed');
|
||||
} else {
|
||||
ele.style.setProperty('color', 'red');
|
||||
ele.innerHTML = _('failed');
|
||||
ele.innerHTML = _('failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -60,6 +109,53 @@ function getConnStat(o, site) {
|
||||
]);
|
||||
}
|
||||
|
||||
function renderConnCards() {
|
||||
const callConnStat = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'connection_check',
|
||||
params: ['site'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const items = [
|
||||
{ site: 'baidu', title: _('Baidu Connection'), icon: 'B', color: '#ff7a00' },
|
||||
{ site: 'google', title: _('Google Connection'), icon: 'G', color: '#4285f4' },
|
||||
{ site: 'github', title: _('GitHub Connection'), icon: 'GH', color: '#7b3bb3' },
|
||||
{ site: 'youtube', title: _('YouTube Connection'), icon: 'YT', color: '#ff0033' }
|
||||
];
|
||||
|
||||
return E('div', { 'class': 'homeproxy-connect-grid' }, items.map((item) => {
|
||||
let result = E('div', {
|
||||
'class': 'homeproxy-connect-result',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
result.textContent = _('Checking...');
|
||||
result.style.setProperty('color', '#f0ad4e');
|
||||
|
||||
return L.resolveDefault(callConnStat(item.site), {}).then((ret) => {
|
||||
if (ret.result && ret.latency != null) {
|
||||
result.style.setProperty('color', 'green');
|
||||
result.textContent = _('%s ms').format(ret.latency);
|
||||
} else {
|
||||
result.style.setProperty('color', '#ff4d35');
|
||||
result.textContent = _('failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
}, [ _('Click to check') ]);
|
||||
|
||||
return E('div', { 'class': 'homeproxy-connect-card' }, [
|
||||
E('div', {
|
||||
'class': 'homeproxy-connect-icon',
|
||||
'style': 'background:%s'.format(item.color)
|
||||
}, [ item.icon ]),
|
||||
E('div', { 'class': 'homeproxy-connect-info' }, [
|
||||
E('div', { 'class': 'homeproxy-connect-title' }, [ item.title ]),
|
||||
result
|
||||
])
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
function getResVersion(o, type) {
|
||||
const callResVersion = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
@@ -227,16 +323,16 @@ return view.extend({
|
||||
render() {
|
||||
let m, s, o;
|
||||
|
||||
hp.installCloseButtonText();
|
||||
|
||||
m = new form.Map('homeproxy');
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Connection check'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_check_baidu', _('BaiDu'));
|
||||
o.cfgvalue = L.bind(getConnStat, this, o, 'baidu');
|
||||
|
||||
o = s.option(form.DummyValue, '_check_google', _('Google'));
|
||||
o.cfgvalue = L.bind(getConnStat, this, o, 'google');
|
||||
o = s.option(form.DummyValue, '_connection_check');
|
||||
o.rawhtml = true;
|
||||
o.render = renderConnCards;
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
|
||||
s.anonymous = true;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -25,8 +25,8 @@ config homeproxy 'migration'
|
||||
config homeproxy 'config'
|
||||
option main_node 'nil'
|
||||
option main_udp_node 'same'
|
||||
option dns_server '8.8.8.8'
|
||||
option china_dns_server '223.5.5.5'
|
||||
option dns_server 'https://dns.quad9.net/dns-query'
|
||||
option china_dns_server 'https://dns.alidns.com/dns-query'
|
||||
option routing_mode 'bypass_mainland_china'
|
||||
option routing_port 'common'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
@@ -38,20 +38,36 @@ config homeproxy 'control'
|
||||
option lan_proxy_mode 'disabled'
|
||||
list wan_proxy_ipv4_ips '91.105.192.0/23'
|
||||
list wan_proxy_ipv4_ips '91.108.4.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.8.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.16.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.12.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.20.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.8.0/21'
|
||||
list wan_proxy_ipv4_ips '91.108.16.0/21'
|
||||
list wan_proxy_ipv4_ips '91.108.56.0/22'
|
||||
list wan_proxy_ipv4_ips '95.161.64.0/20'
|
||||
list wan_proxy_ipv4_ips '149.154.160.0/20'
|
||||
list wan_proxy_ipv4_ips '185.76.151.0/24'
|
||||
list wan_proxy_ipv4_ips '203.208.50.66/32'
|
||||
list wan_proxy_ipv6_ips '2001:67c:4e8::/48'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23c::/48'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23d::/48'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23c::/47'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23f::/48'
|
||||
list wan_proxy_ipv6_ips '2a0a:f280::/32'
|
||||
|
||||
config homeproxy 'clash_api'
|
||||
option external_ui '/etc/homeproxy/run/ui'
|
||||
option external_ui_download_url 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
|
||||
option external_ui_download_detour 'direct-out'
|
||||
option external_controller '0.0.0.0:9095'
|
||||
option secret 'homeproxy'
|
||||
option default_mode 'rule'
|
||||
|
||||
config homeproxy 'ntp'
|
||||
option enabled '1'
|
||||
option server 'ntp.aliyun.com'
|
||||
option server_port '123'
|
||||
option interval '30m'
|
||||
|
||||
config homeproxy 'cache'
|
||||
option enabled '1'
|
||||
option path '/etc/homeproxy/cache.db'
|
||||
option store_rdrc '1'
|
||||
|
||||
config homeproxy 'routing'
|
||||
option sniff_override '1'
|
||||
option default_outbound 'direct-out'
|
||||
@@ -66,13 +82,12 @@ config homeproxy 'dns'
|
||||
config homeproxy 'subscription'
|
||||
option auto_update '0'
|
||||
option allow_insecure '0'
|
||||
option latency_test_mode 'icmp'
|
||||
option packet_encoding 'xudp'
|
||||
option update_via_proxy '0'
|
||||
option filter_nodes 'blacklist'
|
||||
list filter_keywords '重置|到期|过期|剩余|套餐'
|
||||
list filter_keywords 'Expiration|Remaining'
|
||||
option auto_update_time '0 0 * * *'
|
||||
option filter_nodes 'disabled'
|
||||
|
||||
config homeproxy 'server'
|
||||
option enabled '0'
|
||||
option log_level 'warn'
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
20260704060757
|
||||
202606132337
|
||||
@@ -1,6 +1,8 @@
|
||||
2001:250::/30
|
||||
2001:254::/31
|
||||
2001:254::/33
|
||||
2001:255::/32
|
||||
2001:256:100::/48
|
||||
2001:678:53c::/48
|
||||
2001:c68::/32
|
||||
2001:cc0::/32
|
||||
2001:da8::/32
|
||||
@@ -11,8 +13,11 @@
|
||||
2001:dc7::/32
|
||||
2001:dd8:1::/48
|
||||
2001:dd9::/48
|
||||
2001:df5:4740::/48
|
||||
2001:df6:40::/48
|
||||
2001:4510:400::/40
|
||||
2001:4510:1480::/41
|
||||
2001:4511:1480::/41
|
||||
2400:1160::/32
|
||||
2400:3200::/32
|
||||
2400:5280:f803::/48
|
||||
@@ -20,6 +25,8 @@
|
||||
2400:6000::/32
|
||||
2400:6600::/32
|
||||
2400:6e60:1301::/48
|
||||
2400:7100::/32
|
||||
2400:73e0::/32
|
||||
2400:75aa::/32
|
||||
2400:7fc0::/40
|
||||
2400:7fc0:220::/44
|
||||
@@ -36,9 +43,14 @@
|
||||
2400:8200::/32
|
||||
2400:89c0:1010::/44
|
||||
2400:89c0:1050::/46
|
||||
2400:89c0:1130::/44
|
||||
2400:89c0:1150::/48
|
||||
2400:89c0:2100::/48
|
||||
2400:89c0:2200::/48
|
||||
2400:89c0:3010::/44
|
||||
2400:89c0:6000::/48
|
||||
2400:8fc0::/32
|
||||
2400:89c0:6100::/48
|
||||
2400:8fc0:571::/48
|
||||
2400:9020:f010::/46
|
||||
2400:9380:8001::/48
|
||||
2400:9380:8003::/48
|
||||
@@ -85,15 +97,19 @@
|
||||
2400:cb80:e40::/44
|
||||
2400:da00::/32
|
||||
2400:dd00::/28
|
||||
2400:ee00:ffec::/46
|
||||
2400:ee00:fff0::/44
|
||||
2400:e680::/32
|
||||
2400:ee00::/32
|
||||
2400:f6e0::/32
|
||||
2400:f720::/32
|
||||
2400:fe00::/32
|
||||
2401:20::/40
|
||||
2401:20:1330::/46
|
||||
2401:800::/32
|
||||
2401:1160::/32
|
||||
2401:1200::/48
|
||||
2401:1320::/32
|
||||
2401:1d40::/32
|
||||
2401:2780::/32
|
||||
2401:2a00:f000::/43
|
||||
2401:2e00::/32
|
||||
2401:3480::/36
|
||||
@@ -101,11 +117,39 @@
|
||||
2401:3480:3000::/36
|
||||
2401:34a0::/31
|
||||
2401:3800::/32
|
||||
2401:3880::/32
|
||||
2401:3980::/32
|
||||
2401:3a80::/32
|
||||
2401:3b80::/32
|
||||
2401:3c80::/32
|
||||
2401:3d80::/32
|
||||
2401:3e80::/32
|
||||
2401:3f80::/32
|
||||
2401:4180::/32
|
||||
2401:4280::/32
|
||||
2401:4380::/32
|
||||
2401:4480::/32
|
||||
2401:4580::/32
|
||||
2401:4680::/32
|
||||
2401:4780::/32
|
||||
2401:4880::/32
|
||||
2401:4a80::/32
|
||||
2401:5c20:10::/48
|
||||
2401:71c0::/48
|
||||
2401:7660::/48
|
||||
2401:7700::/32
|
||||
2401:7d40::/32
|
||||
2401:7e00::/32
|
||||
2401:8be0::/48
|
||||
2401:8d00::/47
|
||||
2401:8d00:3::/48
|
||||
2401:8d00:4::/47
|
||||
2401:8d00:7::/48
|
||||
2401:8d00:8::/47
|
||||
2401:8d00:b::/48
|
||||
2401:8d00:c::/48
|
||||
2401:8d00:f::/48
|
||||
2401:8d00:12::/48
|
||||
2401:9a00::/44
|
||||
2401:9a00:10::/46
|
||||
2401:a140:1::/48
|
||||
@@ -121,26 +165,56 @@
|
||||
2401:c020:6::/48
|
||||
2401:c020:8::/47
|
||||
2401:c020:14::/48
|
||||
2401:ca00::/32
|
||||
2401:cb80::/32
|
||||
2401:cc00::/32
|
||||
2401:ce00::/32
|
||||
2401:d0e0:3::/48
|
||||
2401:d180::/46
|
||||
2401:d180:10::/47
|
||||
2401:d180:111::/48
|
||||
2401:d180:2120::/48
|
||||
2401:d780::/32
|
||||
2401:d920::/48
|
||||
2401:de00::/32
|
||||
2401:ec00::/32
|
||||
2401:fa00:40::/48
|
||||
2402:840:d000::/46
|
||||
2402:840:e000::/46
|
||||
2402:840:f000::/38
|
||||
2402:c60::/48
|
||||
2402:1440::/32
|
||||
2402:18a0::/32
|
||||
2402:2000::/32
|
||||
2402:2e60::/39
|
||||
2402:34e0::/32
|
||||
2402:36e0::/44
|
||||
2402:3c00::/32
|
||||
2402:3f80:1400::/40
|
||||
2402:4440:9000::/39
|
||||
2402:4440:9200::/48
|
||||
2402:4440:9400::/48
|
||||
2402:4440:9600::/48
|
||||
2402:4440:9800::/48
|
||||
2402:4440:9810::/48
|
||||
2402:4440:9900::/48
|
||||
2402:4440:9a00::/48
|
||||
2402:4440:9b00::/48
|
||||
2402:4440:9c00::/48
|
||||
2402:4440:9d00::/48
|
||||
2402:4440:9e00::/48
|
||||
2402:4440:a000::/39
|
||||
2402:4440:a800::/46
|
||||
2402:4440:a810::/46
|
||||
2402:4b80::/32
|
||||
2402:4e00::/32
|
||||
2402:5e40::/32
|
||||
2402:5ec0::/32
|
||||
2402:6e80::/32
|
||||
2402:6f40::/48
|
||||
2402:6f40:2::/47
|
||||
2402:6fc0::/48
|
||||
2402:7040::/32
|
||||
2402:7d80::/48
|
||||
2402:7d80:240::/48
|
||||
2402:7d80:8888::/48
|
||||
@@ -162,7 +236,12 @@
|
||||
2402:dfc0:50::/44
|
||||
2402:e480::/32
|
||||
2402:e740::/32
|
||||
2402:e7c0::/32
|
||||
2402:e7c0::/37
|
||||
2402:e7c0:800::/38
|
||||
2402:e7c0:1000::/36
|
||||
2402:e7c0:2000::/35
|
||||
2402:e7c0:4000::/34
|
||||
2402:e7c0:8000::/33
|
||||
2402:e880::/48
|
||||
2402:ef40::/32
|
||||
2402:f000::/32
|
||||
@@ -190,6 +269,7 @@
|
||||
2403:7580::/32
|
||||
2403:8080:101::/48
|
||||
2403:8c00::/32
|
||||
2403:9b00:2000::/48
|
||||
2403:9b00:2400::/48
|
||||
2403:9b00:6000::/48
|
||||
2403:a100::/48
|
||||
@@ -251,6 +331,10 @@
|
||||
2404:c940::/48
|
||||
2404:e280::/47
|
||||
2404:e8c0::/32
|
||||
2404:f4c0:f000::/48
|
||||
2404:f4c0:f003::/48
|
||||
2404:f4c0:f004::/46
|
||||
2404:f4c0:f008::/45
|
||||
2405:80:10::/48
|
||||
2405:80:13::/48
|
||||
2405:6c0:2::/48
|
||||
@@ -270,24 +354,130 @@
|
||||
2405:6f00:c602::/48
|
||||
2405:7040:6000::/47
|
||||
2405:78c0:6e00::/43
|
||||
2405:8280::/32
|
||||
2405:8a40::/32
|
||||
2405:a900:ffee::/48
|
||||
2405:a900:fffe::/48
|
||||
2405:ad00::/32
|
||||
2405:b7c0::/32
|
||||
2405:d900::/32
|
||||
2405:e000::/32
|
||||
2405:e600:2::/48
|
||||
2405:f940::/32
|
||||
2406:280::/32
|
||||
2406:840:9680::/44
|
||||
2406:840:8100::/40
|
||||
2406:840:9178::/48
|
||||
2406:840:9962::/47
|
||||
2406:840:9966::/48
|
||||
2406:840:996c::/48
|
||||
2406:840:e080::/44
|
||||
2406:840:e0cf::/48
|
||||
2406:840:e0e0::/46
|
||||
2406:840:e0e4::/47
|
||||
2406:840:e10f::/48
|
||||
2406:840:e14f::/48
|
||||
2406:840:e330::/44
|
||||
2406:840:e600::/44
|
||||
2406:840:e620::/47
|
||||
2406:840:e80f::/48
|
||||
2406:840:eb01::/48
|
||||
2406:840:eb02::/47
|
||||
2406:840:eb04::/47
|
||||
2406:840:eb07::/48
|
||||
2406:840:eb08::/48
|
||||
2406:840:eb0b::/48
|
||||
2406:840:ee40::/47
|
||||
2406:840:ee44::/48
|
||||
2406:840:ee4b::/48
|
||||
2406:840:ee4d::/48
|
||||
2406:840:eee5::/48
|
||||
2406:840:f380::/44
|
||||
2406:840:f44f::/48
|
||||
2406:840:f889::/48
|
||||
2406:840:fa01::/48
|
||||
2406:840:fa60::/44
|
||||
2406:840:fc10::/47
|
||||
2406:840:fc12::/48
|
||||
2406:840:fc20::/47
|
||||
2406:840:fc22::/48
|
||||
2406:840:fc30::/47
|
||||
2406:840:fc32::/48
|
||||
2406:840:fc40::/47
|
||||
2406:840:fc42::/48
|
||||
2406:840:fc50::/47
|
||||
2406:840:fc52::/48
|
||||
2406:840:fc60::/47
|
||||
2406:840:fc62::/48
|
||||
2406:840:fc70::/47
|
||||
2406:840:fc72::/48
|
||||
2406:840:fc90::/47
|
||||
2406:840:fc92::/48
|
||||
2406:840:fca0::/47
|
||||
2406:840:fca2::/48
|
||||
2406:840:fcb0::/47
|
||||
2406:840:fcb2::/48
|
||||
2406:840:fcd0::/48
|
||||
2406:840:fd40::/48
|
||||
2406:840:fd46::/47
|
||||
2406:840:fd48::/48
|
||||
2406:840:fd50::/48
|
||||
2406:840:fd56::/47
|
||||
2406:840:fd58::/48
|
||||
2406:840:fd60::/48
|
||||
2406:840:fd66::/47
|
||||
2406:840:fd68::/48
|
||||
2406:840:fd70::/48
|
||||
2406:840:fd76::/47
|
||||
2406:840:fd78::/48
|
||||
2406:840:fd80::/48
|
||||
2406:840:fd86::/47
|
||||
2406:840:fd88::/48
|
||||
2406:840:fd90::/47
|
||||
2406:840:fd92::/48
|
||||
2406:840:fd9f::/48
|
||||
2406:840:fda0::/48
|
||||
2406:840:fda7::/48
|
||||
2406:840:fdb0::/48
|
||||
2406:840:fdb7::/48
|
||||
2406:840:fdc0::/48
|
||||
2406:840:fdc7::/48
|
||||
2406:840:fe90::/46
|
||||
2406:840:fe94::/48
|
||||
2406:840:fe96::/47
|
||||
2406:840:fe98::/46
|
||||
2406:840:fe9d::/48
|
||||
2406:840:fe9e::/48
|
||||
2406:840:fea2::/47
|
||||
2406:840:fea4::/46
|
||||
2406:840:fea8::/48
|
||||
2406:840:feaa::/47
|
||||
2406:840:feac::/46
|
||||
2406:840:fec0::/48
|
||||
2406:840:fec2::/47
|
||||
2406:840:fec4::/48
|
||||
2406:840:feca::/47
|
||||
2406:840:fecf::/48
|
||||
2406:840:fed1::/48
|
||||
2406:840:fed8::/48
|
||||
2406:840:fedb::/48
|
||||
2406:840:fedc::/48
|
||||
2406:840:fef3::/48
|
||||
2406:840:fef4::/48
|
||||
2406:840:fef6::/47
|
||||
2406:840:fef8::/46
|
||||
2406:840:fefc::/48
|
||||
2406:840:feff::/48
|
||||
2406:1080::/48
|
||||
2406:1e40:f012::/47
|
||||
2406:2700::/32
|
||||
2406:3340::/32
|
||||
2406:3640:1::/48
|
||||
2406:4d00::/48
|
||||
2406:52c0::/32
|
||||
2406:5340:6666::/48
|
||||
2406:5340:8888::/48
|
||||
2406:6100::/32
|
||||
2406:8880::/48
|
||||
2406:b640:100::/48
|
||||
2406:b640:4100::/48
|
||||
2406:cac0::/40
|
||||
@@ -304,6 +494,7 @@
|
||||
2407:23c0::/48
|
||||
2407:2840::/48
|
||||
2407:37c0::/32
|
||||
2407:4980::/32
|
||||
2407:5380::/32
|
||||
2407:6c40:1100::/48
|
||||
2407:6c40:1210::/48
|
||||
@@ -322,7 +513,51 @@
|
||||
2407:c080:6000::/36
|
||||
2407:c080:8000::/36
|
||||
2407:d9c0::/32
|
||||
2408:4000::/22
|
||||
2408:4000::/40
|
||||
2408:4000:100::/47
|
||||
2408:4000:103::/48
|
||||
2408:4000:104::/46
|
||||
2408:4000:108::/45
|
||||
2408:4000:110::/44
|
||||
2408:4000:120::/43
|
||||
2408:4000:140::/42
|
||||
2408:4000:180::/41
|
||||
2408:4000:200::/39
|
||||
2408:4000:400::/38
|
||||
2408:4000:800::/37
|
||||
2408:4000:1000::/36
|
||||
2408:4000:2000::/35
|
||||
2408:4000:4000::/34
|
||||
2408:4000:8000::/33
|
||||
2408:4001::/32
|
||||
2408:4002::/31
|
||||
2408:4004::/30
|
||||
2408:4008::/29
|
||||
2408:4010::/30
|
||||
2408:4014::/31
|
||||
2408:4016:1::/48
|
||||
2408:4016:2::/47
|
||||
2408:4016:4::/46
|
||||
2408:4016:8::/45
|
||||
2408:4016:10::/44
|
||||
2408:4016:20::/43
|
||||
2408:4016:40::/42
|
||||
2408:4016:80::/41
|
||||
2408:4016:100::/40
|
||||
2408:4016:200::/39
|
||||
2408:4016:400::/38
|
||||
2408:4016:800::/37
|
||||
2408:4016:1000::/36
|
||||
2408:4016:2000::/35
|
||||
2408:4016:4000::/34
|
||||
2408:4016:8000::/33
|
||||
2408:4017::/32
|
||||
2408:4018::/29
|
||||
2408:4020::/27
|
||||
2408:4040::/26
|
||||
2408:4080::/25
|
||||
2408:4100::/24
|
||||
2408:4200::/23
|
||||
2408:8000::/48
|
||||
2408:8000:2::/47
|
||||
2408:8000:1000::/36
|
||||
@@ -579,103 +814,86 @@
|
||||
2408:840c:dd00::/40
|
||||
2408:840c:de00::/40
|
||||
2408:840d::/42
|
||||
2408:840d:40::/43
|
||||
2408:840d:200::/42
|
||||
2408:840d:400::/42
|
||||
2408:840d:440::/43
|
||||
2408:840d:600::/42
|
||||
2408:840d:d00::/42
|
||||
2408:840d:d40::/43
|
||||
2408:840d:f00::/42
|
||||
2408:840d:1100::/42
|
||||
2408:840d:1140::/43
|
||||
2408:840d:1300::/42
|
||||
2408:840d:1a00::/42
|
||||
2408:840d:1a40::/43
|
||||
2408:840d:1c00::/42
|
||||
2408:840d:1e00::/42
|
||||
2408:840d:1e40::/43
|
||||
2408:840d:2000::/42
|
||||
2408:840d:2700::/42
|
||||
2408:840d:2740::/43
|
||||
2408:840d:2900::/42
|
||||
2408:840d:2b00::/42
|
||||
2408:840d:2b40::/43
|
||||
2408:840d:2d00::/42
|
||||
2408:840d:3400::/42
|
||||
2408:840d:3440::/43
|
||||
2408:840d:3600::/42
|
||||
2408:840d:3800::/42
|
||||
2408:840d:3840::/43
|
||||
2408:840d:3a00::/42
|
||||
2408:840d:4e00::/42
|
||||
2408:840d:4e40::/43
|
||||
2408:840d:5000::/42
|
||||
2408:840d:5200::/42
|
||||
2408:840d:5240::/43
|
||||
2408:840d:5400::/42
|
||||
2408:840d:5b00::/42
|
||||
2408:840d:5b40::/43
|
||||
2408:840d:5d00::/42
|
||||
2408:840d:5f00::/42
|
||||
2408:840d:5f40::/43
|
||||
2408:840d:6100::/42
|
||||
2408:840d:6800::/42
|
||||
2408:840d:6840::/43
|
||||
2408:840d:6a00::/42
|
||||
2408:840d:6c00::/42
|
||||
2408:840d:6c40::/43
|
||||
2408:840d:6e00::/42
|
||||
2408:840d:7500::/42
|
||||
2408:840d:7540::/43
|
||||
2408:840d:7700::/42
|
||||
2408:840d:7900::/42
|
||||
2408:840d:7940::/43
|
||||
2408:840d:7b00::/42
|
||||
2408:840d:8200::/42
|
||||
2408:840d:8240::/43
|
||||
2408:840d:8400::/42
|
||||
2408:840d:8600::/42
|
||||
2408:840d:8640::/43
|
||||
2408:840d:8800::/42
|
||||
2408:840d:8f00::/42
|
||||
2408:840d:8f40::/43
|
||||
2408:840d:9100::/42
|
||||
2408:840d:9300::/42
|
||||
2408:840d:9340::/43
|
||||
2408:840d:9500::/42
|
||||
2408:840d:9c00::/42
|
||||
2408:840d:9c40::/43
|
||||
2408:840d:9e00::/42
|
||||
2408:840d:a000::/42
|
||||
2408:840d:a040::/43
|
||||
2408:840d:a200::/42
|
||||
2408:840d:a900::/42
|
||||
2408:840d:a940::/43
|
||||
2408:840d:ab00::/42
|
||||
2408:840d:ad00::/42
|
||||
2408:840d:ad40::/43
|
||||
2408:840d:af00::/42
|
||||
2408:840d:b600::/42
|
||||
2408:840d:b640::/43
|
||||
2408:840d:b800::/42
|
||||
2408:840d:ba00::/42
|
||||
2408:840d:ba40::/43
|
||||
2408:840d:bc00::/42
|
||||
2408:840d:c300::/42
|
||||
2408:840d:c340::/43
|
||||
2408:840d:c500::/42
|
||||
2408:840d:c700::/42
|
||||
2408:840d:c740::/43
|
||||
2408:840d:c900::/42
|
||||
2408:840d:d000::/42
|
||||
2408:840d:d040::/43
|
||||
2408:840d:d200::/42
|
||||
2408:840d:d400::/42
|
||||
2408:840d:d440::/43
|
||||
2408:840d:d600::/42
|
||||
2408:840d:dd00::/42
|
||||
2408:840d:dd40::/43
|
||||
2408:840d:de00::/42
|
||||
2408:840e:dd00::/40
|
||||
2408:840e:de00::/40
|
||||
@@ -856,7 +1074,6 @@
|
||||
2408:8a06:100::/47
|
||||
2408:8a07::/40
|
||||
2408:8a21:4000::/35
|
||||
2408:8a22::/35
|
||||
2408:8a23:4000::/34
|
||||
2408:8a24:4000::/34
|
||||
2408:8a26:c000::/34
|
||||
@@ -870,14 +1087,47 @@
|
||||
2409:27fc::/48
|
||||
2409:27fe::/33
|
||||
2409:6100::/44
|
||||
2409:8000::/20
|
||||
2409:8000::/21
|
||||
2409:8800::/22
|
||||
2409:8c00::/25
|
||||
2409:8c80::/30
|
||||
2409:8c84::/32
|
||||
2409:8c85::/34
|
||||
2409:8c85:4000::/36
|
||||
2409:8c85:5000::/38
|
||||
2409:8c85:5400::/43
|
||||
2409:8c85:5420::/44
|
||||
2409:8c85:5430::/47
|
||||
2409:8c85:5433::/48
|
||||
2409:8c85:5434::/46
|
||||
2409:8c85:5438::/45
|
||||
2409:8c85:5440::/42
|
||||
2409:8c85:5480::/41
|
||||
2409:8c85:5500::/40
|
||||
2409:8c85:5600::/39
|
||||
2409:8c85:5800::/37
|
||||
2409:8c85:6000::/35
|
||||
2409:8c85:8000::/33
|
||||
2409:8c86::/31
|
||||
2409:8c88::/29
|
||||
2409:8c90::/28
|
||||
2409:8ca0::/27
|
||||
2409:8cc0::/26
|
||||
2409:8d00::/24
|
||||
2409:8e00::/23
|
||||
240a:2000::/40
|
||||
240a:2001:100::/40
|
||||
240a:2001:1000::/36
|
||||
240a:4002:1b00::/40
|
||||
240a:4010:8000::/33
|
||||
240a:4020:83a::/48
|
||||
240a:4020:883a::/48
|
||||
240a:4021:83a::/48
|
||||
240a:4021:883a::/48
|
||||
240a:4080:8000::/35
|
||||
240a:4084:2000::/35
|
||||
240a:4088:a000::/35
|
||||
240a:408c:2000::/35
|
||||
240a:4090:50::/48
|
||||
240a:4090:120::/48
|
||||
240a:4090:250::/48
|
||||
@@ -892,11 +1142,20 @@
|
||||
240a:4090:5200::/40
|
||||
240a:4090:7000::/39
|
||||
240a:4090:7200::/40
|
||||
240a:4090:8000::/34
|
||||
240a:4093::/35
|
||||
240a:4094:2000::/35
|
||||
240a:409c:2000::/35
|
||||
240a:40a4:2000::/35
|
||||
240a:40a8:8000::/35
|
||||
240a:40ac:2000::/35
|
||||
240a:40b0:83a::/48
|
||||
240a:40b0:283a::/48
|
||||
240a:40b0:483a::/48
|
||||
240a:40b0:683a::/48
|
||||
240a:40c0:8000::/43
|
||||
240a:40c0:8200::/48
|
||||
240a:40c0:8240::/48
|
||||
240a:40c0:a000::/43
|
||||
240a:40c0:c000::/43
|
||||
240a:40c0:e000::/43
|
||||
@@ -923,10 +1182,31 @@
|
||||
240a:40c3:8000::/43
|
||||
240a:40c3:a000::/43
|
||||
240a:40c3:c000::/43
|
||||
240a:40c3:c200::/48
|
||||
240a:40c3:c240::/48
|
||||
240a:40c3:e000::/43
|
||||
240a:40c4::/43
|
||||
240a:40c4:2000::/43
|
||||
240a:40c4:4000::/43
|
||||
240a:4172::/31
|
||||
240a:4180::/31
|
||||
240a:41b0::/34
|
||||
240a:41b8:8800::/40
|
||||
240a:41b8:a000::/40
|
||||
240a:41b8:d400::/40
|
||||
240a:41b8:e400::/40
|
||||
240a:41d6::/31
|
||||
240a:41f2::/31
|
||||
240a:4208::/30
|
||||
240a:4224:9000::/44
|
||||
240a:4224:a000::/44
|
||||
240a:4224:c000::/44
|
||||
240a:4224:d000::/44
|
||||
240a:4224:e000::/44
|
||||
240a:4225:1000::/44
|
||||
240a:4225:3000::/44
|
||||
240a:4230::/31
|
||||
240a:4242::/31
|
||||
240a:4280::/26
|
||||
240a:42c0::/27
|
||||
240a:42e0::/28
|
||||
@@ -943,7 +1223,69 @@
|
||||
240c:4000::/22
|
||||
240c:c000::/20
|
||||
240d:4000::/21
|
||||
240e::/20
|
||||
240e::/21
|
||||
240e:800::/24
|
||||
240e:900::/26
|
||||
240e:940::/27
|
||||
240e:960::/28
|
||||
240e:970::/29
|
||||
240e:978::/30
|
||||
240e:97c::/32
|
||||
240e:97d::/33
|
||||
240e:97d:8001::/48
|
||||
240e:97d:8002::/47
|
||||
240e:97d:8004::/46
|
||||
240e:97d:8008::/45
|
||||
240e:97d:8010::/44
|
||||
240e:97d:8020::/43
|
||||
240e:97d:8040::/42
|
||||
240e:97d:8080::/41
|
||||
240e:97d:8100::/40
|
||||
240e:97d:8200::/39
|
||||
240e:97d:8400::/38
|
||||
240e:97d:8800::/37
|
||||
240e:97d:9000::/36
|
||||
240e:97d:a000::/35
|
||||
240e:97d:c000::/34
|
||||
240e:97e::/31
|
||||
240e:980::/31
|
||||
240e:982::/32
|
||||
240e:983::/35
|
||||
240e:983:2000::/44
|
||||
240e:983:2011::/48
|
||||
240e:983:2012::/48
|
||||
240e:983:2014::/46
|
||||
240e:983:2018::/45
|
||||
240e:983:2021::/48
|
||||
240e:983:2023::/48
|
||||
240e:983:2024::/46
|
||||
240e:983:2028::/45
|
||||
240e:983:2031::/48
|
||||
240e:983:2032::/47
|
||||
240e:983:2034::/46
|
||||
240e:983:2038::/45
|
||||
240e:983:2040::/42
|
||||
240e:983:2080::/41
|
||||
240e:983:2100::/40
|
||||
240e:983:2200::/39
|
||||
240e:983:2400::/38
|
||||
240e:983:2800::/37
|
||||
240e:983:3000::/36
|
||||
240e:983:4000::/34
|
||||
240e:983:8000::/33
|
||||
240e:984::/30
|
||||
240e:988::/29
|
||||
240e:990::/28
|
||||
240e:9a0::/27
|
||||
240e:9c0::/26
|
||||
240e:a00::/23
|
||||
240e:c00::/22
|
||||
2602:f92a:a468::/48
|
||||
2602:f92a:a46d::/48
|
||||
2602:f92a:a47d::/48
|
||||
2602:f9ba:a8::/48
|
||||
2602:f9ba:10c::/48
|
||||
2602:f9f6:450::/48
|
||||
2605:9d80:8001::/48
|
||||
2605:9d80:8011::/48
|
||||
2605:9d80:8021::/48
|
||||
@@ -959,6 +1301,8 @@
|
||||
2605:9d80:9092::/48
|
||||
2804:1e48:9001::/48
|
||||
2804:1e48:9002::/48
|
||||
2a01:e281:a400::/48
|
||||
2a01:e281:a410::/44
|
||||
2a04:3e00:1002::/48
|
||||
2a04:f580:8010::/47
|
||||
2a04:f580:8090::/48
|
||||
@@ -983,26 +1327,163 @@
|
||||
2a04:f580:9270::/48
|
||||
2a04:f580:9280::/48
|
||||
2a04:f580:9290::/48
|
||||
2a0a:d685:1ff::/48
|
||||
2a0a:d685:300::/40
|
||||
2a0b:4340:90::/48
|
||||
2a0b:4340:97::/48
|
||||
2a0d:c7c7:400::/38
|
||||
2a0e:aa06:49f::/48
|
||||
2a05:1087::/32
|
||||
2a06:3600:e000::/40
|
||||
2a06:3600:fb00::/40
|
||||
2a06:3600:fc00::/38
|
||||
2a06:3605::/32
|
||||
2a06:3606::/31
|
||||
2a06:a005:260::/43
|
||||
2a06:a005:280::/43
|
||||
2a06:a005:2a0::/44
|
||||
2a07:54c1:2200::/46
|
||||
2a07:54c1:2205::/48
|
||||
2a0a:d681:e000::/40
|
||||
2a0a:d681:fb00::/40
|
||||
2a0a:d681:fc00::/38
|
||||
2a0a:d687:f001::/48
|
||||
2a0c:b641:571::/48
|
||||
2a0e:aa06::/40
|
||||
2a0e:aa06:440::/48
|
||||
2a0e:aa06:450::/44
|
||||
2a0e:aa06:490::/44
|
||||
2a0e:aa06:4e0::/44
|
||||
2a0e:aa07:e144::/48
|
||||
2a0e:aa07:e146::/48
|
||||
2a0e:aa07:e148::/48
|
||||
2a0e:aa07:e280::/47
|
||||
2a0e:aa07:e284::/46
|
||||
2a0e:aa07:e288::/47
|
||||
2a0e:aa07:e28a::/48
|
||||
2a0e:aa07:e28d::/48
|
||||
2a0e:aa07:e28e::/48
|
||||
2a0e:aa07:f0d0::/46
|
||||
2a0e:aa07:f0d4::/47
|
||||
2a0e:aa07:f0d8::/48
|
||||
2a0e:aa07:f0de::/47
|
||||
2a0e:b107:740::/44
|
||||
2a0e:b107:2440::/44
|
||||
2a0f:1cc5:1600::/44
|
||||
2a0f:1cc5:4700::/40
|
||||
2a12:cb41:1200::/44
|
||||
2a12:cb41:1300::/43
|
||||
2a12:cb41:13f0::/44
|
||||
2a0e:b107:da0::/44
|
||||
2a0e:b107:14a0::/44
|
||||
2a0f:1cc5:14::/46
|
||||
2a0f:1cc5:1f::/48
|
||||
2a0f:1cc5:40::/48
|
||||
2a0f:1cc5:110::/44
|
||||
2a0f:1cc5:130::/44
|
||||
2a0f:1cc5:140::/42
|
||||
2a0f:1cc5:600::/47
|
||||
2a0f:1cc5:603::/48
|
||||
2a0f:1cc5:642::/48
|
||||
2a0f:1cc5:661::/48
|
||||
2a0f:1cc5:662::/48
|
||||
2a0f:1cc5:6a0::/48
|
||||
2a0f:1cc5:f01::/48
|
||||
2a0f:1cc5:f02::/47
|
||||
2a0f:1cc5:f04::/46
|
||||
2a0f:1cc5:f09::/48
|
||||
2a0f:1cc5:fff::/48
|
||||
2a0f:1cc5:1000::/48
|
||||
2a0f:1cc5:1010::/44
|
||||
2a0f:1cc5:1032::/47
|
||||
2a0f:1cc5:1034::/47
|
||||
2a0f:1cc5:1c00::/47
|
||||
2a0f:1cc5:1c02::/48
|
||||
2a0f:1cc5:1c20::/48
|
||||
2a0f:1cc5:1c30::/48
|
||||
2a0f:1cc5:1cc0::/48
|
||||
2a0f:1cc5:1d10::/47
|
||||
2a0f:1cc5:2000::/40
|
||||
2a0f:1cc5:2510::/44
|
||||
2a0f:1cc5:2520::/44
|
||||
2a0f:1cc5:25ff::/48
|
||||
2a0f:1cc5:2600::/41
|
||||
2a0f:1cc5:2680::/42
|
||||
2a0f:1cc5:2d01::/48
|
||||
2a0f:1cc5:2d03::/48
|
||||
2a0f:1cc5:3200::/46
|
||||
2a0f:1cc5:3222::/48
|
||||
2a0f:1cc6:b110::/47
|
||||
2a0f:1cc6:b210::/47
|
||||
2a0f:1cc6:b212::/48
|
||||
2a0f:2706::/32
|
||||
2a0f:85c1:bfe::/48
|
||||
2a12:cb41::/44
|
||||
2a12:cb46::/36
|
||||
2a12:cb47:ffff::/48
|
||||
2a13:1800::/48
|
||||
2a13:1800:10::/48
|
||||
2a13:1800:80::/44
|
||||
2a13:1800:300::/44
|
||||
2a13:1801:180::/43
|
||||
2a13:a5c3:f000::/40
|
||||
2a14:7580:d00::/43
|
||||
2a14:7580:d20::/46
|
||||
2a14:7580:d41::/48
|
||||
2a14:7580:d42::/47
|
||||
2a14:7580:dff::/48
|
||||
2a13:1801:200::/44
|
||||
2a13:8c87::/32
|
||||
2a13:a5c3:ff41::/48
|
||||
2a13:a5c3:ff50::/44
|
||||
2a14:67c1:70::/48
|
||||
2a14:67c1:74::/48
|
||||
2a14:67c1:a110::/44
|
||||
2a14:67c1:a123::/48
|
||||
2a14:67c1:a125::/48
|
||||
2a14:67c1:a128::/48
|
||||
2a14:67c1:b500::/48
|
||||
2a14:67c1:b511::/48
|
||||
2a14:67c1:b514::/48
|
||||
2a14:67c1:b530::/44
|
||||
2a14:67c1:b563::/48
|
||||
2a14:67c1:b578::/48
|
||||
2a14:67c1:b586::/47
|
||||
2a14:67c1:b588::/47
|
||||
2a14:67c1:b590::/48
|
||||
2a14:67c1:b599::/48
|
||||
2a14:67c1:b5e0::/44
|
||||
2a14:67c1:c300::/40
|
||||
2a14:67c2:519::/48
|
||||
2a14:67c2:a10::/47
|
||||
2a14:67c2:a21::/48
|
||||
2a14:67c2:a22::/47
|
||||
2a14:67c2:a2f::/48
|
||||
2a14:67c2:a30::/48
|
||||
2a14:67c2:a41::/48
|
||||
2a14:67c2:a50::/48
|
||||
2a14:67c2:a70::/47
|
||||
2a14:67c2:a80::/47
|
||||
2a14:67c2:a90::/48
|
||||
2a14:67c3:c0::/48
|
||||
2a14:67c3:190::/47
|
||||
2a14:67c3:192::/48
|
||||
2a14:67c3:1100::/47
|
||||
2a14:67c3:9990::/44
|
||||
2a14:7580:740::/44
|
||||
2a14:7580:fffa::/48
|
||||
2a14:7581:ffb::/48
|
||||
2a14:7581:30b6::/48
|
||||
2a14:7583:f224::/47
|
||||
2a14:7583:f228::/48
|
||||
2a14:7583:f22c::/48
|
||||
2a14:7583:f244::/48
|
||||
2a14:7583:f264::/48
|
||||
2a14:7583:f268::/48
|
||||
2a14:7583:f300::/46
|
||||
2a14:7583:f304::/47
|
||||
2a14:7583:f306::/48
|
||||
2a14:7583:f411::/48
|
||||
2a14:7583:f4f0::/48
|
||||
2a14:7583:f4f4::/48
|
||||
2a14:7583:f4fe::/48
|
||||
2a14:7583:f701::/48
|
||||
2a14:7583:f703::/48
|
||||
2a14:7583:f704::/46
|
||||
2a14:7583:f708::/46
|
||||
2a14:7583:f70c::/48
|
||||
2a14:7583:f743::/48
|
||||
2a14:7583:f744::/48
|
||||
2a14:7583:f764::/48
|
||||
2a14:7586:6100::/47
|
||||
2a14:7586:6103::/48
|
||||
2a14:7586:6104::/47
|
||||
2a14:7586:6110::/48
|
||||
2a14:7586:6300::/44
|
||||
2a14:7dc0:506::/48
|
||||
2a14:ae00:50::/44
|
||||
2c0f:f7a8:8011::/48
|
||||
2c0f:f7a8:8050::/48
|
||||
2c0f:f7a8:805f::/48
|
||||
@@ -1011,4 +1492,5 @@
|
||||
2c0f:f7a8:8211::/48
|
||||
2c0f:f7a8:9020::/48
|
||||
2c0f:f7a8:9041::/48
|
||||
2c0f:f7a8:9211::/48
|
||||
2c0f:f7a8:9220::/48
|
||||
@@ -1 +1 @@
|
||||
20260704060757
|
||||
202606132337
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
202607052256
|
||||
202606132337
|
||||
@@ -13,7 +13,6 @@
|
||||
173ng.com
|
||||
18board.com
|
||||
18comic.org
|
||||
18mh.net
|
||||
1984bbs.com
|
||||
1991way.com
|
||||
1e100.net
|
||||
@@ -74,7 +73,6 @@
|
||||
9cache.com
|
||||
9gag.com
|
||||
9news.com.au
|
||||
9oju31.launches.appsflyersdk.com
|
||||
a-normal-day.com
|
||||
a248.e.akamai.net
|
||||
aamacau.com
|
||||
@@ -89,7 +87,6 @@ about.gitlab.com
|
||||
about.me
|
||||
abplive.com
|
||||
acast.com
|
||||
accountboy.com
|
||||
accountkit.com
|
||||
acevpn.com
|
||||
acg.mn
|
||||
@@ -131,7 +128,6 @@ aiosearch.com
|
||||
aiph.net
|
||||
airconsole.com
|
||||
airitilibrary.com
|
||||
airuniversity.af.edu
|
||||
airvpn.org
|
||||
ait.org.tw
|
||||
aiweiweiblog.com
|
||||
@@ -211,18 +207,13 @@ aoxvpn.com
|
||||
apartmentratings.com
|
||||
apartments.com
|
||||
apat1989.org
|
||||
api-gl.lbkrs.com
|
||||
api-secure.recaptcha.net
|
||||
api-verify.recaptcha.net
|
||||
api.ai
|
||||
api.futunn.com
|
||||
api.lbkrs.com
|
||||
api.moomoobull.com
|
||||
api.palworldgame.com
|
||||
api.pureapk.com
|
||||
api.recaptcha.net
|
||||
api.steampowered.com
|
||||
api5.futunn.com
|
||||
apiary.io
|
||||
apigee.com
|
||||
apk.support
|
||||
@@ -400,7 +391,6 @@ bigjapanesesex.com
|
||||
bignews.org
|
||||
bigone.com
|
||||
bild.de
|
||||
bilinovel.com
|
||||
biliworld.com
|
||||
binance.com
|
||||
binance.org
|
||||
@@ -665,7 +655,6 @@ cdjp.org
|
||||
cdn-images.mailchimp.com
|
||||
cdn-telegram.org
|
||||
cdn.arstechnica.net
|
||||
cdn.futustatic.com
|
||||
cdn.jwplayer.com
|
||||
cdn.printfriendly.com
|
||||
cdn.seatguru.com
|
||||
@@ -689,7 +678,6 @@ centurys.net
|
||||
certificate-transparency.org
|
||||
certificate.revocationcheck.com
|
||||
cfr.org
|
||||
cg.play-analytics.com
|
||||
cgdepot.org
|
||||
change.org
|
||||
changp.com
|
||||
@@ -871,7 +859,6 @@ crazypool.org
|
||||
crazyshit.com
|
||||
crbug.com
|
||||
crchina.org
|
||||
creader.com
|
||||
creaders.net
|
||||
creativelab5.com
|
||||
cristyli.com
|
||||
@@ -1037,7 +1024,6 @@ dougscripts.com
|
||||
doujincafe.com
|
||||
dowjones.io
|
||||
download.dappcdn.com
|
||||
download.futunn.com
|
||||
dpool.top
|
||||
dpp.org.tw
|
||||
dpr.info
|
||||
@@ -1163,7 +1149,6 @@ expatshield.com
|
||||
expecthim.com
|
||||
expekt.com
|
||||
exploader.net
|
||||
expressnews.com
|
||||
expressvpn.com
|
||||
exrates.me
|
||||
extmatrix.com
|
||||
@@ -1354,36 +1339,7 @@ funkyimg.com
|
||||
furbo.org
|
||||
furinkan.com
|
||||
furrybar.com
|
||||
futu.cn
|
||||
futu.com
|
||||
futu.link
|
||||
futu0.com
|
||||
futu1.com
|
||||
futu2.com
|
||||
futu3.com
|
||||
futu4.com
|
||||
futu5.com
|
||||
futu6.com
|
||||
futu7.com
|
||||
futu8.com
|
||||
futu9.com
|
||||
futuau.com
|
||||
futubull.cn
|
||||
futuchain.com
|
||||
futuesop.com
|
||||
futufin.com
|
||||
futuhk.com
|
||||
futuhk2.com
|
||||
futuhkapp.com
|
||||
futuhn.com
|
||||
futuholdings.com
|
||||
futuinc.com
|
||||
futuniuniu.com
|
||||
futunn.com
|
||||
futuremessage.org
|
||||
futustatic.com
|
||||
fututrade.com
|
||||
fututrustee.com
|
||||
fw.cm
|
||||
fxcm-chinese.com
|
||||
fxnetworks.com
|
||||
@@ -1406,7 +1362,6 @@ gaoming.net
|
||||
gaopi.net
|
||||
gartlive.com
|
||||
garudalinux.org
|
||||
gate.com
|
||||
gate.io
|
||||
gatecoin.com
|
||||
gather.com
|
||||
@@ -1427,7 +1382,6 @@ generated.photos
|
||||
genius.com
|
||||
geph.io
|
||||
get.app
|
||||
get.dappcdn.com
|
||||
get.dev
|
||||
get.how
|
||||
get.page
|
||||
@@ -1785,7 +1739,6 @@ gtv.org
|
||||
gtv1.org
|
||||
gu-chu-sum.org
|
||||
guaguass.com
|
||||
guangming.com.my
|
||||
guishan.org
|
||||
gumroad.com
|
||||
gunsamerica.com
|
||||
@@ -1876,7 +1829,6 @@ hkgolden.com
|
||||
hkgpao.com
|
||||
hklts.org.hk
|
||||
hkmap.live
|
||||
hkong.hk
|
||||
hkopentv.com
|
||||
hkpeanut.com
|
||||
hkreporter.com
|
||||
@@ -2091,7 +2043,6 @@ itasoftware.com
|
||||
itch.io
|
||||
itemfix.com
|
||||
itiger.com
|
||||
itigerup.com
|
||||
itshidden.com
|
||||
itweet.net
|
||||
iuhrdf.org
|
||||
@@ -2181,7 +2132,6 @@ kenengba.com
|
||||
kepard.com
|
||||
kex.com
|
||||
keycdn.com
|
||||
kfor.com
|
||||
khatrimaza.org
|
||||
kichiku-doujinko.com
|
||||
kik.com
|
||||
@@ -2231,15 +2181,12 @@ landofhope.tv
|
||||
lantern.io
|
||||
laogai.org
|
||||
laogairesearch.org
|
||||
laohu8.com
|
||||
laqingdan.net
|
||||
larsgeorge.com
|
||||
lastcombat.com
|
||||
lastfm.es
|
||||
lausan.hk
|
||||
lbank.info
|
||||
lbctrl.com
|
||||
lbkrs.com
|
||||
ldplayer.net
|
||||
ldplayer.tw
|
||||
le-vpn.com
|
||||
@@ -2308,15 +2255,6 @@ localdomain.ws
|
||||
lockestek.com
|
||||
login.target.com
|
||||
logos.com.hk
|
||||
longbridge.cloud
|
||||
longbridge.com
|
||||
longbridge.global
|
||||
longbridge.hk
|
||||
longbridge.sg
|
||||
longbridgeapp.com
|
||||
longbridgehk.com
|
||||
longportapp.cn
|
||||
longportapp.com
|
||||
longtermly.net
|
||||
longtoes.com
|
||||
lookpic.com
|
||||
@@ -2434,7 +2372,6 @@ mfxmedia.com
|
||||
mgoon.com
|
||||
mgstage.com
|
||||
mh4u.org
|
||||
mhwindow.org
|
||||
microvpn.com
|
||||
mihua.org
|
||||
mij.rip
|
||||
@@ -2504,9 +2441,6 @@ monitorchina.org
|
||||
monocloud.me
|
||||
monster.com
|
||||
moodyz.com
|
||||
moomoo.com
|
||||
moomooequity.com
|
||||
moomootrustee.com
|
||||
moon.fm
|
||||
moonbbs.com
|
||||
moonbingo.com
|
||||
@@ -2747,13 +2681,6 @@ ooni.io
|
||||
ooni.org
|
||||
open.firstory.me
|
||||
openai.com
|
||||
openapi-quote.longbridge.cn
|
||||
openapi-quote.longbridge.com
|
||||
openapi-trade.longbridge.cn
|
||||
openapi-trade.longbridge.com
|
||||
openapi.futunn.com
|
||||
openapi.longbridge.cn
|
||||
openapi.longbridge.com
|
||||
openart.ai
|
||||
opencritic.com
|
||||
opendemocracy.net
|
||||
@@ -2819,7 +2746,6 @@ pbs.org
|
||||
pbworks.com
|
||||
pbxes.com
|
||||
pbxes.org
|
||||
pcgamesn.com
|
||||
pcgamestorrents.com
|
||||
pcij.org
|
||||
pct.org.tw
|
||||
@@ -2838,7 +2764,6 @@ peoplenews.tw
|
||||
peopo.org
|
||||
perfect-privacy.com
|
||||
periscope.tv
|
||||
perma.cc
|
||||
perplexity.ai
|
||||
pewresearch.org
|
||||
phayul.com
|
||||
@@ -3010,15 +2935,12 @@ qmp4.com
|
||||
qoos.com
|
||||
qq.co.za
|
||||
qstatus.com
|
||||
qtcard.futunn.com
|
||||
qtcardfthk.futufin.com
|
||||
qtrac.eu
|
||||
questvisual.com
|
||||
quitccp.org
|
||||
quiz.directory
|
||||
quora.com
|
||||
quoracdn.net
|
||||
quote.longbridge.global
|
||||
quran.com
|
||||
qz.com
|
||||
r-pool.net
|
||||
@@ -3087,7 +3009,6 @@ rentry.co
|
||||
renyurenquan.org
|
||||
resilio.com
|
||||
resistchina.org
|
||||
restofworld.org
|
||||
retweetrank.com
|
||||
reuters.com
|
||||
reutersmedia.net
|
||||
@@ -3161,10 +3082,6 @@ saveuighur.org
|
||||
sbme.me
|
||||
sbti.unun.dev
|
||||
schema.org
|
||||
schwab.co.uk
|
||||
schwab.com
|
||||
schwab.com.cn
|
||||
schwab.com.hk
|
||||
scmp.com
|
||||
scramble.io
|
||||
scratch.mit.edu
|
||||
@@ -3203,7 +3120,6 @@ sexinsex.net
|
||||
sf.net
|
||||
sfshibao.com
|
||||
sftuk.org
|
||||
sgqt0j.launches.appsflyersdk.com
|
||||
shadeyouvpn.com
|
||||
shadowsocks.be
|
||||
shadowsocks.com.hk
|
||||
@@ -3228,7 +3144,6 @@ shodanhq.com
|
||||
shooshtime.com
|
||||
shopee.tw
|
||||
shopping.yahoo.co.jp
|
||||
shortconn.im.qcloud.com
|
||||
showwe.tw
|
||||
shutterstock.com
|
||||
shwchurch.org
|
||||
@@ -3253,7 +3168,6 @@ sinoants.com
|
||||
sinoca.com
|
||||
sinocast.com
|
||||
sinoinsider.com
|
||||
sinyalee.com
|
||||
sipml5.org
|
||||
sis001.com
|
||||
site.new
|
||||
@@ -3323,7 +3237,6 @@ southmongolia.org
|
||||
southnews.com.tw
|
||||
southpark.cc.com
|
||||
sowers.org.hk
|
||||
spaceforce.mil
|
||||
spaces.hightail.com
|
||||
spacex.com
|
||||
spankbang.com
|
||||
@@ -3414,8 +3327,6 @@ supchina.com
|
||||
superpages.com
|
||||
supervpn.net
|
||||
superzooi.com
|
||||
support.futunn.com
|
||||
supremecourt.gov
|
||||
suprememastertv.com
|
||||
surfeasy.com
|
||||
surfeasy.com.au
|
||||
@@ -3527,7 +3438,6 @@ thehansindia.com
|
||||
thehindu.com
|
||||
thehun.net
|
||||
theinitium.com
|
||||
theintercept.com
|
||||
thenewslens.com
|
||||
thepiratebay.org
|
||||
theporndude.com
|
||||
@@ -3606,12 +3516,6 @@ tibettelegraph.com
|
||||
tibettimes.net
|
||||
tibettruth.com
|
||||
tibetwrites.org
|
||||
tigerbbs.cn
|
||||
tigerbbs.com
|
||||
tigerbrokers.com
|
||||
tigerbrokers.net
|
||||
tigerbrokers.nz
|
||||
tigerfintech.com
|
||||
tigervpn.com
|
||||
tiktok.com
|
||||
tiktokcdn-eu.com
|
||||
@@ -3674,9 +3578,6 @@ totalvpn.com
|
||||
tou.tv
|
||||
tpi.org.tw
|
||||
tracfone.com
|
||||
trade.futunn.com
|
||||
trade.longportapp.com
|
||||
tradeup.com
|
||||
tradingview.com
|
||||
translate.goog
|
||||
transparency.org
|
||||
@@ -3685,7 +3586,6 @@ trendsmap.com
|
||||
tronscan.org
|
||||
trouw.nl
|
||||
trt.net.tr
|
||||
trtworld.com
|
||||
truebuddha-md.org
|
||||
trustwallet.com
|
||||
truthsocial.com
|
||||
@@ -3781,7 +3681,6 @@ twitturk.com
|
||||
twitturly.com
|
||||
twkan.com
|
||||
twreporter.org
|
||||
twstalker.com
|
||||
twt.tl
|
||||
twtkr.com
|
||||
twttr.com
|
||||
@@ -3996,7 +3895,6 @@ wanz-factory.com
|
||||
warroom.org
|
||||
waselpro.com
|
||||
washingtonpost.com
|
||||
washingtontimes.com
|
||||
watchinese.com
|
||||
watchmygf.net
|
||||
watchout.tw
|
||||
@@ -4005,7 +3903,6 @@ wav.tv
|
||||
waveprotocol.org
|
||||
waybig.com
|
||||
waymo.com
|
||||
wbrks.com
|
||||
wd.bible
|
||||
wealth.com.tw
|
||||
wearn.com
|
||||
@@ -4015,7 +3912,6 @@ webmproject.org
|
||||
webpkgcache.com
|
||||
webrtc.org
|
||||
websdr.org
|
||||
webshare.io
|
||||
website.new
|
||||
webwarper.net
|
||||
wechatlawsuit.com
|
||||
@@ -4185,7 +4081,6 @@ xgmyd.com
|
||||
xhamster.com
|
||||
xianjian.tw
|
||||
xiaohexie.com
|
||||
xiaohu8.com
|
||||
xiaolan.me
|
||||
xiaoma.org
|
||||
xiaomi.eu
|
||||
@@ -4210,7 +4105,6 @@ xn--noss43i.com
|
||||
xn--oiq.cc
|
||||
xn--p8j9a0d9c9a.xn--q9jyb4c
|
||||
xn--u2u927b.com
|
||||
xn--vuqv2cf7wzyig79c.com
|
||||
xnpool.com
|
||||
xnxx.com
|
||||
xpud.org
|
||||
@@ -4219,7 +4113,6 @@ xskywalker.com
|
||||
xt.com
|
||||
xt.pub
|
||||
xtube.com
|
||||
xuan.com.my
|
||||
xuchao.net
|
||||
xuchao.org
|
||||
xuehua.us
|
||||
@@ -4326,8 +4219,6 @@ zhao.1984.city
|
||||
zhengjian.org
|
||||
zhengwunet.org
|
||||
zhenxiang.biz
|
||||
zhijianfengyi.cn
|
||||
zhijianfengyi.com
|
||||
zhizhu.top
|
||||
zhongguo.ca
|
||||
zhongguotese.net
|
||||
|
||||
@@ -1 +1 @@
|
||||
202607052256
|
||||
202606132337
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { md5 } from 'digest';
|
||||
import { readfile, writefile } from 'fs';
|
||||
import { isnan } from 'math';
|
||||
import { connect } from 'ubus';
|
||||
@@ -29,7 +30,10 @@ uci.load(uciconfig);
|
||||
|
||||
const uciinfra = 'infra',
|
||||
ucimain = 'config',
|
||||
ucicontrol = 'control';
|
||||
ucicontrol = 'control',
|
||||
uciclashapi = 'clash_api',
|
||||
ucintp = 'ntp',
|
||||
ucicache = 'cache';
|
||||
|
||||
const ucidnssetting = 'dns',
|
||||
ucidnsserver = 'dns_server',
|
||||
@@ -41,23 +45,63 @@ const uciroutingsetting = 'routing',
|
||||
|
||||
const ucinode = 'node';
|
||||
const uciruleset = 'ruleset';
|
||||
const RULESET_DIR = HP_DIR + '/ruleset';
|
||||
|
||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
|
||||
|
||||
function normalize_list(value) {
|
||||
if (isEmpty(value))
|
||||
return [];
|
||||
if (type(value) === 'array')
|
||||
return value;
|
||||
return [value];
|
||||
}
|
||||
|
||||
function ruleset_file_name(tag, format) {
|
||||
let filename = replace(tag || 'ruleset', /[^A-Za-z0-9_.-]+/g, '_');
|
||||
if (isEmpty(filename))
|
||||
filename = 'ruleset';
|
||||
|
||||
return filename + ((format === 'source') ? '.json' : '.srs');
|
||||
}
|
||||
|
||||
function ruleset_default_path(tag, format) {
|
||||
return RULESET_DIR + '/' + ruleset_file_name(tag, format);
|
||||
}
|
||||
|
||||
function ruleset_remote_path(path, tag, format) {
|
||||
if (isEmpty(path))
|
||||
return ruleset_default_path(tag, format);
|
||||
|
||||
if (match(path, /\/$/))
|
||||
return path + ruleset_file_name(tag, format);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']?.[0];
|
||||
if (!wan_dns)
|
||||
wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '8.8.8.8' : '223.5.5.5';
|
||||
wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '9.9.9.9' : '223.5.5.5';
|
||||
|
||||
const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333';
|
||||
|
||||
const ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server') || 'time.apple.com';
|
||||
let ntp_enabled = uci.get(uciconfig, ucintp, 'enabled'),
|
||||
ntp_server = uci.get(uciconfig, ucintp, 'server'),
|
||||
ntp_server_port = uci.get(uciconfig, ucintp, 'server_port'),
|
||||
ntp_interval = uci.get(uciconfig, ucintp, 'interval');
|
||||
|
||||
if (isEmpty(ntp_enabled) && isEmpty(ntp_server)) {
|
||||
ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server');
|
||||
ntp_enabled = !isEmpty(ntp_server) ? '1' : '0';
|
||||
}
|
||||
|
||||
const ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0';
|
||||
|
||||
let main_node, main_udp_node, dedicated_udp_node, default_outbound, default_outbound_dns,
|
||||
let cache_file_enabled, cache_file_path, cache_store_rdrc, cache_rdrc_timeout,
|
||||
main_node, main_udp_node, dedicated_udp_node, default_outbound, default_outbound_dns,
|
||||
domain_strategy, sniff_override, dns_server, china_dns_server, dns_default_strategy,
|
||||
dns_default_server, dns_disable_cache, dns_disable_cache_expire, dns_independent_cache,
|
||||
dns_client_subnet, cache_file_store_rdrc, cache_file_rdrc_timeout, direct_domain_list,
|
||||
dns_client_subnet, direct_domain_list,
|
||||
proxy_domain_list;
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
@@ -93,8 +137,6 @@ if (routing_mode !== 'custom') {
|
||||
dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
|
||||
dns_independent_cache = uci.get(uciconfig, ucidnssetting, 'independent_cache');
|
||||
dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
|
||||
cache_file_store_rdrc = uci.get(uciconfig, ucidnssetting, 'cache_file_store_rdrc'),
|
||||
cache_file_rdrc_timeout = uci.get(uciconfig, ucidnssetting, 'cache_file_rdrc_timeout');
|
||||
|
||||
/* Routing settings */
|
||||
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
|
||||
@@ -110,7 +152,7 @@ const mixed_port = uci.get(uciconfig, uciinfra, 'mixed_port') || '5330';
|
||||
|
||||
let self_mark, redirect_port, tproxy_port, tun_name,
|
||||
tun_addr4, tun_addr6, tun_mtu, tcpip_stack,
|
||||
endpoint_independent_nat, udp_timeout;
|
||||
udp_timeout;
|
||||
|
||||
if (routing_mode === 'custom')
|
||||
udp_timeout = uci.get(uciconfig, uciroutingsetting, 'udp_timeout');
|
||||
@@ -130,13 +172,18 @@ if (match(proxy_mode, /tun/)) {
|
||||
tun_addr6 = uci.get(uciconfig, uciinfra, 'tun_addr6') || 'fdfe:dcba:9876::1/126';
|
||||
tun_mtu = uci.get(uciconfig, uciinfra, 'tun_mtu') || '9000';
|
||||
tcpip_stack = 'system';
|
||||
if (routing_mode === 'custom') {
|
||||
if (routing_mode === 'custom')
|
||||
tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'system';
|
||||
endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat');
|
||||
}
|
||||
}
|
||||
|
||||
const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn';
|
||||
|
||||
cache_file_enabled = uci.get(uciconfig, ucicache, 'enabled');
|
||||
cache_file_path = uci.get(uciconfig, ucicache, 'path');
|
||||
cache_store_rdrc = uci.get(uciconfig, ucicache, 'store_rdrc');
|
||||
cache_rdrc_timeout = uci.get(uciconfig, ucicache, 'rdrc_timeout');
|
||||
|
||||
let clash_api = null;
|
||||
/* UCI config end */
|
||||
|
||||
/* Config helper start */
|
||||
@@ -168,6 +215,31 @@ function parse_dnsserver(server_addr, default_protocol) {
|
||||
}
|
||||
}
|
||||
|
||||
function parse_custom_dnsserver(cfg) {
|
||||
let server = cfg.server,
|
||||
server_port = (cfg.type in ['https', 'h3']) ? null : strToInt(cfg.server_port),
|
||||
path = cfg.path;
|
||||
|
||||
if (!isEmpty(server) && match(server, /:\/\//)) {
|
||||
const server_url = parseURL(server);
|
||||
|
||||
server = server_url.hostname;
|
||||
if (isEmpty(server_port))
|
||||
server_port = strToInt(server_url.port);
|
||||
if ((cfg.type in ['https', 'h3']) && isEmpty(path) && server_url.pathname !== '/')
|
||||
path = server_url.pathname;
|
||||
}
|
||||
if ((cfg.type in ['https', 'h3']) && isEmpty(path))
|
||||
path = '/dns-query';
|
||||
|
||||
return {
|
||||
type: cfg.type,
|
||||
server,
|
||||
server_port,
|
||||
path
|
||||
};
|
||||
}
|
||||
|
||||
function parse_dnsquery(strquery) {
|
||||
if (type(strquery) !== 'array' || isEmpty(strquery))
|
||||
return null;
|
||||
@@ -180,13 +252,149 @@ function parse_dnsquery(strquery) {
|
||||
|
||||
}
|
||||
|
||||
function filter_existing_nodes(nodes) {
|
||||
if (type(nodes) !== 'array' || isEmpty(nodes))
|
||||
return [];
|
||||
|
||||
return filter(nodes, (k) => {
|
||||
const node = uci.get_all(uciconfig, k) || {};
|
||||
return !isEmpty(node);
|
||||
});
|
||||
}
|
||||
|
||||
function collect_group_nodes(groups, subscription_nodes, nodes, legacy_nodes) {
|
||||
let result = [];
|
||||
|
||||
for (let group in normalize_list(groups))
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
if (cfg.grouphash === group && !~index(result, cfg['.name']))
|
||||
push(result, cfg['.name']);
|
||||
});
|
||||
|
||||
for (let node in filter_existing_nodes(normalize_list(subscription_nodes)))
|
||||
if (!~index(result, node))
|
||||
push(result, node);
|
||||
|
||||
for (let node in filter_existing_nodes(normalize_list(nodes)))
|
||||
if (!~index(result, node))
|
||||
push(result, node);
|
||||
|
||||
for (let node in filter_existing_nodes(normalize_list(legacy_nodes)))
|
||||
if (!~index(result, node))
|
||||
push(result, node);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function collect_policy_nodes(nodes, current) {
|
||||
let result = [];
|
||||
|
||||
for (let node in normalize_list(nodes)) {
|
||||
if (node === current || ~index(result, node))
|
||||
continue;
|
||||
|
||||
const outbound = uci.get_all(uciconfig, node) || {};
|
||||
if (!isEmpty(outbound) && outbound['.type'] === uciroutingnode && outbound.enabled === '1' && outbound.node in ['urltest', 'selector'])
|
||||
push(result, node);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const reserved_outbound_tags = {
|
||||
'GLOBAL': true,
|
||||
'main-out': true,
|
||||
'main-udp-out': true,
|
||||
'direct-out': true,
|
||||
'block-out': true
|
||||
};
|
||||
|
||||
let outbound_tag_map = {},
|
||||
outbound_tag_used = {};
|
||||
|
||||
function normalize_outbound_tag(tag) {
|
||||
if (isEmpty(tag))
|
||||
return null;
|
||||
|
||||
tag = trim(sprintf('%s', tag));
|
||||
tag = replace(tag, /[\r\n\t]+/g, ' ');
|
||||
tag = replace(tag, /[\/\\?#%]+/g, '-');
|
||||
tag = replace(tag, /\s+/g, ' ');
|
||||
|
||||
return tag || null;
|
||||
}
|
||||
|
||||
function section_display_name(section) {
|
||||
if (type(section) !== 'object' || isEmpty(section))
|
||||
return null;
|
||||
|
||||
let label = normalize_outbound_tag(section.label);
|
||||
if (!isEmpty(label))
|
||||
return label;
|
||||
|
||||
if (section['.type'] === ucinode) {
|
||||
const address = trim(section.override_address || section.address || '');
|
||||
const port = trim(section.override_port || section.port || '');
|
||||
|
||||
if (address && port)
|
||||
return normalize_outbound_tag(`${address}:${port}`);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function register_outbound_tag(section_id, display_name) {
|
||||
if (isEmpty(section_id))
|
||||
return null;
|
||||
|
||||
let tag = outbound_tag_map[section_id];
|
||||
if (!isEmpty(tag))
|
||||
return tag;
|
||||
|
||||
const fallback_tag = `cfg-${section_id}-out`;
|
||||
const base_tag = normalize_outbound_tag(display_name) || fallback_tag;
|
||||
|
||||
tag = base_tag;
|
||||
let suffix = 2;
|
||||
while (reserved_outbound_tags[tag] || outbound_tag_used[tag]) {
|
||||
tag = `${base_tag} (${suffix})`;
|
||||
suffix++;
|
||||
}
|
||||
|
||||
outbound_tag_map[section_id] = tag;
|
||||
outbound_tag_used[tag] = true;
|
||||
|
||||
return tag;
|
||||
}
|
||||
|
||||
function get_section_outbound_tag(section_id) {
|
||||
if (isEmpty(section_id))
|
||||
return null;
|
||||
|
||||
let tag = outbound_tag_map[section_id];
|
||||
if (!isEmpty(tag))
|
||||
return tag;
|
||||
|
||||
const section = uci.get_all(uciconfig, section_id) || {};
|
||||
return register_outbound_tag(section_id, section_display_name(section));
|
||||
}
|
||||
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
register_outbound_tag(cfg['.name'], section_display_name(cfg));
|
||||
});
|
||||
|
||||
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
|
||||
if (cfg.enabled === '1')
|
||||
register_outbound_tag(cfg['.name'], section_display_name(cfg));
|
||||
});
|
||||
|
||||
function generate_endpoint(node) {
|
||||
if (type(node) !== 'object' || isEmpty(node))
|
||||
return null;
|
||||
|
||||
const endpoint = {
|
||||
type: node.type,
|
||||
tag: 'cfg-' + node['.name'] + '-out',
|
||||
tag: get_section_outbound_tag(node['.name']),
|
||||
address: node.wireguard_local_address,
|
||||
mtu: strToInt(node.wireguard_mtu),
|
||||
private_key: node.wireguard_private_key,
|
||||
@@ -217,13 +425,14 @@ function generate_outbound(node) {
|
||||
if (type(node) !== 'object' || isEmpty(node))
|
||||
return null;
|
||||
|
||||
const tls_utls_value = (node.type === 'anytls' && isEmpty(node.tls_utls)) ? 'chrome' : node.tls_utls;
|
||||
const outbound = {
|
||||
type: node.type,
|
||||
tag: 'cfg-' + node['.name'] + '-out',
|
||||
tag: get_section_outbound_tag(node['.name']),
|
||||
routing_mark: strToInt(self_mark),
|
||||
|
||||
server: node.address,
|
||||
server_port: strToInt(node.port),
|
||||
server: (node.type !== 'direct') ? node.address : null,
|
||||
server_port: (node.type !== 'direct') ? strToInt(node.port) : null,
|
||||
/* Hysteria(2) */
|
||||
server_ports: node.hysteria_hopping_port,
|
||||
|
||||
@@ -231,10 +440,6 @@ function generate_outbound(node) {
|
||||
user: (node.type === 'ssh') ? node.username : null,
|
||||
password: node.password,
|
||||
|
||||
/* Direct */
|
||||
override_address: node.override_address,
|
||||
override_port: strToInt(node.override_port),
|
||||
proxy_protocol: strToInt(node.proxy_protocol),
|
||||
/* AnyTLS */
|
||||
idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval),
|
||||
idle_session_timeout: strToTime(node.anytls_idle_session_timeout),
|
||||
@@ -306,9 +511,9 @@ function generate_outbound(node) {
|
||||
config: node.tls_ech_config,
|
||||
config_path: node.tls_ech_config_path
|
||||
} : null,
|
||||
utls: !isEmpty(node.tls_utls) ? {
|
||||
utls: !isEmpty(tls_utls_value) ? {
|
||||
enabled: true,
|
||||
fingerprint: node.tls_utls
|
||||
fingerprint: tls_utls_value
|
||||
} : null,
|
||||
reality: (node.tls_reality === '1') ? {
|
||||
enabled: true,
|
||||
@@ -364,10 +569,10 @@ function get_outbound(cfg) {
|
||||
const node = uci.get(uciconfig, cfg, 'node');
|
||||
if (isEmpty(node))
|
||||
die(sprintf("%s's node is missing, please check your configuration.", cfg));
|
||||
else if (node === 'urltest')
|
||||
return 'cfg-' + cfg + '-out';
|
||||
else if (node in ['urltest', 'selector'])
|
||||
return get_section_outbound_tag(cfg);
|
||||
else
|
||||
return 'cfg-' + node + '-out';
|
||||
return get_section_outbound_tag(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -390,10 +595,117 @@ function get_ruleset(cfg) {
|
||||
return null;
|
||||
|
||||
let rules = [];
|
||||
for (let i in cfg)
|
||||
push(rules, isEmpty(i) ? null : 'cfg-' + i + '-rule');
|
||||
for (let i in cfg) {
|
||||
if (isEmpty(i)) {
|
||||
push(rules, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
const ruleset = uci.get_all(uciconfig, i);
|
||||
push(rules, !isEmpty(ruleset?.tag) ? ruleset.tag : ('cfg-' + i + '-rule'));
|
||||
}
|
||||
return rules;
|
||||
}
|
||||
|
||||
function subscription_provider_name(name) {
|
||||
name = trim(name || '');
|
||||
return name;
|
||||
}
|
||||
|
||||
function unique_provider_name(name, used) {
|
||||
const base = name;
|
||||
let suffix = 2;
|
||||
|
||||
while (used[name]) {
|
||||
name = sprintf('%s (%d)', base, suffix);
|
||||
suffix++;
|
||||
}
|
||||
used[name] = true;
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
function get_subscription_info(group_hash) {
|
||||
const value = uci.get(uciconfig, 'subscription', 'subinfo_' + substr(group_hash, 0, 16));
|
||||
if (isEmpty(value))
|
||||
return null;
|
||||
|
||||
try {
|
||||
const info = json(value);
|
||||
if (type(info) !== 'object')
|
||||
return null;
|
||||
|
||||
const has_subscription_info = ('upload' in info) || ('download' in info) ||
|
||||
('total' in info) || ('expire' in info);
|
||||
|
||||
return {
|
||||
updated_at: info.updated_at,
|
||||
subscription_info: has_subscription_info ? {
|
||||
upload: info.upload,
|
||||
download: info.download,
|
||||
total: info.total,
|
||||
expire: info.expire
|
||||
} : null
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function build_proxy_providers() {
|
||||
let providers = [],
|
||||
used_names = {},
|
||||
subscription_names = normalize_list(uci.get(uciconfig, 'subscription', 'subscription_name')),
|
||||
sub_index = 0;
|
||||
|
||||
for (let suburl in normalize_list(uci.get(uciconfig, 'subscription', 'subscription_url'))) {
|
||||
const subscription_name = subscription_names[sub_index];
|
||||
sub_index++;
|
||||
|
||||
if (isEmpty(suburl) || isEmpty(trim(subscription_name || '')))
|
||||
continue;
|
||||
|
||||
const url = replace(suburl, /#.*$/, ''),
|
||||
group_hash = md5(url);
|
||||
|
||||
let proxies = [];
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
if (cfg.grouphash !== group_hash)
|
||||
return;
|
||||
|
||||
const tag = get_section_outbound_tag(cfg['.name']);
|
||||
if (!isEmpty(tag) && !~index(proxies, tag))
|
||||
push(proxies, tag);
|
||||
});
|
||||
|
||||
if (isEmpty(proxies))
|
||||
continue;
|
||||
|
||||
const subscription_info = get_subscription_info(group_hash);
|
||||
|
||||
push(providers, {
|
||||
name: unique_provider_name(subscription_provider_name(subscription_name), used_names),
|
||||
type: 'Proxy',
|
||||
vehicle_type: 'HTTP',
|
||||
proxies: proxies,
|
||||
update_id: (routing_mode === 'custom') ? group_hash : null,
|
||||
updated_at: subscription_info?.updated_at,
|
||||
subscription_info: subscription_info?.subscription_info
|
||||
});
|
||||
}
|
||||
|
||||
return providers;
|
||||
}
|
||||
|
||||
clash_api = {
|
||||
external_controller: uci.get(uciconfig, uciclashapi, 'external_controller') || '127.0.0.1:9090',
|
||||
external_ui: uci.get(uciconfig, uciclashapi, 'external_ui'),
|
||||
external_ui_download_url: uci.get(uciconfig, uciclashapi, 'external_ui_download_url'),
|
||||
external_ui_download_detour: get_outbound(uci.get(uciconfig, uciclashapi, 'external_ui_download_detour')),
|
||||
secret: uci.get(uciconfig, uciclashapi, 'secret'),
|
||||
default_mode: uci.get(uciconfig, uciclashapi, 'default_mode') || 'rule',
|
||||
proxy_providers: build_proxy_providers()
|
||||
};
|
||||
/* Config helper end */
|
||||
|
||||
const config = {};
|
||||
@@ -409,8 +721,10 @@ config.log = {
|
||||
/* NTP */
|
||||
if (!isEmpty(ntp_server))
|
||||
config.ntp = {
|
||||
enabled: true,
|
||||
enabled: ntp_enabled === '1',
|
||||
server: ntp_server,
|
||||
server_port: strToInt(ntp_server_port),
|
||||
interval: ntp_interval,
|
||||
detour: 'direct-out',
|
||||
domain_resolver: 'default-dns',
|
||||
};
|
||||
@@ -520,10 +834,7 @@ if (!isEmpty(main_node)) {
|
||||
|
||||
push(config.dns.servers, {
|
||||
tag: 'cfg-' + cfg['.name'] + '-dns',
|
||||
type: cfg.type,
|
||||
server: cfg.server,
|
||||
server_port: strToInt(cfg.server_port),
|
||||
path: cfg.path,
|
||||
...parse_custom_dnsserver(cfg),
|
||||
headers: cfg.headers,
|
||||
tls: cfg.tls_sni ? {
|
||||
enabled: true,
|
||||
@@ -565,7 +876,6 @@ if (!isEmpty(main_node)) {
|
||||
user: cfg.user,
|
||||
rule_set: get_ruleset(cfg.rule_set),
|
||||
rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source),
|
||||
rule_set_ip_cidr_accept_empty: strToBool(cfg.rule_set_ip_cidr_accept_empty),
|
||||
invert: strToBool(cfg.invert),
|
||||
outbound: get_outbound(cfg.outbound),
|
||||
action: cfg.action,
|
||||
@@ -606,8 +916,6 @@ push(config.inbounds, {
|
||||
listen: '::',
|
||||
listen_port: int(mixed_port),
|
||||
udp_timeout: strToTime(udp_timeout),
|
||||
sniff: true,
|
||||
sniff_override_destination: strToBool(sniff_override),
|
||||
set_system_proxy: false
|
||||
});
|
||||
|
||||
@@ -617,9 +925,7 @@ if (match(proxy_mode, /redirect/))
|
||||
tag: 'redirect-in',
|
||||
|
||||
listen: '::',
|
||||
listen_port: int(redirect_port),
|
||||
sniff: true,
|
||||
sniff_override_destination: strToBool(sniff_override)
|
||||
listen_port: int(redirect_port)
|
||||
});
|
||||
if (match(proxy_mode, /tproxy/))
|
||||
push(config.inbounds, {
|
||||
@@ -629,9 +935,7 @@ if (match(proxy_mode, /tproxy/))
|
||||
listen: '::',
|
||||
listen_port: int(tproxy_port),
|
||||
network: 'udp',
|
||||
udp_timeout: strToTime(udp_timeout),
|
||||
sniff: true,
|
||||
sniff_override_destination: strToBool(sniff_override)
|
||||
udp_timeout: strToTime(udp_timeout)
|
||||
});
|
||||
if (match(proxy_mode, /tun/))
|
||||
push(config.inbounds, {
|
||||
@@ -642,11 +946,8 @@ if (match(proxy_mode, /tun/))
|
||||
address: (ipv6_support === '1') ? [tun_addr4, tun_addr6] : [tun_addr4],
|
||||
mtu: strToInt(tun_mtu),
|
||||
auto_route: false,
|
||||
endpoint_independent_nat: strToBool(endpoint_independent_nat),
|
||||
udp_timeout: strToTime(udp_timeout),
|
||||
stack: tcpip_stack,
|
||||
sniff: true,
|
||||
sniff_override_destination: strToBool(sniff_override)
|
||||
stack: tcpip_stack
|
||||
});
|
||||
/* Inbound end */
|
||||
|
||||
@@ -671,14 +972,16 @@ if (!isEmpty(main_node)) {
|
||||
let urltest_nodes = [];
|
||||
|
||||
if (main_node === 'urltest') {
|
||||
const main_urltest_nodes = uci.get(uciconfig, ucimain, 'main_urltest_nodes') || [];
|
||||
const main_urltest_nodes = filter_existing_nodes(
|
||||
normalize_list(uci.get(uciconfig, ucimain, 'main_urltest_nodes'))
|
||||
);
|
||||
const main_urltest_interval = uci.get(uciconfig, ucimain, 'main_urltest_interval');
|
||||
const main_urltest_tolerance = uci.get(uciconfig, ucimain, 'main_urltest_tolerance');
|
||||
|
||||
push(config.outbounds, {
|
||||
type: 'urltest',
|
||||
tag: 'main-out',
|
||||
outbounds: map(main_urltest_nodes, (k) => `cfg-${k}-out`),
|
||||
outbounds: map(main_urltest_nodes, (k) => get_section_outbound_tag(k)),
|
||||
interval: strToTime(main_urltest_interval),
|
||||
tolerance: strToInt(main_urltest_tolerance),
|
||||
idle_timeout: (strToInt(main_urltest_interval) > 1800) ? `${main_urltest_interval * 2}s` : null,
|
||||
@@ -687,23 +990,31 @@ if (!isEmpty(main_node)) {
|
||||
} else {
|
||||
const main_node_cfg = uci.get_all(uciconfig, main_node) || {};
|
||||
if (main_node_cfg.type === 'wireguard') {
|
||||
push(config.endpoints, generate_endpoint(main_node_cfg));
|
||||
config.endpoints[length(config.endpoints)-1].tag = 'main-out';
|
||||
const main_endpoint = generate_endpoint(main_node_cfg);
|
||||
if (main_endpoint) {
|
||||
main_endpoint.tag = 'main-out';
|
||||
push(config.endpoints, main_endpoint);
|
||||
}
|
||||
} else {
|
||||
push(config.outbounds, generate_outbound(main_node_cfg));
|
||||
config.outbounds[length(config.outbounds)-1].tag = 'main-out';
|
||||
const main_outbound = generate_outbound(main_node_cfg);
|
||||
if (main_outbound) {
|
||||
main_outbound.tag = 'main-out';
|
||||
push(config.outbounds, main_outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (main_udp_node === 'urltest') {
|
||||
const main_udp_urltest_nodes = uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes') || [];
|
||||
const main_udp_urltest_nodes = filter_existing_nodes(
|
||||
normalize_list(uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes'))
|
||||
);
|
||||
const main_udp_urltest_interval = uci.get(uciconfig, ucimain, 'main_udp_urltest_interval');
|
||||
const main_udp_urltest_tolerance = uci.get(uciconfig, ucimain, 'main_udp_urltest_tolerance');
|
||||
|
||||
push(config.outbounds, {
|
||||
type: 'urltest',
|
||||
tag: 'main-udp-out',
|
||||
outbounds: map(main_udp_urltest_nodes, (k) => `cfg-${k}-out`),
|
||||
outbounds: map(main_udp_urltest_nodes, (k) => get_section_outbound_tag(k)),
|
||||
interval: strToTime(main_udp_urltest_interval),
|
||||
tolerance: strToInt(main_udp_urltest_tolerance),
|
||||
idle_timeout: (strToInt(main_udp_urltest_interval) > 1800) ? `${main_udp_urltest_interval * 2}s` : null,
|
||||
@@ -712,22 +1023,37 @@ if (!isEmpty(main_node)) {
|
||||
} else if (dedicated_udp_node) {
|
||||
const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {};
|
||||
if (main_udp_node_cfg.type === 'wireguard') {
|
||||
push(config.endpoints, generate_endpoint(main_udp_node_cfg));
|
||||
config.endpoints[length(config.endpoints)-1].tag = 'main-udp-out';
|
||||
const main_udp_endpoint = generate_endpoint(main_udp_node_cfg);
|
||||
if (main_udp_endpoint) {
|
||||
main_udp_endpoint.tag = 'main-udp-out';
|
||||
push(config.endpoints, main_udp_endpoint);
|
||||
}
|
||||
} else {
|
||||
push(config.outbounds, generate_outbound(main_udp_node_cfg));
|
||||
config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out';
|
||||
const main_udp_outbound = generate_outbound(main_udp_node_cfg);
|
||||
if (main_udp_outbound) {
|
||||
main_udp_outbound.tag = 'main-udp-out';
|
||||
push(config.outbounds, main_udp_outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i in urltest_nodes) {
|
||||
const urltest_node = uci.get_all(uciconfig, i) || {};
|
||||
if (isEmpty(urltest_node))
|
||||
continue;
|
||||
|
||||
if (urltest_node.type === 'wireguard') {
|
||||
push(config.endpoints, generate_endpoint(urltest_node));
|
||||
config.endpoints[length(config.endpoints)-1].tag = 'cfg-' + i + '-out';
|
||||
const endpoint = generate_endpoint(urltest_node);
|
||||
if (endpoint) {
|
||||
endpoint.tag = get_section_outbound_tag(i);
|
||||
push(config.endpoints, endpoint);
|
||||
}
|
||||
} else {
|
||||
push(config.outbounds, generate_outbound(urltest_node));
|
||||
config.outbounds[length(config.outbounds)-1].tag = 'cfg-' + i + '-out';
|
||||
const outbound = generate_outbound(urltest_node);
|
||||
if (outbound) {
|
||||
outbound.tag = get_section_outbound_tag(i);
|
||||
push(config.outbounds, outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!isEmpty(default_outbound)) {
|
||||
@@ -739,37 +1065,63 @@ if (!isEmpty(main_node)) {
|
||||
return;
|
||||
|
||||
if (cfg.node === 'urltest') {
|
||||
const legacy_nodes = (isEmpty(cfg.subscription_groups) && isEmpty(cfg.subscription_nodes) && isEmpty(cfg.selected_nodes)) ? cfg.urltest_nodes : null;
|
||||
const urltest_list = collect_group_nodes(cfg.subscription_groups, cfg.subscription_nodes, cfg.selected_nodes, legacy_nodes);
|
||||
push(config.outbounds, {
|
||||
type: 'urltest',
|
||||
tag: 'cfg-' + cfg['.name'] + '-out',
|
||||
outbounds: map(cfg.urltest_nodes, (k) => `cfg-${k}-out`),
|
||||
tag: get_section_outbound_tag(cfg['.name']),
|
||||
outbounds: map(urltest_list, (k) => get_section_outbound_tag(k)),
|
||||
url: cfg.urltest_url,
|
||||
interval: strToTime(cfg.urltest_interval),
|
||||
tolerance: strToInt(cfg.urltest_tolerance),
|
||||
idle_timeout: strToTime(cfg.urltest_idle_timeout),
|
||||
interrupt_exist_connections: strToBool(cfg.urltest_interrupt_exist_connections)
|
||||
});
|
||||
urltest_nodes = [...urltest_nodes, ...filter(cfg.urltest_nodes, (l) => !~index(urltest_nodes, l))];
|
||||
urltest_nodes = [...urltest_nodes, ...filter(urltest_list, (l) => !~index(urltest_nodes, l))];
|
||||
} else if (cfg.node === 'selector') {
|
||||
const selector_node_list = collect_group_nodes(cfg.subscription_groups, cfg.subscription_nodes, cfg.selected_nodes, null);
|
||||
const selector_policy_list = collect_policy_nodes(cfg.policy_nodes, cfg['.name']);
|
||||
const selector_list = [...selector_node_list, ...filter(selector_policy_list, (l) => !~index(selector_node_list, l))];
|
||||
const selector_default = (!isEmpty(cfg.selector_default) && ~index(selector_list, cfg.selector_default)) ? cfg.selector_default : null;
|
||||
push(config.outbounds, {
|
||||
type: 'selector',
|
||||
tag: get_section_outbound_tag(cfg['.name']),
|
||||
outbounds: map(selector_list, (k) => get_section_outbound_tag(k)),
|
||||
default: selector_default ? get_section_outbound_tag(selector_default) : null,
|
||||
interrupt_exist_connections: strToBool(cfg.selector_interrupt_exist_connections)
|
||||
});
|
||||
urltest_nodes = [...urltest_nodes, ...filter(selector_node_list, (l) => !~index(urltest_nodes, l))];
|
||||
} else {
|
||||
const outbound = uci.get_all(uciconfig, cfg.node) || {};
|
||||
if (isEmpty(outbound))
|
||||
return;
|
||||
|
||||
if (outbound.type === 'wireguard') {
|
||||
push(config.endpoints, generate_endpoint(outbound));
|
||||
config.endpoints[length(config.endpoints)-1].bind_interface = cfg.bind_interface;
|
||||
config.endpoints[length(config.endpoints)-1].detour = get_outbound(cfg.outbound);
|
||||
const endpoint = generate_endpoint(outbound);
|
||||
if (!endpoint)
|
||||
return;
|
||||
|
||||
endpoint.bind_interface = cfg.bind_interface;
|
||||
endpoint.detour = get_outbound(cfg.outbound);
|
||||
if (cfg.domain_resolver)
|
||||
config.endpoints[length(config.endpoints)-1].domain_resolver = {
|
||||
endpoint.domain_resolver = {
|
||||
server: get_resolver(cfg.domain_resolver),
|
||||
strategy: cfg.domain_strategy
|
||||
};
|
||||
push(config.endpoints, endpoint);
|
||||
} else {
|
||||
push(config.outbounds, generate_outbound(outbound));
|
||||
config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface;
|
||||
config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound);
|
||||
const routed_outbound = generate_outbound(outbound);
|
||||
if (!routed_outbound)
|
||||
return;
|
||||
|
||||
routed_outbound.bind_interface = cfg.bind_interface;
|
||||
routed_outbound.detour = get_outbound(cfg.outbound);
|
||||
if (cfg.domain_resolver)
|
||||
config.outbounds[length(config.outbounds)-1].domain_resolver = {
|
||||
routed_outbound.domain_resolver = {
|
||||
server: get_resolver(cfg.domain_resolver),
|
||||
strategy: cfg.domain_strategy
|
||||
};
|
||||
push(config.outbounds, routed_outbound);
|
||||
}
|
||||
push(routing_nodes, cfg.node);
|
||||
}
|
||||
@@ -777,10 +1129,18 @@ if (!isEmpty(main_node)) {
|
||||
|
||||
for (let i in filter(urltest_nodes, (l) => !~index(routing_nodes, l))) {
|
||||
const urltest_node = uci.get_all(uciconfig, i) || {};
|
||||
if (urltest_node.type === 'wireguard')
|
||||
push(config.endpoints, generate_endpoint(urltest_node));
|
||||
else
|
||||
push(config.outbounds, generate_outbound(urltest_node));
|
||||
if (isEmpty(urltest_node))
|
||||
continue;
|
||||
|
||||
if (urltest_node.type === 'wireguard') {
|
||||
const endpoint = generate_endpoint(urltest_node);
|
||||
if (endpoint)
|
||||
push(config.endpoints, endpoint);
|
||||
} else {
|
||||
const outbound = generate_outbound(urltest_node);
|
||||
if (outbound)
|
||||
push(config.outbounds, outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -795,13 +1155,10 @@ config.route = {
|
||||
{
|
||||
inbound: 'dns-in',
|
||||
action: 'hijack-dns'
|
||||
},
|
||||
{
|
||||
action: 'sniff'
|
||||
}
|
||||
/*
|
||||
* leave for sing-box 1.13.0
|
||||
* {
|
||||
* action: 'sniff'
|
||||
* }
|
||||
*/
|
||||
],
|
||||
rule_set: [],
|
||||
auto_detect_interface: isEmpty(default_interface) ? true : null,
|
||||
@@ -866,6 +1223,7 @@ if (!isEmpty(main_node)) {
|
||||
tag: 'geoip-cn',
|
||||
format: 'binary',
|
||||
url: 'https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs',
|
||||
path: ruleset_default_path('geoip-cn', 'binary'),
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
push(config.route.rule_set, {
|
||||
@@ -873,6 +1231,7 @@ if (!isEmpty(main_node)) {
|
||||
tag: 'geosite-cn',
|
||||
format: 'binary',
|
||||
url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs',
|
||||
path: ruleset_default_path('geosite-cn', 'binary'),
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
push(config.route.rule_set, {
|
||||
@@ -880,6 +1239,7 @@ if (!isEmpty(main_node)) {
|
||||
tag: 'geosite-noncn',
|
||||
format: 'binary',
|
||||
url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs',
|
||||
path: ruleset_default_path('geosite-noncn', 'binary'),
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
}
|
||||
@@ -924,17 +1284,25 @@ if (!isEmpty(main_node)) {
|
||||
user: cfg.user,
|
||||
rule_set: get_ruleset(cfg.rule_set),
|
||||
rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source),
|
||||
rule_set_ip_cidr_accept_empty: strToBool(cfg.rule_set_ip_cidr_accept_empty),
|
||||
invert: strToBool(cfg.invert),
|
||||
action: cfg.action,
|
||||
outbound: get_outbound(cfg.outbound),
|
||||
override_address: cfg.override_address,
|
||||
override_port: strToInt(cfg.override_port),
|
||||
udp_disable_domain_unmapping: strToBool(cfg.udp_disable_domain_unmapping),
|
||||
udp_connect: strToBool(cfg.udp_connect),
|
||||
udp_timeout: strToTime(cfg.udp_timeout),
|
||||
tls_fragment: strToBool(cfg.tls_fragment),
|
||||
tls_fragment_fallback_delay: strToTime(cfg.tls_fragment_fallback_delay),
|
||||
tls_record_fragment: strToBool(cfg.tls_record_fragment)
|
||||
outbound: (cfg.action === 'route') ? get_outbound(cfg.outbound) : null,
|
||||
server: (cfg.action === 'resolve') ? get_resolver(cfg.resolve_server) : null,
|
||||
strategy: (cfg.action === 'resolve') ? cfg.resolve_strategy : null,
|
||||
disable_cache: (cfg.action === 'resolve') ? strToBool(cfg.resolve_disable_cache) : null,
|
||||
rewrite_ttl: (cfg.action === 'resolve') ? strToInt(cfg.resolve_rewrite_ttl) : null,
|
||||
client_subnet: (cfg.action === 'resolve') ? cfg.resolve_client_subnet : null,
|
||||
override_address: (cfg.action in ['route', 'route-options']) ? cfg.override_address : null,
|
||||
override_port: (cfg.action in ['route', 'route-options']) ? strToInt(cfg.override_port) : null,
|
||||
udp_disable_domain_unmapping: (cfg.action in ['route', 'route-options']) ? strToBool(cfg.udp_disable_domain_unmapping) : null,
|
||||
udp_connect: (cfg.action in ['route', 'route-options']) ? strToBool(cfg.udp_connect) : null,
|
||||
udp_timeout: (cfg.action in ['route', 'route-options']) ? strToTime(cfg.udp_timeout) : null,
|
||||
tls_fragment: (cfg.action in ['route', 'route-options']) ? strToBool(cfg.tls_fragment) : null,
|
||||
tls_fragment_fallback_delay: (cfg.action in ['route', 'route-options']) ? strToTime(cfg.tls_fragment_fallback_delay) : null,
|
||||
tls_record_fragment: (cfg.action in ['route', 'route-options']) ? strToBool(cfg.tls_record_fragment) : null,
|
||||
method: (cfg.action === 'reject') ? cfg.reject_method : null,
|
||||
no_drop: (cfg.action === 'reject') ? strToBool(cfg.reject_no_drop) : null
|
||||
});
|
||||
});
|
||||
|
||||
@@ -945,14 +1313,16 @@ if (!isEmpty(main_node)) {
|
||||
if (cfg.enabled !== '1')
|
||||
return null;
|
||||
|
||||
const tag = !isEmpty(cfg.tag) ? cfg.tag : ('cfg-' + cfg['.name'] + '-rule');
|
||||
|
||||
push(config.route.rule_set, {
|
||||
type: cfg.type,
|
||||
tag: 'cfg-' + cfg['.name'] + '-rule',
|
||||
tag: tag,
|
||||
format: cfg.format,
|
||||
path: cfg.path,
|
||||
url: cfg.url,
|
||||
path: (cfg.type === 'remote') ? ruleset_remote_path(cfg.remote_path || cfg.path, tag, cfg.format) : cfg.path,
|
||||
download_detour: get_outbound(cfg.outbound),
|
||||
update_interval: cfg.update_interval
|
||||
update_interval: (cfg.type === 'remote') ? '87600h' : null
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -961,15 +1331,17 @@ if (!isEmpty(main_node)) {
|
||||
/* Experimental start */
|
||||
if (routing_mode in ['bypass_mainland_china', 'custom']) {
|
||||
config.experimental = {
|
||||
clash_api,
|
||||
cache_file: {
|
||||
enabled: true,
|
||||
path: RUN_DIR + '/cache.db',
|
||||
store_rdrc: strToBool(cache_file_store_rdrc),
|
||||
rdrc_timeout: strToTime(cache_file_rdrc_timeout),
|
||||
enabled: cache_file_enabled !== '0',
|
||||
path: cache_file_path || (HP_DIR + '/cache.db'),
|
||||
store_rdrc: strToBool(cache_store_rdrc),
|
||||
rdrc_timeout: strToTime(cache_rdrc_timeout),
|
||||
}
|
||||
};
|
||||
}
|
||||
/* Experimental end */
|
||||
|
||||
system('mkdir -p ' + RUN_DIR);
|
||||
system('mkdir -p ' + RULESET_DIR);
|
||||
writefile(RUN_DIR + '/sing-box-c.json', sprintf('%.J\n', removeBlankAttrs(config)));
|
||||
@@ -67,7 +67,7 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
password: cfg.hysteria_obfs_password
|
||||
} : cfg.hysteria_obfs_password,
|
||||
recv_window_conn: strToInt(cfg.hysteria_recv_window_conn),
|
||||
recv_window_client: strToInt(cfg.hysteria_revc_window_client),
|
||||
recv_window_client: strToInt(cfg.hysteria_recv_window_client),
|
||||
max_conn_client: strToInt(cfg.hysteria_max_conn_client),
|
||||
disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery),
|
||||
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
|
||||
|
||||
Regular → Executable
+22
-3
@@ -66,15 +66,34 @@ export function getTime(epoch) {
|
||||
|
||||
};
|
||||
|
||||
export function wGET(url, ua) {
|
||||
export function wGETResponse(url, ua) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
if (!ua)
|
||||
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
|
||||
|
||||
const output = executeCommand(`/usr/bin/wget -qO- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
|
||||
return trim(output.stdout);
|
||||
const output = executeCommand(`/usr/bin/wget -O- -S --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
|
||||
return {
|
||||
body: trim(output.stdout),
|
||||
headers: output.stderr || '',
|
||||
exitcode: output.exitcode
|
||||
};
|
||||
};
|
||||
|
||||
export function wGET(url, ua) {
|
||||
return wGETResponse(url, ua)?.body;
|
||||
};
|
||||
|
||||
export function wGETHeaders(url, ua) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
if (!ua)
|
||||
ua = 'clash.meta';
|
||||
|
||||
const output = executeCommand(`/usr/bin/wget -O /dev/null -S --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
|
||||
return output.stderr || '';
|
||||
};
|
||||
/* Utilities end */
|
||||
|
||||
|
||||
@@ -7,14 +7,34 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { popen } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty, parseURL, validation } from 'homeproxy';
|
||||
import { isEmpty, parseURL } from 'homeproxy';
|
||||
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
function random_secret() {
|
||||
const fd = popen('tr -dc "A-Za-z0-9" < /dev/urandom 2>/dev/null | head -c 16');
|
||||
let random = '';
|
||||
if (fd) {
|
||||
random = trim(fd.read('all') || '');
|
||||
fd.close();
|
||||
}
|
||||
|
||||
if (isEmpty(random)) {
|
||||
const fallback = popen('awk \'BEGIN{srand(); printf "%06d", int(rand() * 1000000)}\'');
|
||||
if (fallback) {
|
||||
random = trim(fallback.read('all') || '');
|
||||
fallback.close();
|
||||
}
|
||||
}
|
||||
|
||||
return random;
|
||||
}
|
||||
|
||||
const uciinfra = 'infra',
|
||||
ucimigration = 'migration',
|
||||
ucimain = 'config',
|
||||
@@ -25,8 +45,16 @@ const uciinfra = 'infra',
|
||||
ucirouting = 'routing',
|
||||
uciroutingnode = 'routing_node',
|
||||
uciroutingrule = 'routing_rule',
|
||||
uciclashapi = 'clash_api',
|
||||
ucintp = 'ntp',
|
||||
ucicache = 'cache',
|
||||
uciserver = 'server';
|
||||
|
||||
function is_update_cron(value) {
|
||||
let matched = match(value || '', /^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+([0-7*])$/);
|
||||
return !isEmpty(matched) && int(matched[1]) <= 59 && int(matched[2]) <= 23;
|
||||
}
|
||||
|
||||
/* chinadns-ng has been removed */
|
||||
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
|
||||
uci.delete(uciconfig, uciinfra, 'china_dns_port');
|
||||
@@ -36,9 +64,7 @@ const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
|
||||
if (type(china_dns_server) === 'array') {
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]);
|
||||
} else {
|
||||
if (china_dns_server === 'wan_114')
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', '114.114.114.114');
|
||||
else if (match(china_dns_server, /,/))
|
||||
if (match(china_dns_server, /,/))
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
|
||||
}
|
||||
|
||||
@@ -57,6 +83,93 @@ if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
|
||||
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
|
||||
uci.delete(uciconfig, uciinfra, 'tun_gso');
|
||||
|
||||
/* endpoint_independent_nat was removed in sing-box 1.13 */
|
||||
if (!isEmpty(uci.get(uciconfig, ucirouting, 'endpoint_independent_nat')))
|
||||
uci.delete(uciconfig, ucirouting, 'endpoint_independent_nat');
|
||||
|
||||
/* direct outbound proxy_protocol was removed in sing-box 1.13 */
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
if (cfg.type === 'direct' && !isEmpty(cfg.proxy_protocol))
|
||||
uci.delete(uciconfig, cfg['.name'], 'proxy_protocol');
|
||||
});
|
||||
|
||||
const legacy_panel_url = 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip';
|
||||
const legacy_panel_proxy_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip';
|
||||
const old_panel_url = 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
|
||||
const old_panel_proxy_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
|
||||
const default_panel_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
|
||||
|
||||
/* clash api panel options were introduced */
|
||||
if (!uci.get(uciconfig, uciclashapi))
|
||||
uci.set(uciconfig, uciclashapi, uciconfig);
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_ui')))
|
||||
uci.set(uciconfig, uciclashapi, 'external_ui', '/etc/homeproxy/run/ui');
|
||||
|
||||
const panel_url = uci.get(uciconfig, uciclashapi, 'external_ui_download_url');
|
||||
if (isEmpty(panel_url) || panel_url === legacy_panel_url || panel_url === legacy_panel_proxy_url || panel_url === old_panel_proxy_url)
|
||||
uci.set(uciconfig, uciclashapi, 'external_ui_download_url', default_panel_url);
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_ui_download_detour')))
|
||||
uci.set(uciconfig, uciclashapi, 'external_ui_download_detour', 'direct-out');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_controller')))
|
||||
uci.set(uciconfig, uciclashapi, 'external_controller', '0.0.0.0:9095');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'secret')) || uci.get(uciconfig, uciclashapi, 'secret') === 'homeproxy') {
|
||||
let random = random_secret();
|
||||
if (!isEmpty(random))
|
||||
uci.set(uciconfig, uciclashapi, 'secret', random);
|
||||
}
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'default_mode')))
|
||||
uci.set(uciconfig, uciclashapi, 'default_mode', 'rule');
|
||||
|
||||
/* ntp options were moved into a dedicated section */
|
||||
if (!uci.get(uciconfig, ucintp))
|
||||
uci.set(uciconfig, ucintp, uciconfig);
|
||||
|
||||
const legacy_ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server');
|
||||
if (isEmpty(uci.get(uciconfig, ucintp, 'enabled')))
|
||||
uci.set(uciconfig, ucintp, 'enabled', '1');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucintp, 'server')))
|
||||
uci.set(uciconfig, ucintp, 'server', !isEmpty(legacy_ntp_server) ? legacy_ntp_server : 'ntp.aliyun.com');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucintp, 'server_port')))
|
||||
uci.set(uciconfig, ucintp, 'server_port', '123');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucintp, 'interval')))
|
||||
uci.set(uciconfig, ucintp, 'interval', '30m');
|
||||
|
||||
/* cache file options were moved into a dedicated section */
|
||||
if (!uci.get(uciconfig, ucicache))
|
||||
uci.set(uciconfig, ucicache, uciconfig);
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucicache, 'enabled')))
|
||||
uci.set(uciconfig, ucicache, 'enabled', '1');
|
||||
|
||||
const old_cache_path = '/var/run/homeproxy/cache.db';
|
||||
const default_cache_path = '/etc/homeproxy/cache.db';
|
||||
const cache_path = uci.get(uciconfig, ucicache, 'path');
|
||||
if (isEmpty(cache_path) || cache_path === old_cache_path)
|
||||
uci.set(uciconfig, ucicache, 'path', default_cache_path);
|
||||
|
||||
if (!isEmpty(uci.get(uciconfig, ucicache, 'store_fakeip')))
|
||||
uci.delete(uciconfig, ucicache, 'store_fakeip');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucicache, 'store_rdrc')))
|
||||
uci.set(uciconfig, ucicache, 'store_rdrc', isEmpty(uci.get(uciconfig, ucidns, 'cache_file_store_rdrc')) ? '1' : uci.get(uciconfig, ucidns, 'cache_file_store_rdrc'));
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucicache, 'rdrc_timeout')) && !isEmpty(uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout')))
|
||||
uci.set(uciconfig, ucicache, 'rdrc_timeout', uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout'));
|
||||
|
||||
if (!isEmpty(uci.get(uciconfig, ucidns, 'cache_file_store_rdrc')))
|
||||
uci.delete(uciconfig, ucidns, 'cache_file_store_rdrc');
|
||||
|
||||
if (!isEmpty(uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout')))
|
||||
uci.delete(uciconfig, ucidns, 'cache_file_rdrc_timeout');
|
||||
|
||||
/* create migration section */
|
||||
if (!uci.get(uciconfig, ucimigration))
|
||||
uci.set(uciconfig, ucimigration, uciconfig);
|
||||
@@ -75,6 +188,10 @@ if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
|
||||
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
|
||||
uci.set(uciconfig, uciserver, 'log_level', 'warn');
|
||||
|
||||
if (!isEmpty(uci.get(uciconfig, 'subscription', 'auto_update_time')) &&
|
||||
!is_update_cron(uci.get(uciconfig, 'subscription', 'auto_update_time')))
|
||||
uci.set(uciconfig, 'subscription', 'auto_update_time', '0 0 * * *');
|
||||
|
||||
/* empty value defaults to all ports now */
|
||||
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
|
||||
uci.delete(uciconfig, ucimain, 'routing_port');
|
||||
@@ -239,6 +356,47 @@ uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* routing node options */
|
||||
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
|
||||
if (cfg.node === 'urltest' && !isEmpty(cfg.urltest_nodes)) {
|
||||
let custom_nodes = [],
|
||||
subscription_nodes = [];
|
||||
|
||||
for (let node in (type(cfg.urltest_nodes) === 'array' ? cfg.urltest_nodes : [ cfg.urltest_nodes ])) {
|
||||
let node_cfg = uci.get_all(uciconfig, node);
|
||||
if (isEmpty(node_cfg))
|
||||
continue;
|
||||
|
||||
if (!isEmpty(node_cfg.grouphash))
|
||||
push(subscription_nodes, node);
|
||||
else
|
||||
push(custom_nodes, node);
|
||||
}
|
||||
|
||||
if (isEmpty(cfg.selected_nodes) && !isEmpty(custom_nodes))
|
||||
uci.set(uciconfig, cfg['.name'], 'selected_nodes', custom_nodes);
|
||||
|
||||
if (isEmpty(cfg.subscription_nodes) && !isEmpty(subscription_nodes))
|
||||
uci.set(uciconfig, cfg['.name'], 'subscription_nodes', subscription_nodes);
|
||||
|
||||
uci.delete(uciconfig, cfg['.name'], 'urltest_nodes');
|
||||
}
|
||||
});
|
||||
|
||||
/* rule set options */
|
||||
uci.foreach(uciconfig, uciruleset, (cfg) => {
|
||||
if (isEmpty(cfg.tag))
|
||||
uci.set(uciconfig, cfg['.name'], 'tag', 'cfg-' + cfg['.name'] + '-rule');
|
||||
|
||||
if (cfg.type === 'remote') {
|
||||
if (isEmpty(cfg.auto_update))
|
||||
uci.set(uciconfig, cfg['.name'], 'auto_update', '1');
|
||||
|
||||
if (!is_update_cron(cfg.update_interval))
|
||||
uci.set(uciconfig, cfg['.name'], 'update_interval', '0 0 * * *');
|
||||
}
|
||||
});
|
||||
|
||||
/* server options */
|
||||
/* auto_firewall was moved into server options */
|
||||
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
|
||||
@@ -246,6 +404,13 @@ if (!isEmpty(auto_firewall))
|
||||
uci.delete(uciconfig, uciserver, 'auto_firewall');
|
||||
|
||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
if (!isEmpty(cfg.hysteria_revc_window_client) && isEmpty(cfg.hysteria_recv_window_client)) {
|
||||
uci.set(uciconfig, cfg['.name'], 'hysteria_recv_window_client', cfg.hysteria_revc_window_client);
|
||||
}
|
||||
|
||||
if (!isEmpty(cfg.hysteria_revc_window_client))
|
||||
uci.delete(uciconfig, cfg['.name'], 'hysteria_revc_window_client');
|
||||
|
||||
/* auto_firewall was moved into server options */
|
||||
if (auto_firewall === '1')
|
||||
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
set -eu
|
||||
|
||||
NAME="homeproxy"
|
||||
RUN_DIR="/var/run/$NAME"
|
||||
REQUEST_FILE="$RUN_DIR/provider-update-id"
|
||||
LOCK_FILE="$RUN_DIR/provider-update-v2.lock"
|
||||
LOG_FILE="$RUN_DIR/homeproxy.log"
|
||||
PROVIDER_ID="${1:-}"
|
||||
|
||||
log() {
|
||||
printf '%s [SUBSCRIBE] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
case "$PROVIDER_ID" in
|
||||
*[!0-9a-f]*|'') exit 1 ;;
|
||||
esac
|
||||
[ "${#PROVIDER_ID}" -eq 32 ] || exit 1
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
if ! lock -n "$LOCK_FILE" 2>/dev/null; then
|
||||
log "Another provider update is running; waiting for it to finish..."
|
||||
lock "$LOCK_FILE"
|
||||
fi
|
||||
|
||||
umask 077
|
||||
printf '%s\n' "$PROVIDER_ID" > "$REQUEST_FILE"
|
||||
cleanup() {
|
||||
rm -f "$REQUEST_FILE"
|
||||
lock -u "$LOCK_FILE" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
# Give the Clash API response time to reach the panel before HomeProxy restarts.
|
||||
log "Updating provider $PROVIDER_ID from Clash API..."
|
||||
sleep 1
|
||||
if /etc/homeproxy/scripts/update_subscriptions.uc; then
|
||||
log "Provider $PROVIDER_ID update completed."
|
||||
else
|
||||
log "Provider $PROVIDER_ID update failed."
|
||||
exit 4
|
||||
fi
|
||||
@@ -21,64 +21,65 @@ to_upper() {
|
||||
}
|
||||
|
||||
check_list_update() {
|
||||
local listtype="$1"
|
||||
local listrepo="$2"
|
||||
local listref="$3"
|
||||
local listname="$4"
|
||||
local lock="$RUN_DIR/update_resources-$listtype.lock"
|
||||
local github_token="$(uci -q get homeproxy.config.github_token)"
|
||||
local wget="wget --timeout=10 -q"
|
||||
local LIST_FILE="$1"
|
||||
local REPO_NAME="$2"
|
||||
local REPO_BRANCH="$3"
|
||||
local REPO_FILE="$4"
|
||||
local LOCK_FILE="$RUN_DIR/update_resources-$LIST_FILE.lock"
|
||||
local GITHUB_TOKEN="$(uci -q get homeproxy.config.github_token)"
|
||||
|
||||
exec 200>"$lock"
|
||||
if ! flock -n 200 &> "/dev/null"; then
|
||||
log "[$(to_upper "$listtype")] A task is already running."
|
||||
if ! lock -n "$LOCK_FILE" 2>"/dev/null"; then
|
||||
log "[$(to_upper "$LIST_FILE")] A task is already running."
|
||||
return 2
|
||||
fi
|
||||
|
||||
[ -z "$github_token" ] || github_token="--header=Authorization: Bearer $github_token"
|
||||
local list_info="$($wget "${github_token:--q}" -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")"
|
||||
local list_sha="$(echo -e "$list_info" | jsonfilter -qe "@[0].sha")"
|
||||
local list_ver="$(echo -e "$list_info" | jsonfilter -qe "@[0].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')"
|
||||
if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then
|
||||
log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later."
|
||||
local NEW_VER=$(curl -sL ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} "https://api.github.com/repos/$REPO_NAME/releases/latest" | jsonfilter -e "@.tag_name")
|
||||
if [ -z "$NEW_VER" ]; then
|
||||
log "[$(to_upper "$LIST_FILE")] Failed to get the latest version, please retry later."
|
||||
|
||||
return 1
|
||||
fi
|
||||
|
||||
local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT FOUND")"
|
||||
if [ "$local_list_ver" = "$list_ver" ]; then
|
||||
log "[$(to_upper "$listtype")] Current version: $list_ver."
|
||||
log "[$(to_upper "$listtype")] You're already at the latest version."
|
||||
local OLD_VER=$(cat "$RESOURCES_DIR/$LIST_FILE.ver" 2>/dev/null || echo "NOT FOUND")
|
||||
if [ "$OLD_VER" = "$NEW_VER" ]; then
|
||||
log "[$(to_upper "$LIST_FILE")] Current version: $NEW_VER."
|
||||
log "[$(to_upper "$LIST_FILE")] You're already at the latest version."
|
||||
|
||||
return 3
|
||||
else
|
||||
log "[$(to_upper "$listtype")] Local version: $local_list_ver, latest version: $list_ver."
|
||||
log "[$(to_upper "$LIST_FILE")] Local version: $OLD_VER, latest version: $NEW_VER."
|
||||
fi
|
||||
|
||||
if ! $wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname" || [ ! -s "$RUN_DIR/$listname" ]; then
|
||||
rm -f "$RUN_DIR/$listname"
|
||||
log "[$(to_upper "$listtype")] Update failed."
|
||||
if ! curl -sL -o "$RUN_DIR/$REPO_FILE" "https://cdn.jsdelivr.net/gh/$REPO_NAME@$REPO_BRANCH/$REPO_FILE" || [ ! -s "$RUN_DIR/$REPO_FILE" ]; then
|
||||
rm -f "$RUN_DIR/$REPO_FILE"
|
||||
log "[$(to_upper "$LIST_FILE")] Update failed."
|
||||
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}"
|
||||
echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
|
||||
log "[$(to_upper "$listtype")] Successfully updated."
|
||||
mv -f "$RUN_DIR/$REPO_FILE" "$RESOURCES_DIR/$LIST_FILE.${REPO_FILE##*.}"
|
||||
echo -e "$NEW_VER" > "$RESOURCES_DIR/$LIST_FILE.ver"
|
||||
log "[$(to_upper "$LIST_FILE")] Successfully updated."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"china_ip4")
|
||||
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt"
|
||||
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "cncidr.txt" && \
|
||||
sed -i "/IP-CIDR6,/d; s/IP-CIDR,//g" "$RESOURCES_DIR/china_ip4.txt"
|
||||
;;
|
||||
"china_ip6")
|
||||
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt"
|
||||
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "cncidr.txt" && \
|
||||
sed -i "/IP-CIDR,/d; s/IP-CIDR6,//g" "$RESOURCES_DIR/china_ip6.txt"
|
||||
;;
|
||||
"gfw_list")
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
|
||||
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "gfw.txt" && \
|
||||
sed -i "s/^\.//g" "$RESOURCES_DIR/gfw_list.txt"
|
||||
;;
|
||||
"china_list")
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \
|
||||
sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt"
|
||||
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "direct.txt" && \
|
||||
sed -i "s/^\.//g" "$RESOURCES_DIR/china_list.txt"
|
||||
;;
|
||||
*)
|
||||
echo -e "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2025 ImmortalWrt.org
|
||||
|
||||
CACHE_PATH="$(uci -q get homeproxy.cache.path)"
|
||||
[ -n "$CACHE_PATH" ] || CACHE_PATH="/etc/homeproxy/cache.db"
|
||||
|
||||
rm -f "$CACHE_PATH"
|
||||
/etc/init.d/homeproxy restart
|
||||
File diff suppressed because it is too large.
Load diff
@@ -27,13 +27,82 @@ log() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
init_logs() {
|
||||
mkdir -p "$RUN_DIR"
|
||||
mkdir -p "$HP_DIR/run/ui"
|
||||
touch "$LOG_PATH" "$RUN_DIR/sing-box-c.log" "$RUN_DIR/sing-box-s.log"
|
||||
chown -R sing-box:sing-box "$RUN_DIR" 2>"/dev/null"
|
||||
chown -R sing-box:sing-box "$HP_DIR/run" 2>"/dev/null"
|
||||
}
|
||||
|
||||
valid_update_cron() {
|
||||
echo "$1" | grep -Eq '^([0-9]|[0-5][0-9]) ([0-9]|[01][0-9]|2[0-3]) \* \* ([0-7]|\*)$'
|
||||
}
|
||||
|
||||
has_ruleset_cron() {
|
||||
printf '%s\n' "$ruleset_cron_list" | grep -Fxq -- "$1"
|
||||
}
|
||||
|
||||
add_ruleset_cron() {
|
||||
local section="$1"
|
||||
local enabled auto_update type update_time
|
||||
|
||||
config_get_bool enabled "$section" "enabled" "0"
|
||||
config_get_bool auto_update "$section" "auto_update" "1"
|
||||
config_get type "$section" "type" "local"
|
||||
config_get update_time "$section" "update_interval" ""
|
||||
|
||||
[ "$enabled" = "1" ] || return
|
||||
[ "$auto_update" = "1" ] || return
|
||||
[ "$type" = "remote" ] || return
|
||||
[ -n "$update_time" ] || return
|
||||
valid_update_cron "$update_time" || return
|
||||
has_ruleset_cron "$update_time" && return
|
||||
|
||||
echo -e "$update_time sh $HP_DIR/scripts/update_rulesets.sh #${CONF}_autosetup" >> "/etc/crontabs/root"
|
||||
ruleset_cron_list="${ruleset_cron_list}
|
||||
${update_time}"
|
||||
cron_reload="1"
|
||||
}
|
||||
|
||||
setup_auto_update_cron() {
|
||||
local cron_reload="0"
|
||||
mkdir -p "/etc/crontabs"
|
||||
touch "/etc/crontabs/root"
|
||||
grep -q "#${CONF}_autosetup" "/etc/crontabs/root" 2>"/dev/null" && cron_reload="1"
|
||||
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null"
|
||||
|
||||
local auto_update auto_update_time
|
||||
config_get_bool auto_update "subscription" "auto_update" "0"
|
||||
if [ "$auto_update" = "1" ]; then
|
||||
config_get auto_update_time "subscription" "auto_update_time" "0 0 * * *"
|
||||
if valid_update_cron "$auto_update_time"; then
|
||||
echo -e "$auto_update_time sh $HP_DIR/scripts/update_crond.sh #${CONF}_autosetup" >> "/etc/crontabs/root"
|
||||
cron_reload="1"
|
||||
fi
|
||||
fi
|
||||
|
||||
local ruleset_cron_list=""
|
||||
config_foreach add_ruleset_cron "ruleset"
|
||||
[ "$cron_reload" = "1" ] && /etc/init.d/cron restart
|
||||
}
|
||||
|
||||
start_service() {
|
||||
init_logs
|
||||
|
||||
config_load "$CONF"
|
||||
setup_auto_update_cron
|
||||
|
||||
local routing_mode proxy_mode
|
||||
config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
|
||||
config_get proxy_mode "config" "proxy_mode" "redirect_tproxy"
|
||||
|
||||
local cache_path
|
||||
config_get cache_path "cache" "path" "$HP_DIR/cache.db"
|
||||
mkdir -p "$(dirname "$cache_path")"
|
||||
[ -e "$cache_path" ] || touch "$cache_path"
|
||||
chown sing-box:sing-box "$cache_path" 2>"/dev/null"
|
||||
|
||||
local outbound_node
|
||||
if [ "$routing_mode" != "custom" ]; then
|
||||
config_get outbound_node "config" "main_node" "nil"
|
||||
@@ -48,8 +117,6 @@ start_service() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
# Generate/Validate client config
|
||||
ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH"
|
||||
@@ -62,16 +129,6 @@ start_service() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Auto update
|
||||
local auto_update auto_update_time
|
||||
config_get_bool auto_update "subscription" "auto_update" "0"
|
||||
if [ "$auto_update" = "1" ]; then
|
||||
config_get auto_update_time "subscription" "auto_update_time" "2"
|
||||
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null"
|
||||
echo -e "0 $auto_update_time * * * $HP_DIR/scripts/update_crond.sh #${CONF}_autosetup" >> "/etc/crontabs/root"
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
|
||||
# DNSMasq rules
|
||||
local ipv6_support dns_port
|
||||
config_get_bool ipv6_support "config" "ipv6_support" "0"
|
||||
@@ -104,8 +161,6 @@ start_service() {
|
||||
sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_proxy_addr_v4$wan_nftset_v6/g" \
|
||||
"$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf"
|
||||
fi
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
|
||||
# Setup routing table
|
||||
local table_mark
|
||||
config_get table_mark "infra" "table_mark" "100"
|
||||
@@ -156,7 +211,8 @@ start_service() {
|
||||
procd_add_jail "sing-box-c" log procfs
|
||||
procd_add_jail_mount "$RUN_DIR/sing-box-c.json"
|
||||
procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log"
|
||||
[ "$routing_mode" != "bypass_mainland_china" ] || procd_add_jail_mount_rw "$RUN_DIR/cache.db"
|
||||
procd_add_jail_mount_rw "$cache_path"
|
||||
procd_add_jail_mount_rw "$HP_DIR/run/"
|
||||
procd_add_jail_mount "$HP_DIR/certs/"
|
||||
procd_add_jail_mount "/etc/ssl/"
|
||||
procd_add_jail_mount "/etc/localtime"
|
||||
@@ -226,19 +282,15 @@ start_service() {
|
||||
procd_close_instance
|
||||
|
||||
case "$routing_mode" in
|
||||
"bypass_mainland_china")
|
||||
# Prepare cache db
|
||||
[ -e "$RUN_DIR/cache.db" ] || touch "$RUN_DIR/cache.db"
|
||||
;;
|
||||
"custom")
|
||||
# Prepare ruleset directory
|
||||
[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ "$outbound_node" = "nil" ] || echo > "$RUN_DIR/sing-box-c.log"
|
||||
[ "$outbound_node" = "nil" ] || : > "$RUN_DIR/sing-box-c.log"
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
echo > "$RUN_DIR/sing-box-s.log"
|
||||
: > "$RUN_DIR/sing-box-s.log"
|
||||
mkdir -p "$HP_DIR/certs"
|
||||
fi
|
||||
|
||||
@@ -249,6 +301,7 @@ start_service() {
|
||||
ucode "$HP_DIR/scripts/firewall_pre.uc"
|
||||
[ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft"
|
||||
fw4 reload >"/dev/null" 2>&1
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
|
||||
log "sing-box $(sing-box version -n) started."
|
||||
}
|
||||
@@ -308,8 +361,8 @@ stop_service() {
|
||||
rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR"
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
|
||||
rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.log" \
|
||||
"$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.log"
|
||||
rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-s.json"
|
||||
init_logs
|
||||
|
||||
log "Service stopped."
|
||||
}
|
||||
|
||||
Regular → Executable
+30
@@ -3,6 +3,36 @@
|
||||
[ -f "/www/luci-static/resources/icons/loading.gif" ] && \
|
||||
sed -i "s,/loading.svg,/loading.gif,g" "/www/luci-static/resources/view/homeproxy/status.js"
|
||||
|
||||
uci -q set luci.main.lang='zh_cn'
|
||||
uci -q commit luci
|
||||
|
||||
api_secret="$(uci -q get homeproxy.clash_api.secret)"
|
||||
if [ -z "$api_secret" ] || [ "$api_secret" = "homeproxy" ]; then
|
||||
api_secret="$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>"/dev/null" | head -c 16)"
|
||||
[ -n "$api_secret" ] || api_secret="$(awk 'BEGIN{srand(); printf "%06d", int(rand() * 1000000)}')"
|
||||
uci -q set homeproxy.clash_api.secret="$api_secret"
|
||||
uci -q commit homeproxy
|
||||
fi
|
||||
|
||||
legacy_panel_url="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip"
|
||||
legacy_panel_proxy_url="https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip"
|
||||
old_panel_url="https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
old_panel_proxy_url="https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
default_panel_url="https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
panel_url="$(uci -q get homeproxy.clash_api.external_ui_download_url)"
|
||||
if [ -z "$panel_url" ] || [ "$panel_url" = "$legacy_panel_url" ] || [ "$panel_url" = "$legacy_panel_proxy_url" ] || [ "$panel_url" = "$old_panel_proxy_url" ]; then
|
||||
uci -q set homeproxy.clash_api.external_ui_download_url="$default_panel_url"
|
||||
uci -q commit homeproxy
|
||||
fi
|
||||
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>"/dev/null"
|
||||
|
||||
mkdir -p /var/run/homeproxy
|
||||
touch /var/run/homeproxy/homeproxy.log \
|
||||
/var/run/homeproxy/sing-box-c.log \
|
||||
/var/run/homeproxy/sing-box-s.log
|
||||
chown -R sing-box:sing-box /var/run/homeproxy 2>"/dev/null"
|
||||
|
||||
uci -q batch <<-EOF >"/dev/null"
|
||||
delete firewall.homeproxy_pre
|
||||
|
||||
|
||||
Regular → Executable
File mode changed.
@@ -9,6 +9,7 @@
|
||||
"/var/run/homeproxy/sing-box-s.log": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"rc": [ "*" ],
|
||||
"service": [ "list" ],
|
||||
"luci.homeproxy": [ "*" ]
|
||||
},
|
||||
@@ -16,7 +17,11 @@
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/tmp/homeproxy_certificate.tmp": [ "write" ]
|
||||
"/tmp/homeproxy_certificate.tmp": [ "write" ],
|
||||
"/tmp/homeproxy_panel.tmp": [ "write" ]
|
||||
},
|
||||
"ubus": {
|
||||
"rc": [ "*" ]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,183 @@
|
||||
# luci-app-pingpacket
|
||||
|
||||
`luci-app-pingpacket` 是一个面向 OpenWrt 的 LuCI 状态监控插件,用于持续观察国内直连质量和国外经代理访问质量,并提供统计、日志和快速排障能力。
|
||||
|
||||
## 界面截图
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="50%" align="center">
|
||||
<img src="jpg/1.png" alt="Ping丢包监控总览" width="100%">
|
||||
<br>
|
||||
<sub>监控总览</sub>
|
||||
</td>
|
||||
<td width="50%" align="center">
|
||||
<img src="jpg/2.png" alt="Ping丢包监控设置" width="100%">
|
||||
<br>
|
||||
<sub>监控设置</sub>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" align="center">
|
||||
<img src="jpg/3.png" alt="Ping丢包监控运行日志" width="78%">
|
||||
<br>
|
||||
<sub>运行日志</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## 适用平台
|
||||
|
||||
- OpenWrt 24.10.x,发布 `ipk` 安装包
|
||||
- OpenWrt 25.12.x,发布 `apk` 安装包
|
||||
- 已安装 LuCI Web 管理界面
|
||||
- 设备需具备 `ping`、`curl`、`awk`、`sed`、`grep`、`tail` 等基础命令
|
||||
|
||||
当前仓库的 GitHub Actions 工作流默认构建 `x86/64` SDK 版本;插件本身为 LuCI 页面和 shell 脚本,原则上也可移植到其他支持 LuCI 的 OpenWrt 架构。
|
||||
|
||||
## 页面位置
|
||||
|
||||
后台路径:
|
||||
|
||||
`状态 -> Ping丢包监控`
|
||||
|
||||
子页面:
|
||||
|
||||
- `监控`
|
||||
- `日志`
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 监控页
|
||||
|
||||
- 支持分别配置国内目标和国外测速 URL
|
||||
- 国内目标通过本机 `ping` 进行 ICMP 探测
|
||||
- 国外目标通过 `curl` + `socks5h` 代理进行连续测速,更接近代理实际访问质量
|
||||
- 国外测速仅将 HTTP `200/204` 视为成功
|
||||
- 页面内可直接启用或关闭监控服务
|
||||
- 点击“保存并应用”后会保存配置;如果当前已启用监控,服务会自动重启并按新配置重新开始累计统计
|
||||
- 当检测到“数据停滞”时,页面会直接显示“重新启动服务”按钮
|
||||
- 实时显示开始时间、运行时长、最近更新时间、CPU 占用、内存占用和服务状态
|
||||
|
||||
### 统计卡片
|
||||
|
||||
国内和国外分别独立统计以下数据:
|
||||
|
||||
- 平均延迟
|
||||
- 最低延迟
|
||||
- 最高延迟
|
||||
- 丢包率
|
||||
- 丢包数
|
||||
- 样本数
|
||||
|
||||
统计口径说明:
|
||||
|
||||
- 以上数据均为“本次服务启用后的累计统计值”
|
||||
- 停止服务、重新启用服务或重启服务后,统计会重新开始
|
||||
- 国内和国外分别独立累计,不合并计算
|
||||
|
||||
### 日志页
|
||||
|
||||
- 显示插件运行日志
|
||||
- 顶部支持“自动刷新”开关,默认勾选
|
||||
- 支持“清除日志”按钮
|
||||
- 显示日志最后更新时间、日志行数和日志大小
|
||||
- 默认保留最近 500 行日志
|
||||
|
||||
当前日志会记录:
|
||||
|
||||
- 服务启动
|
||||
- 服务停止
|
||||
- 单次丢包事件
|
||||
- 恢复事件
|
||||
- 国外代理测速失败原因
|
||||
|
||||
日志规则:
|
||||
|
||||
- 只要单次探测失败,就立即记录一条日志
|
||||
- 连续失败时,每次失败都会单独记录
|
||||
- 故障后的首次成功响应会记录恢复日志
|
||||
|
||||
## 当前探测方式
|
||||
|
||||
### 国内
|
||||
|
||||
- 目标类型:IP 或域名
|
||||
- 探测方式:ICMP `ping`
|
||||
- 默认目标:`www.baidu.com`
|
||||
- 默认 Ping 间隔:`1` 秒
|
||||
|
||||
### 国外
|
||||
|
||||
- 目标类型:域名或完整 URL
|
||||
- 探测方式:通过 `socks5h` 代理发起 `curl` 请求
|
||||
- 默认测速 URL:`https://www.google.com/generate_204`
|
||||
- 默认代理端口:`7891`
|
||||
- 默认测速间隔:`5` 秒
|
||||
- 代理主机固定为 OpenWrt 本机 `127.0.0.1`
|
||||
|
||||
## 配置规则
|
||||
|
||||
- 国内和国外目标可以只填其中一个
|
||||
- 两个目标都为空时,不能“保存并应用”
|
||||
- 两个目标都为空时,也不能通过开关启用服务
|
||||
- 若填写国外测速 URL,则必须同时填写代理端口
|
||||
- 代理端口必须为 `1-65535` 的数字
|
||||
- 外国测速 URL 未填写协议时,系统会自动按 `https://` 处理
|
||||
- 页面轮询刷新时,不会覆盖用户正在编辑但尚未保存的输入内容
|
||||
|
||||
## 默认配置
|
||||
|
||||
安装或补齐配置时,默认值如下:
|
||||
|
||||
- 国内目标:`www.baidu.com`
|
||||
- 国外测速 URL:`https://www.google.com/generate_204`
|
||||
- 国外代理类型:`socks5h`
|
||||
- 国外代理端口:`7891`
|
||||
- 国内 Ping 间隔:`1`
|
||||
- 国外测速间隔:`5`
|
||||
|
||||
默认 UCI 配置示例:
|
||||
|
||||
```uci
|
||||
config pingpacket 'config'
|
||||
option enabled '0'
|
||||
option domestic_target 'www.baidu.com'
|
||||
option foreign_target 'https://www.google.com/generate_204'
|
||||
option foreign_proxy_type 'socks5h'
|
||||
option foreign_proxy_host '127.0.0.1'
|
||||
option foreign_proxy_port '7891'
|
||||
option domestic_interval '1'
|
||||
option foreign_interval '5'
|
||||
```
|
||||
|
||||
## 依赖
|
||||
|
||||
- `luci-base`
|
||||
- `luci-lua-runtime`
|
||||
- `luci-compat`
|
||||
- `curl`
|
||||
|
||||
## 安装
|
||||
|
||||
### OpenWrt 24.10.x
|
||||
|
||||
上传 `ipk` 安装包后执行:
|
||||
|
||||
```sh
|
||||
opkg install luci-app-pingpacket_v1.0.22_all.ipk
|
||||
```
|
||||
|
||||
### OpenWrt 25.12.x
|
||||
|
||||
上传 `apk` 安装包后执行:
|
||||
|
||||
```sh
|
||||
apk add --allow-untrusted ./luci-app-pingpacket_v1.0.22_all.apk
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 升级时会尽量保留已有 `/etc/config/pingpacket`
|
||||
- 若系统中存在历史遗留的 `/etc/config/pingpacket-opkg`,安装脚本会尽量兼容处理
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 142 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -0,0 +1,55 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI web interface for Pingpacket monitoring
|
||||
LUCI_DEPENDS:=+luci-base +luci-lua-runtime +luci-compat +curl
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_NAME:=luci-app-pingpacket
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=20
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/controller
|
||||
$(INSTALL_DATA) ./luasrc/controller/pingpacket.lua $(1)/usr/lib/lua/luci/controller/pingpacket.lua
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/view/pingpacket
|
||||
$(INSTALL_DATA) ./luasrc/view/pingpacket/status.htm $(1)/usr/lib/lua/luci/view/pingpacket/status.htm
|
||||
$(INSTALL_DATA) ./luasrc/view/pingpacket/logs.htm $(1)/usr/lib/lua/luci/view/pingpacket/logs.htm
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./root/etc/config/pingpacket $(1)/etc/config/pingpacket
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./root/etc/init.d/pingpacket $(1)/etc/init.d/pingpacket
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) ./root/usr/bin/pingpacket.sh $(1)/usr/bin/pingpacket.sh
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-pingpacket.json \
|
||||
$(1)/usr/share/rpcd/acl.d/luci-app-pingpacket.json
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
rm -f /tmp/luci-indexcache.*
|
||||
rm -rf /tmp/luci-modulecache/
|
||||
/etc/init.d/rpcd reload 2>/dev/null
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] && exit 0
|
||||
[ -f /etc/init.d/pingpacket ] && {
|
||||
/etc/init.d/pingpacket disable 2>/dev/null
|
||||
/etc/init.d/pingpacket stop 2>/dev/null
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,518 @@
|
||||
module("luci.controller.pingpacket", package.seeall)
|
||||
|
||||
local LOG_FILE = "/tmp/pingpacket.log"
|
||||
local CPU_STATE_FILE = "/tmp/pingpacket_cpu_state"
|
||||
local DEFAULT_PROXY_HOST = "127.0.0.1"
|
||||
local function trim(value)
|
||||
return (value or ""):gsub("%c", ""):match("^%s*(.-)%s*$") or ""
|
||||
end
|
||||
|
||||
local function normalize_enabled(value)
|
||||
return value == "1" and "1" or "0"
|
||||
end
|
||||
|
||||
local function normalize_proxy_type(value)
|
||||
return "socks5h"
|
||||
end
|
||||
|
||||
local function normalize_proxy_port(value)
|
||||
value = trim(value)
|
||||
|
||||
if value == "" then
|
||||
return ""
|
||||
end
|
||||
|
||||
if not value:match("^%d+$") then
|
||||
return nil
|
||||
end
|
||||
|
||||
local port = tonumber(value)
|
||||
if not port or port < 1 or port > 65535 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return tostring(port)
|
||||
end
|
||||
|
||||
local function normalize_proxy_host(value)
|
||||
value = trim(value)
|
||||
|
||||
if value ~= "" then
|
||||
return value
|
||||
end
|
||||
|
||||
return DEFAULT_PROXY_HOST
|
||||
end
|
||||
|
||||
local function normalize_probe_interval(value)
|
||||
value = trim(value)
|
||||
|
||||
if value == "" then
|
||||
return "1"
|
||||
end
|
||||
|
||||
if not value:match("^%d+$") then
|
||||
return nil
|
||||
end
|
||||
|
||||
local interval = tonumber(value)
|
||||
if not interval or interval < 1 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return tostring(interval)
|
||||
end
|
||||
|
||||
local function normalize_foreign_probe_interval(value)
|
||||
value = trim(value)
|
||||
|
||||
if value == "" then
|
||||
return "5"
|
||||
end
|
||||
|
||||
return normalize_probe_interval(value)
|
||||
end
|
||||
|
||||
local function write_json(payload)
|
||||
local http = require("luci.http")
|
||||
local jsonc = require("luci.jsonc")
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write(jsonc.stringify(payload))
|
||||
end
|
||||
|
||||
local function read_start_time()
|
||||
local start_time_file = io.open("/var/run/pingpacket/start_time", "r")
|
||||
if not start_time_file then
|
||||
return ""
|
||||
end
|
||||
|
||||
local value = trim(start_time_file:read("*a"))
|
||||
start_time_file:close()
|
||||
return value
|
||||
end
|
||||
|
||||
local function should_require_target(request_source, enabled)
|
||||
if request_source == "save" then
|
||||
return true
|
||||
end
|
||||
|
||||
return enabled == "1"
|
||||
end
|
||||
|
||||
local function has_any_target(uci)
|
||||
local domestic_target = trim(uci:get("pingpacket", "config", "domestic_target"))
|
||||
local foreign_target = trim(uci:get("pingpacket", "config", "foreign_target"))
|
||||
|
||||
return domestic_target ~= "" or foreign_target ~= ""
|
||||
end
|
||||
|
||||
local function has_valid_foreign_proxy(uci)
|
||||
local foreign_target = trim(uci:get("pingpacket", "config", "foreign_target"))
|
||||
local proxy_port = trim(uci:get("pingpacket", "config", "foreign_proxy_port"))
|
||||
|
||||
if foreign_target == "" then
|
||||
return true
|
||||
end
|
||||
|
||||
return proxy_port:match("^%d+$") and tonumber(proxy_port) and tonumber(proxy_port) >= 1 and tonumber(proxy_port) <= 65535
|
||||
end
|
||||
|
||||
local function read_file_content(path)
|
||||
local file = io.open(path, "r")
|
||||
if not file then
|
||||
return ""
|
||||
end
|
||||
|
||||
local content = file:read("*a") or ""
|
||||
file:close()
|
||||
return content
|
||||
end
|
||||
|
||||
local function read_log_payload()
|
||||
local nixio_fs = require("nixio.fs")
|
||||
local stat = nixio_fs.stat(LOG_FILE)
|
||||
|
||||
return {
|
||||
content = read_file_content(LOG_FILE),
|
||||
updated_at = (stat and stat.mtime) and os.date("%Y-%m-%d %H:%M:%S", stat.mtime) or "",
|
||||
size = (stat and stat.size) or 0
|
||||
}
|
||||
end
|
||||
|
||||
local function read_cpu_sample()
|
||||
local file = io.open("/proc/stat", "r")
|
||||
if not file then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local line = file:read("*l") or ""
|
||||
file:close()
|
||||
|
||||
local total = 0
|
||||
local idle = 0
|
||||
local index = 0
|
||||
|
||||
for token in line:gmatch("%S+") do
|
||||
if token ~= "cpu" then
|
||||
index = index + 1
|
||||
if index > 8 then
|
||||
break
|
||||
end
|
||||
|
||||
local value = tonumber(token) or 0
|
||||
total = total + value
|
||||
|
||||
if index == 4 or index == 5 then
|
||||
idle = idle + value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if total <= 0 then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
return total, idle
|
||||
end
|
||||
|
||||
local function read_saved_cpu_sample()
|
||||
local content = trim(read_file_content(CPU_STATE_FILE))
|
||||
local total, idle, timestamp = content:match("^(%d+)%s+(%d+)%s*(%d*)$")
|
||||
|
||||
if not total or not idle then
|
||||
return nil, nil, nil
|
||||
end
|
||||
|
||||
timestamp = tonumber(timestamp)
|
||||
|
||||
return tonumber(total), tonumber(idle), timestamp
|
||||
end
|
||||
|
||||
local function write_cpu_sample(total, idle)
|
||||
local file = io.open(CPU_STATE_FILE, "w")
|
||||
if not file then
|
||||
return
|
||||
end
|
||||
|
||||
file:write(string.format("%d %d %d\n", total, idle, os.time()))
|
||||
file:close()
|
||||
end
|
||||
|
||||
local function format_percent(value)
|
||||
value = tonumber(value) or 0
|
||||
|
||||
if value < 0 then
|
||||
value = 0
|
||||
elseif value > 100 then
|
||||
value = 100
|
||||
end
|
||||
|
||||
return string.format("%.1f", value)
|
||||
end
|
||||
|
||||
local function read_memory_usage()
|
||||
local file = io.open("/proc/meminfo", "r")
|
||||
if not file then
|
||||
return "0.0"
|
||||
end
|
||||
|
||||
local mem_total
|
||||
local mem_available
|
||||
local mem_free = 0
|
||||
local buffers = 0
|
||||
local cached = 0
|
||||
|
||||
for line in file:lines() do
|
||||
local key, value = line:match("^(%w+):%s+(%d+)")
|
||||
value = tonumber(value)
|
||||
|
||||
if key == "MemTotal" then
|
||||
mem_total = value
|
||||
elseif key == "MemAvailable" then
|
||||
mem_available = value
|
||||
elseif key == "MemFree" then
|
||||
mem_free = value or 0
|
||||
elseif key == "Buffers" then
|
||||
buffers = value or 0
|
||||
elseif key == "Cached" then
|
||||
cached = value or 0
|
||||
end
|
||||
end
|
||||
|
||||
file:close()
|
||||
|
||||
if not mem_total or mem_total <= 0 then
|
||||
return "0.0"
|
||||
end
|
||||
|
||||
if not mem_available then
|
||||
mem_available = mem_free + buffers + cached
|
||||
end
|
||||
|
||||
return format_percent(((mem_total - mem_available) * 100) / mem_total)
|
||||
end
|
||||
|
||||
local function read_system_metrics()
|
||||
local current_total, current_idle = read_cpu_sample()
|
||||
local previous_total, previous_idle, previous_timestamp = read_saved_cpu_sample()
|
||||
local cpu_usage = "0.0"
|
||||
|
||||
if current_total and current_idle then
|
||||
if not previous_timestamp or (os.time() - previous_timestamp) > 10 then
|
||||
previous_total = nil
|
||||
previous_idle = nil
|
||||
end
|
||||
|
||||
if previous_total and previous_idle and current_total > previous_total then
|
||||
local total_delta = current_total - previous_total
|
||||
local idle_delta = current_idle - previous_idle
|
||||
|
||||
if total_delta > 0 then
|
||||
cpu_usage = format_percent(((total_delta - idle_delta) * 100) / total_delta)
|
||||
end
|
||||
end
|
||||
|
||||
write_cpu_sample(current_total, current_idle)
|
||||
end
|
||||
|
||||
return {
|
||||
cpu_usage = cpu_usage,
|
||||
memory_usage = read_memory_usage()
|
||||
}
|
||||
end
|
||||
|
||||
local function validate_config(request_source, enabled, domestic, foreign, proxy_port)
|
||||
if should_require_target(request_source, enabled) and domestic == "" and foreign == "" then
|
||||
return false, "请至少填写一个监控目标。"
|
||||
end
|
||||
|
||||
if foreign ~= "" and proxy_port == "" then
|
||||
return false, "已填写国外目标时,请同时填写代理端口。"
|
||||
end
|
||||
|
||||
return true, ""
|
||||
end
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/pingpacket") then
|
||||
return
|
||||
end
|
||||
|
||||
local page = entry(
|
||||
{"admin", "status", "pingpacket"},
|
||||
alias("admin", "status", "pingpacket", "monitor"),
|
||||
"Ping丢包监控",
|
||||
40
|
||||
)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-pingpacket" }
|
||||
|
||||
entry({"admin", "status", "pingpacket", "monitor"}, template("pingpacket/status"), "监控", 1).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "logs"}, template("pingpacket/logs"), "日志", 2).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "status"}, alias("admin", "status", "pingpacket", "monitor")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "get_data"}, call("action_get_data")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "get_logs"}, call("action_get_logs")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "clear_logs"}, call("action_clear_logs")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "save_config"}, call("action_save_config")).leaf = true
|
||||
entry({"admin", "status", "pingpacket", "restart_service"}, call("action_restart_service")).leaf = true
|
||||
end
|
||||
|
||||
function action_get_data()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local sys = require("luci.sys")
|
||||
local jsonc = require("luci.jsonc")
|
||||
|
||||
local enabled = normalize_enabled(uci:get("pingpacket", "config", "enabled"))
|
||||
local domestic_target = trim(uci:get("pingpacket", "config", "domestic_target"))
|
||||
local foreign_target = trim(uci:get("pingpacket", "config", "foreign_target"))
|
||||
local foreign_proxy_type = normalize_proxy_type(uci:get("pingpacket", "config", "foreign_proxy_type"))
|
||||
local foreign_proxy_host = normalize_proxy_host(uci:get("pingpacket", "config", "foreign_proxy_host"))
|
||||
local foreign_proxy_port = trim(uci:get("pingpacket", "config", "foreign_proxy_port"))
|
||||
local domestic_interval = normalize_probe_interval(uci:get("pingpacket", "config", "domestic_interval")) or "1"
|
||||
local foreign_interval = normalize_foreign_probe_interval(uci:get("pingpacket", "config", "foreign_interval")) or "5"
|
||||
|
||||
local result = {
|
||||
enabled = enabled,
|
||||
domestic_target = domestic_target,
|
||||
foreign_target = foreign_target,
|
||||
foreign_proxy_type = foreign_proxy_type,
|
||||
foreign_proxy_host = foreign_proxy_host,
|
||||
foreign_proxy_port = foreign_proxy_port,
|
||||
domestic_interval = domestic_interval,
|
||||
foreign_interval = foreign_interval,
|
||||
server_time = os.date("%Y-%m-%d %H:%M:%S"),
|
||||
start_time = read_start_time(),
|
||||
updated_at = "",
|
||||
service_running = (sys.call("[ -s /var/run/pingpacket/start_time ]") == 0),
|
||||
system = read_system_metrics(),
|
||||
domestic = {
|
||||
avg = "0.0",
|
||||
min = "0.0",
|
||||
max = "0.0",
|
||||
loss_rate = "0.0",
|
||||
loss_count = 0,
|
||||
count = 0,
|
||||
samples = 0
|
||||
},
|
||||
foreign = {
|
||||
avg = "0.0",
|
||||
min = "0.0",
|
||||
max = "0.0",
|
||||
loss_rate = "0.0",
|
||||
loss_count = 0,
|
||||
count = 0,
|
||||
samples = 0
|
||||
}
|
||||
}
|
||||
|
||||
local status_file = io.open("/tmp/pingpacket_status.json", "r")
|
||||
if status_file then
|
||||
local content = status_file:read("*a")
|
||||
status_file:close()
|
||||
|
||||
local ok, data = pcall(jsonc.parse, content)
|
||||
if ok and data then
|
||||
if data.start_time then
|
||||
result.start_time = data.start_time
|
||||
end
|
||||
if data.updated_at then
|
||||
result.updated_at = data.updated_at
|
||||
end
|
||||
if data.domestic then
|
||||
result.domestic = data.domestic
|
||||
end
|
||||
if data.foreign then
|
||||
result.foreign = data.foreign
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
write_json(result)
|
||||
end
|
||||
|
||||
function action_get_logs()
|
||||
local payload = read_log_payload()
|
||||
payload.server_time = os.date("%Y-%m-%d %H:%M:%S")
|
||||
write_json(payload)
|
||||
end
|
||||
|
||||
function action_clear_logs()
|
||||
local nixio_fs = require("nixio.fs")
|
||||
nixio_fs.remove(LOG_FILE)
|
||||
|
||||
write_json({
|
||||
success = true,
|
||||
message = ""
|
||||
})
|
||||
end
|
||||
|
||||
function action_save_config()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local sys = require("luci.sys")
|
||||
local http = require("luci.http")
|
||||
|
||||
local enabled = normalize_enabled(http.formvalue("enabled"))
|
||||
local domestic = trim(http.formvalue("domestic_target"))
|
||||
local foreign = trim(http.formvalue("foreign_target"))
|
||||
local proxy_type = normalize_proxy_type(http.formvalue("foreign_proxy_type"))
|
||||
local proxy_host = DEFAULT_PROXY_HOST
|
||||
local proxy_port = normalize_proxy_port(http.formvalue("foreign_proxy_port"))
|
||||
local domestic_interval = normalize_probe_interval(http.formvalue("domestic_interval"))
|
||||
local foreign_interval = normalize_foreign_probe_interval(http.formvalue("foreign_interval"))
|
||||
local request_source = trim(http.formvalue("request_source"))
|
||||
|
||||
if proxy_port == nil then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "代理端口必须是 1 到 65535 之间的数字。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if domestic_interval == nil then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "国内 Ping间隔必须是大于 0 的整数秒。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if foreign_interval == nil then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "国外 Ping间隔必须是大于 0 的整数秒。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local valid, message = validate_config(request_source, enabled, domestic, foreign, proxy_port)
|
||||
if not valid then
|
||||
write_json({
|
||||
success = false,
|
||||
message = message
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
uci:set("pingpacket", "config", "enabled", enabled)
|
||||
uci:set("pingpacket", "config", "domestic_target", domestic)
|
||||
uci:set("pingpacket", "config", "foreign_target", foreign)
|
||||
uci:set("pingpacket", "config", "foreign_proxy_type", proxy_type)
|
||||
uci:set("pingpacket", "config", "foreign_proxy_host", proxy_host)
|
||||
uci:set("pingpacket", "config", "foreign_proxy_port", proxy_port)
|
||||
uci:set("pingpacket", "config", "domestic_interval", domestic_interval)
|
||||
uci:set("pingpacket", "config", "foreign_interval", foreign_interval)
|
||||
uci:commit("pingpacket")
|
||||
|
||||
sys.call("rm -f /tmp/luci-indexcache*")
|
||||
|
||||
local rc
|
||||
if enabled == "1" then
|
||||
rc = sys.call("/bin/sh /etc/init.d/pingpacket restart >/dev/null 2>&1")
|
||||
else
|
||||
rc = sys.call("/bin/sh /etc/init.d/pingpacket stop >/dev/null 2>&1")
|
||||
end
|
||||
|
||||
write_json({
|
||||
success = (rc == 0),
|
||||
message = (rc == 0) and "" or "服务状态更新失败。"
|
||||
})
|
||||
end
|
||||
|
||||
function action_restart_service()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local sys = require("luci.sys")
|
||||
|
||||
local enabled = normalize_enabled(uci:get("pingpacket", "config", "enabled"))
|
||||
|
||||
if enabled ~= "1" then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "当前监控未启用。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if not has_any_target(uci) then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "请至少填写一个监控目标。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
if not has_valid_foreign_proxy(uci) then
|
||||
write_json({
|
||||
success = false,
|
||||
message = "国外目标已填写,但代理端口无效。"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local rc = sys.call("/bin/sh /etc/init.d/pingpacket restart >/dev/null 2>&1")
|
||||
write_json({
|
||||
success = (rc == 0),
|
||||
message = (rc == 0) and "" or "重新启动监控服务失败。"
|
||||
})
|
||||
end
|
||||
@@ -0,0 +1,477 @@
|
||||
<%+header%>
|
||||
|
||||
<style>
|
||||
.pp-log-page {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 10px 20px;
|
||||
}
|
||||
|
||||
.pp-log-hero {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 24px 26px;
|
||||
margin: 12px 0 18px;
|
||||
border-radius: 18px;
|
||||
color: #fff;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 255, 255, 0.2), transparent 34%),
|
||||
linear-gradient(135deg, #10615b, #0f766e 60%, #164e63);
|
||||
box-shadow: 0 18px 38px rgba(15, 118, 110, 0.18);
|
||||
}
|
||||
|
||||
.pp-log-copy {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.pp-log-kicker {
|
||||
margin: 0 0 8px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.78;
|
||||
}
|
||||
|
||||
.pp-log-title {
|
||||
margin: 0;
|
||||
font-size: 30px;
|
||||
line-height: 1.15;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pp-log-subtitle {
|
||||
margin: 10px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.65;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
.pp-log-summary {
|
||||
min-width: 250px;
|
||||
padding: 16px 18px;
|
||||
border-radius: 16px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
backdrop-filter: blur(6px);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.pp-log-summary-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.pp-log-summary-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: "JetBrains Mono", "Consolas", "Menlo", monospace;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.pp-log-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.pp-log-meta,
|
||||
.pp-log-panel {
|
||||
background: linear-gradient(180deg, #ffffff, #fbfdff);
|
||||
border: 1px solid #dde8f6;
|
||||
box-shadow: 0 10px 28px rgba(27, 73, 124, 0.08);
|
||||
}
|
||||
|
||||
.pp-log-meta {
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
}
|
||||
|
||||
.pp-log-meta-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
color: #6d7d92;
|
||||
}
|
||||
|
||||
.pp-log-meta-value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #24364d;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: "JetBrains Mono", "Consolas", "Menlo", monospace;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.pp-log-panel {
|
||||
padding: 22px;
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.pp-log-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.pp-log-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pp-log-refresh {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #344a63;
|
||||
}
|
||||
|
||||
.pp-log-refresh input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.pp-log-note {
|
||||
font-size: 13px;
|
||||
color: #6f8198;
|
||||
}
|
||||
|
||||
.pp-log-button {
|
||||
height: 40px;
|
||||
padding: 0 16px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #a34717;
|
||||
background: linear-gradient(135deg, #ffe0c7, #ffd2b0);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 10px 24px rgba(240, 123, 45, 0.14);
|
||||
transition: transform 0.16s ease, box-shadow 0.16s ease, opacity 0.16s ease;
|
||||
}
|
||||
|
||||
.pp-log-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 12px 28px rgba(240, 123, 45, 0.18);
|
||||
}
|
||||
|
||||
.pp-log-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.pp-log-message {
|
||||
min-height: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pp-log-message.success {
|
||||
color: #1f8f52;
|
||||
}
|
||||
|
||||
.pp-log-message.error {
|
||||
color: #d13d38;
|
||||
}
|
||||
|
||||
.pp-log-output {
|
||||
min-height: 420px;
|
||||
max-height: 620px;
|
||||
margin: 0;
|
||||
padding: 18px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #dce7f5;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0)),
|
||||
#102132;
|
||||
color: #d5e5f6;
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
font-family: "JetBrains Mono", "Consolas", "Menlo", monospace;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.pp-log-hero,
|
||||
.pp-log-toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.pp-log-summary {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.pp-log-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.pp-log-page {
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.pp-log-hero,
|
||||
.pp-log-panel {
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="pp-log-page">
|
||||
<div class="pp-log-hero">
|
||||
<div class="pp-log-copy">
|
||||
<div class="pp-log-kicker">OpenWrt Status</div>
|
||||
<h2 class="pp-log-title">运行日志</h2>
|
||||
<p class="pp-log-subtitle">这里会持续记录监控服务的启动、停止、单次丢包、恢复事件,以及国外代理 curl 探测失败时的原因,便于快速排查最近一次异常发生的时间和内容。</p>
|
||||
</div>
|
||||
|
||||
<div class="pp-log-summary">
|
||||
<span class="pp-log-summary-label">日志说明</span>
|
||||
<div class="pp-log-summary-value">默认保留最近 500 行事件记录,适合排查服务重启、连续失败、单次丢包、代理配置问题和恢复过程。</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pp-log-grid">
|
||||
<div class="pp-log-meta">
|
||||
<span class="pp-log-meta-label">最后更新时间</span>
|
||||
<div class="pp-log-meta-value" id="logUpdatedAt">--</div>
|
||||
</div>
|
||||
<div class="pp-log-meta">
|
||||
<span class="pp-log-meta-label">日志行数</span>
|
||||
<div class="pp-log-meta-value" id="logLineCount">0</div>
|
||||
</div>
|
||||
<div class="pp-log-meta">
|
||||
<span class="pp-log-meta-label">日志大小</span>
|
||||
<div class="pp-log-meta-value" id="logSize">0 B</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pp-log-panel">
|
||||
<div class="pp-log-toolbar">
|
||||
<div class="pp-log-tools">
|
||||
<label class="pp-log-refresh">
|
||||
<input type="checkbox" id="autoRefresh" checked>
|
||||
<span>自动刷新</span>
|
||||
</label>
|
||||
<button class="pp-log-button" id="clearLogsButton" type="button">清除日志</button>
|
||||
<span class="pp-log-message" id="logMessage"></span>
|
||||
</div>
|
||||
<div class="pp-log-note">取消勾选后会停止自动轮询,当前页面内容会保持不变。</div>
|
||||
</div>
|
||||
|
||||
<pre class="pp-log-output" id="logContent">正在加载日志...</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
var apiBase = '<%=luci.dispatcher.build_url("admin", "status", "pingpacket")%>';
|
||||
var token = (window.L && L.env && L.env.token) ? L.env.token : '<%=token%>';
|
||||
var logsUrl = apiBase + '/get_logs';
|
||||
var clearUrl = apiBase + '/clear_logs';
|
||||
var refreshTimer = null;
|
||||
var fetchInFlight = false;
|
||||
var clearInFlight = false;
|
||||
var refreshInterval = 2000;
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function setMessage(text, type) {
|
||||
var node = $('logMessage');
|
||||
node.textContent = text || '';
|
||||
node.className = type ? 'pp-log-message ' + type : 'pp-log-message';
|
||||
}
|
||||
|
||||
function scheduleRefresh(delay) {
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
}
|
||||
|
||||
if (!$('autoRefresh').checked) {
|
||||
refreshTimer = null;
|
||||
return;
|
||||
}
|
||||
|
||||
refreshTimer = setTimeout(fetchLogs, typeof delay === 'number' ? delay : refreshInterval);
|
||||
}
|
||||
|
||||
function formatBytes(size) {
|
||||
var value = Number(size) || 0;
|
||||
if (value < 1024) {
|
||||
return value + ' B';
|
||||
}
|
||||
if (value < 1024 * 1024) {
|
||||
return (value / 1024).toFixed(1) + ' KB';
|
||||
}
|
||||
return (value / (1024 * 1024)).toFixed(1) + ' MB';
|
||||
}
|
||||
|
||||
function countLines(content) {
|
||||
var normalized;
|
||||
|
||||
if (!content) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
normalized = content.replace(/\n+$/, '');
|
||||
if (!normalized) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return normalized.split('\n').length;
|
||||
}
|
||||
|
||||
function updateView(payload) {
|
||||
var content = payload.content || '';
|
||||
var output = $('logContent');
|
||||
|
||||
output.textContent = content || '当前暂无日志记录。';
|
||||
$('logUpdatedAt').textContent = payload.updated_at || '--';
|
||||
$('logLineCount').textContent = String(countLines(content));
|
||||
$('logSize').textContent = formatBytes(payload.size || 0);
|
||||
if ($('autoRefresh').checked) {
|
||||
output.scrollTop = output.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
function fetchLogs() {
|
||||
var xhr;
|
||||
|
||||
if (fetchInFlight || clearInFlight) {
|
||||
scheduleRefresh(refreshInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
fetchInFlight = true;
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', logsUrl, true);
|
||||
xhr.onload = function() {
|
||||
var response = null;
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
response = JSON.parse(xhr.responseText);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (response) {
|
||||
updateView(response);
|
||||
} else {
|
||||
setMessage('日志读取失败,请稍后重试。', 'error');
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
setMessage('日志读取失败,请稍后重试。', 'error');
|
||||
};
|
||||
xhr.onloadend = function() {
|
||||
fetchInFlight = false;
|
||||
scheduleRefresh(refreshInterval);
|
||||
};
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
var xhr;
|
||||
|
||||
if (fetchInFlight || clearInFlight) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInFlight = true;
|
||||
$('clearLogsButton').disabled = true;
|
||||
setMessage('正在清除日志...', '');
|
||||
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', clearUrl, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
|
||||
xhr.onload = function() {
|
||||
var response = null;
|
||||
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
response = JSON.parse(xhr.responseText);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (response && response.success) {
|
||||
updateView({
|
||||
content: '',
|
||||
updated_at: '',
|
||||
size: 0
|
||||
});
|
||||
setMessage('日志已清除。', 'success');
|
||||
} else {
|
||||
setMessage('清除日志失败,请稍后重试。', 'error');
|
||||
}
|
||||
};
|
||||
xhr.onerror = function() {
|
||||
setMessage('清除日志失败,请稍后重试。', 'error');
|
||||
};
|
||||
xhr.onloadend = function() {
|
||||
clearInFlight = false;
|
||||
$('clearLogsButton').disabled = false;
|
||||
if ($('autoRefresh').checked) {
|
||||
scheduleRefresh(refreshInterval);
|
||||
}
|
||||
};
|
||||
xhr.send('token=' + encodeURIComponent(token));
|
||||
}
|
||||
|
||||
$('autoRefresh').addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
setMessage('已恢复自动刷新。', 'success');
|
||||
fetchLogs();
|
||||
return;
|
||||
}
|
||||
|
||||
if (refreshTimer) {
|
||||
clearTimeout(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
|
||||
setMessage('已暂停自动刷新。', '');
|
||||
});
|
||||
|
||||
$('clearLogsButton').addEventListener('click', clearLogs);
|
||||
|
||||
document.addEventListener('visibilitychange', function() {
|
||||
if (!document.hidden && $('autoRefresh').checked) {
|
||||
fetchLogs();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('focus', function() {
|
||||
if ($('autoRefresh').checked) {
|
||||
fetchLogs();
|
||||
}
|
||||
});
|
||||
|
||||
fetchLogs();
|
||||
})();
|
||||
</script>
|
||||
|
||||
<%+footer%>
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,81 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
|
||||
msgid "Ping Packet Loss Monitor"
|
||||
msgstr "Ping丢包监控"
|
||||
|
||||
msgid "Disabled"
|
||||
msgstr "已关闭"
|
||||
|
||||
msgid "Enabled"
|
||||
msgstr "已启用"
|
||||
|
||||
msgid "Start Time"
|
||||
msgstr "开始时间"
|
||||
|
||||
msgid "Duration"
|
||||
msgstr "运行时长"
|
||||
|
||||
msgid "Domestic"
|
||||
msgstr "国内"
|
||||
|
||||
msgid "Foreign"
|
||||
msgstr "国外"
|
||||
|
||||
msgid "Average"
|
||||
msgstr "平均延迟"
|
||||
|
||||
msgid "Minimum"
|
||||
msgstr "最低延迟"
|
||||
|
||||
msgid "Maximum"
|
||||
msgstr "最高延迟"
|
||||
|
||||
msgid "Packet Loss"
|
||||
msgstr "丢包率"
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "未配置"
|
||||
|
||||
msgid "Monitoring Settings"
|
||||
msgstr "监控设置"
|
||||
|
||||
msgid "Ping Domestic IP or Domain"
|
||||
msgstr "Ping 国内 IP 或域名"
|
||||
|
||||
msgid "e.g. 114.114.114.114 or baidu.com"
|
||||
msgstr "例如:114.114.114.114 或 baidu.com"
|
||||
|
||||
msgid "Leave empty to disable domestic monitoring."
|
||||
msgstr "留空则不启用国内监控。"
|
||||
|
||||
msgid "Ping Foreign IP or Domain"
|
||||
msgstr "Ping 国外 IP 或域名"
|
||||
|
||||
msgid "e.g. 8.8.8.8 or google.com"
|
||||
msgstr "例如:8.8.8.8 或 google.com"
|
||||
|
||||
msgid "Leave empty to disable foreign monitoring."
|
||||
msgstr "留空则不启用国外监控。"
|
||||
|
||||
msgid "Save & Apply"
|
||||
msgstr "保存并应用"
|
||||
|
||||
msgid "Saved successfully."
|
||||
msgstr "保存成功。"
|
||||
|
||||
msgid "Save failed."
|
||||
msgstr "保存失败。"
|
||||
|
||||
msgid "Ping Packet Loss"
|
||||
msgstr "Ping丢包监控"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "Please configure at least one target."
|
||||
msgstr "请至少配置一个目标。"
|
||||
|
||||
msgid "Failed to update service state."
|
||||
msgstr "服务状态更新失败。"
|
||||
@@ -0,0 +1,9 @@
|
||||
config pingpacket 'config'
|
||||
option enabled '0'
|
||||
option domestic_target 'www.baidu.com'
|
||||
option foreign_target 'https://www.google.com/generate_204'
|
||||
option foreign_proxy_type 'socks5h'
|
||||
option foreign_proxy_host '127.0.0.1'
|
||||
option foreign_proxy_port '7891'
|
||||
option domestic_interval '1'
|
||||
option foreign_interval '5'
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=90
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/bin/pingpacket.sh
|
||||
RUN_DIR=/var/run/pingpacket
|
||||
STATUS_FILE=/tmp/pingpacket_status.json
|
||||
CONFIG_FILE=$RUN_DIR/config
|
||||
LOG_FILE=/tmp/pingpacket.log
|
||||
LOG_MAX_LINES=500
|
||||
|
||||
log_rotate() {
|
||||
[ -f "$LOG_FILE" ] || return 0
|
||||
tail -n "$LOG_MAX_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp" 2>/dev/null && \
|
||||
mv -f "${LOG_FILE}.tmp" "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_event() {
|
||||
local event_type="$1"
|
||||
local message="$2"
|
||||
local timestamp
|
||||
|
||||
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
printf '[%s] [%s] %s\n' "$timestamp" "$event_type" "$message" >> "$LOG_FILE"
|
||||
log_rotate
|
||||
}
|
||||
|
||||
format_foreign_summary() {
|
||||
local target="$1"
|
||||
local proxy_type="$2"
|
||||
local proxy_host="$3"
|
||||
local proxy_port="$4"
|
||||
|
||||
[ -n "$target" ] || {
|
||||
echo "未配置"
|
||||
return
|
||||
}
|
||||
|
||||
if [ -n "$proxy_port" ]; then
|
||||
echo "${target}(代理 ${proxy_type} 本机端口 ${proxy_port})"
|
||||
else
|
||||
echo "${target}(代理端口未配置)"
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
local enabled domestic foreign proxy_type proxy_host proxy_port domestic_interval foreign_interval foreign_summary
|
||||
|
||||
config_load pingpacket
|
||||
config_get enabled config enabled "0"
|
||||
config_get domestic config domestic_target ""
|
||||
config_get foreign config foreign_target ""
|
||||
config_get proxy_type config foreign_proxy_type "socks5h"
|
||||
config_get proxy_host config foreign_proxy_host "127.0.0.1"
|
||||
config_get proxy_port config foreign_proxy_port "7891"
|
||||
config_get domestic_interval config domestic_interval "1"
|
||||
config_get foreign_interval config foreign_interval "5"
|
||||
proxy_type="socks5h"
|
||||
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
[ -n "$domestic" ] || [ -n "$foreign" ] || return 0
|
||||
|
||||
rm -rf "$RUN_DIR"
|
||||
rm -f "$STATUS_FILE"
|
||||
mkdir -p "$RUN_DIR"
|
||||
printf '%s\n' "$domestic" > "$CONFIG_FILE"
|
||||
printf '%s\n' "$foreign" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "$proxy_type" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "${proxy_host:-127.0.0.1}" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "$proxy_port" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "$domestic_interval" >> "$CONFIG_FILE"
|
||||
printf '%s\n' "$foreign_interval" >> "$CONFIG_FILE"
|
||||
|
||||
foreign_summary="$(format_foreign_summary "$foreign" "$proxy_type" "$proxy_host" "$proxy_port")"
|
||||
log_event "启动" "监控服务已启动,国内目标:${domestic:-未配置},国外目标:${foreign_summary}"
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command /bin/sh "$PROG"
|
||||
procd_set_param respawn 3600 10 5
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
if [ -d "$RUN_DIR" ] || [ -f "$STATUS_FILE" ]; then
|
||||
log_event "停止" "监控服务已停止。"
|
||||
fi
|
||||
|
||||
rm -rf "$RUN_DIR"
|
||||
rm -f "$STATUS_FILE"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "pingpacket"
|
||||
}
|
||||
@@ -0,0 +1,682 @@
|
||||
#!/bin/sh
|
||||
|
||||
STATUS_FILE="/tmp/pingpacket_status.json"
|
||||
STATUS_TMP="${STATUS_FILE}.tmp"
|
||||
LOG_FILE="/tmp/pingpacket.log"
|
||||
LOG_MAX_LINES=500
|
||||
RUN_DIR="/var/run/pingpacket"
|
||||
START_TIME_FILE="$RUN_DIR/start_time"
|
||||
child_pids=""
|
||||
domestic_fail_count=0
|
||||
foreign_fail_count=0
|
||||
domestic_fault_open=0
|
||||
foreign_fault_open=0
|
||||
domestic_total_samples=0
|
||||
foreign_total_samples=0
|
||||
domestic_success_count=0
|
||||
foreign_success_count=0
|
||||
domestic_loss_count=0
|
||||
foreign_loss_count=0
|
||||
domestic_rtt_sum="0"
|
||||
foreign_rtt_sum="0"
|
||||
domestic_min_rtt=""
|
||||
foreign_min_rtt=""
|
||||
domestic_max_rtt=""
|
||||
foreign_max_rtt=""
|
||||
domestic_last_probe_at=0
|
||||
foreign_last_probe_at=0
|
||||
domestic_last_fault_probe_at=0
|
||||
foreign_last_fault_probe_at=0
|
||||
domestic_last_stats_probe_at=0
|
||||
foreign_last_stats_probe_at=0
|
||||
|
||||
log_rotate() {
|
||||
[ -f "$LOG_FILE" ] || return 0
|
||||
tail -n "$LOG_MAX_LINES" "$LOG_FILE" > "${LOG_FILE}.tmp" 2>/dev/null && \
|
||||
mv -f "${LOG_FILE}.tmp" "$LOG_FILE"
|
||||
}
|
||||
|
||||
log_event() {
|
||||
local event_type="$1"
|
||||
local scope="$2"
|
||||
local message="$3"
|
||||
local timestamp
|
||||
|
||||
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
if [ -n "$scope" ]; then
|
||||
printf '[%s] [%s] [%s] %s\n' "$timestamp" "$event_type" "$scope" "$message" >> "$LOG_FILE"
|
||||
else
|
||||
printf '[%s] [%s] %s\n' "$timestamp" "$event_type" "$message" >> "$LOG_FILE"
|
||||
fi
|
||||
log_rotate
|
||||
}
|
||||
|
||||
kill_children() {
|
||||
[ -n "$child_pids" ] || return 0
|
||||
kill $child_pids 2>/dev/null
|
||||
wait $child_pids 2>/dev/null
|
||||
child_pids=""
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
kill_children
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap cleanup INT TERM
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
date '+%Y-%m-%d %H:%M:%S' > "$START_TIME_FILE"
|
||||
|
||||
write_result_file() {
|
||||
local path="$1"
|
||||
local success="$2"
|
||||
local rtt="$3"
|
||||
local detail="$4"
|
||||
local probe_at="$5"
|
||||
|
||||
[ -n "$probe_at" ] || probe_at="$(date '+%s' 2>/dev/null || echo 0)"
|
||||
|
||||
mkdir -p "$(dirname "$path")"
|
||||
printf '%s\n%s\n%s\n%s\n' "$success" "$rtt" "$detail" "$probe_at" > "$path"
|
||||
}
|
||||
|
||||
normalize_foreign_url() {
|
||||
case "$1" in
|
||||
http://*|https://*)
|
||||
printf '%s\n' "$1"
|
||||
;;
|
||||
*)
|
||||
printf 'https://%s\n' "$1"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_runtime_state() {
|
||||
local domestic
|
||||
local foreign
|
||||
local proxy_type
|
||||
local proxy_host
|
||||
local proxy_port
|
||||
local domestic_interval
|
||||
local foreign_interval
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
if [ ! -s "$START_TIME_FILE" ]; then
|
||||
date '+%Y-%m-%d %H:%M:%S' > "$START_TIME_FILE"
|
||||
fi
|
||||
|
||||
if [ -f "$RUN_DIR/config" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v uci >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
domestic="$(uci -q get pingpacket.config.domestic_target)"
|
||||
foreign="$(uci -q get pingpacket.config.foreign_target)"
|
||||
proxy_type="$(uci -q get pingpacket.config.foreign_proxy_type)"
|
||||
proxy_host="$(uci -q get pingpacket.config.foreign_proxy_host)"
|
||||
proxy_port="$(uci -q get pingpacket.config.foreign_proxy_port)"
|
||||
domestic_interval="$(uci -q get pingpacket.config.domestic_interval)"
|
||||
foreign_interval="$(uci -q get pingpacket.config.foreign_interval)"
|
||||
|
||||
proxy_type="socks5h"
|
||||
[ -n "$proxy_host" ] || proxy_host="127.0.0.1"
|
||||
[ -n "$proxy_port" ] || proxy_port="7891"
|
||||
[ -n "$domestic_interval" ] || domestic_interval="1"
|
||||
[ -n "$foreign_interval" ] || foreign_interval="5"
|
||||
|
||||
printf '%s\n' "$domestic" > "$RUN_DIR/config"
|
||||
printf '%s\n' "$foreign" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$proxy_type" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$proxy_host" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$proxy_port" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$domestic_interval" >> "$RUN_DIR/config"
|
||||
printf '%s\n' "$foreign_interval" >> "$RUN_DIR/config"
|
||||
}
|
||||
|
||||
normalize_interval() {
|
||||
local value="$1"
|
||||
|
||||
case "$value" in
|
||||
""|*[!0-9]*)
|
||||
printf '1\n'
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$value" -ge 1 ] 2>/dev/null; then
|
||||
printf '%s\n' "$value"
|
||||
else
|
||||
printf '1\n'
|
||||
fi
|
||||
}
|
||||
|
||||
should_probe_now() {
|
||||
local last_probe_at="${1:-0}"
|
||||
local interval="${2:-1}"
|
||||
local now_ts="${3:-0}"
|
||||
|
||||
if [ "$last_probe_at" -le 0 ] 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ $((now_ts - last_probe_at)) -ge "$interval" ] 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_ip_literal() {
|
||||
case "$1" in
|
||||
*:*|*.*)
|
||||
printf '%s\n' "$1" | awk '
|
||||
/^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/ { found=1 }
|
||||
/^[0-9A-Fa-f:]+$/ { found=1 }
|
||||
END { exit(found ? 0 : 1) }
|
||||
'
|
||||
;;
|
||||
*)
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
resolve_target_ip() {
|
||||
local target="$1"
|
||||
local name="$2"
|
||||
local cache_target_file="$RUN_DIR/${name}_resolved_target"
|
||||
local cache_ip_file="$RUN_DIR/${name}_resolved_ip"
|
||||
local cache_time_file="$RUN_DIR/${name}_resolved_at"
|
||||
local cached_target
|
||||
local cached_ip
|
||||
local cached_time
|
||||
local now
|
||||
local resolved_ip=""
|
||||
|
||||
if is_ip_literal "$target"; then
|
||||
printf '%s\n' "$target"
|
||||
return 0
|
||||
fi
|
||||
|
||||
cached_target="$(cat "$cache_target_file" 2>/dev/null || echo "")"
|
||||
cached_ip="$(cat "$cache_ip_file" 2>/dev/null || echo "")"
|
||||
cached_time="$(cat "$cache_time_file" 2>/dev/null || echo "")"
|
||||
now="$(date '+%s' 2>/dev/null || echo 0)"
|
||||
|
||||
if [ "$cached_target" = "$target" ] && [ -n "$cached_ip" ] && [ -n "$cached_time" ]; then
|
||||
if [ $((now - cached_time)) -lt 300 ] 2>/dev/null; then
|
||||
printf '%s\n' "$cached_ip"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
if command -v resolveip >/dev/null 2>&1; then
|
||||
resolved_ip="$(
|
||||
resolveip -4 -t 3 "$target" 2>/dev/null | awk '
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
|
||||
print $i
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ -z "$resolved_ip" ] && command -v nslookup >/dev/null 2>&1; then
|
||||
resolved_ip="$(
|
||||
nslookup "$target" 2>/dev/null | awk '
|
||||
{
|
||||
for (i = 1; i <= NF; i++) {
|
||||
if ($i ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
|
||||
print $i
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
'
|
||||
)"
|
||||
fi
|
||||
|
||||
if [ -n "$resolved_ip" ]; then
|
||||
printf '%s\n' "$target" > "$cache_target_file"
|
||||
printf '%s\n' "$resolved_ip" > "$cache_ip_file"
|
||||
printf '%s\n' "$now" > "$cache_time_file"
|
||||
printf '%s\n' "$resolved_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$cached_target" = "$target" ] && [ -n "$cached_ip" ]; then
|
||||
printf '%s\n' "$cached_ip"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '%s\n' "$target"
|
||||
}
|
||||
|
||||
do_ping() {
|
||||
local target="$1"
|
||||
local name="$2"
|
||||
local result_file="$RUN_DIR/${name}_last_result"
|
||||
local ping_target
|
||||
local result
|
||||
local rtt
|
||||
|
||||
ping_target="$(resolve_target_ip "$target" "$name")"
|
||||
result="$(ping -n -c 1 -W 2 "$ping_target" 2>/dev/null)"
|
||||
if echo "$result" | grep -q "bytes from"; then
|
||||
rtt="$(echo "$result" | sed -n 's/.*time=\([0-9.]*\).*/\1/p')"
|
||||
write_result_file "$result_file" "1" "${rtt:-0}" ""
|
||||
else
|
||||
write_result_file "$result_file" "0" "" "ping 无响应"
|
||||
fi
|
||||
}
|
||||
|
||||
do_proxy_curl() {
|
||||
local target="$1"
|
||||
local proxy_type="$2"
|
||||
local proxy_host="${3:-127.0.0.1}"
|
||||
local proxy_port="$4"
|
||||
local name="$5"
|
||||
local result_file="$RUN_DIR/${name}_last_result"
|
||||
local error_file="$RUN_DIR/${name}_curl_error"
|
||||
local url
|
||||
local proxy_addr
|
||||
local curl_result
|
||||
local http_code
|
||||
local time_pretransfer
|
||||
local rtt_ms
|
||||
local detail
|
||||
local rc
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
write_result_file "$result_file" "0" "" "系统未安装 curl"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -z "$proxy_port" ]; then
|
||||
write_result_file "$result_file" "0" "" "代理端口未配置"
|
||||
return 0
|
||||
fi
|
||||
|
||||
url="$(normalize_foreign_url "$target")"
|
||||
proxy_addr="${proxy_host}:${proxy_port}"
|
||||
|
||||
curl_result="$(
|
||||
curl -k -I -L -sS --noproxy "" \
|
||||
--connect-timeout 5 --max-time 10 \
|
||||
--socks5-hostname "$proxy_addr" \
|
||||
-o /dev/null -w '%{http_code}:%{time_pretransfer}' \
|
||||
"$url" 2>"$error_file"
|
||||
)"
|
||||
rc=$?
|
||||
|
||||
http_code="$(printf '%s' "$curl_result" | awk -F ':' 'NR==1 { print $1 }')"
|
||||
time_pretransfer="$(printf '%s' "$curl_result" | awk -F ':' 'NR==1 { print $2 }')"
|
||||
|
||||
if [ "$rc" -eq 0 ] && [ -n "$http_code" ] && [ -n "$time_pretransfer" ] && { [ "$http_code" = "200" ] || [ "$http_code" = "204" ]; }; then
|
||||
rtt_ms="$(awk -v sec="$time_pretransfer" 'BEGIN { printf "%.1f", (sec + 0) * 1000 }')"
|
||||
write_result_file "$result_file" "1" "$rtt_ms" "curl 连接探测成功,HTTP ${http_code}"
|
||||
else
|
||||
detail="$(sed -n '1p' "$error_file" 2>/dev/null)"
|
||||
[ -n "$detail" ] || detail="curl 退出码 ${rc},HTTP ${http_code:-000}"
|
||||
write_result_file "$result_file" "0" "" "$detail"
|
||||
fi
|
||||
|
||||
rm -f "$error_file"
|
||||
}
|
||||
|
||||
get_target_state() {
|
||||
local name="$1"
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
fail_count="$domestic_fail_count"
|
||||
fault_open="$domestic_fault_open"
|
||||
;;
|
||||
foreign)
|
||||
fail_count="$foreign_fail_count"
|
||||
fault_open="$foreign_fault_open"
|
||||
;;
|
||||
*)
|
||||
fail_count=0
|
||||
fault_open=0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
set_target_state() {
|
||||
local name="$1"
|
||||
local next_fail_count="$2"
|
||||
local next_fault_open="$3"
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
domestic_fail_count="$next_fail_count"
|
||||
domestic_fault_open="$next_fault_open"
|
||||
;;
|
||||
foreign)
|
||||
foreign_fail_count="$next_fail_count"
|
||||
foreign_fault_open="$next_fault_open"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
get_processed_probe_at() {
|
||||
local name="$1"
|
||||
local kind="$2"
|
||||
|
||||
case "${name}:${kind}" in
|
||||
domestic:fault)
|
||||
processed_probe_at="$domestic_last_fault_probe_at"
|
||||
;;
|
||||
foreign:fault)
|
||||
processed_probe_at="$foreign_last_fault_probe_at"
|
||||
;;
|
||||
domestic:stats)
|
||||
processed_probe_at="$domestic_last_stats_probe_at"
|
||||
;;
|
||||
foreign:stats)
|
||||
processed_probe_at="$foreign_last_stats_probe_at"
|
||||
;;
|
||||
*)
|
||||
processed_probe_at=0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
set_processed_probe_at() {
|
||||
local name="$1"
|
||||
local kind="$2"
|
||||
local value="${3:-0}"
|
||||
|
||||
case "${name}:${kind}" in
|
||||
domestic:fault)
|
||||
domestic_last_fault_probe_at="$value"
|
||||
;;
|
||||
foreign:fault)
|
||||
foreign_last_fault_probe_at="$value"
|
||||
;;
|
||||
domestic:stats)
|
||||
domestic_last_stats_probe_at="$value"
|
||||
;;
|
||||
foreign:stats)
|
||||
foreign_last_stats_probe_at="$value"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
reset_target_state() {
|
||||
local name="$1"
|
||||
|
||||
rm -f "$RUN_DIR/${name}_last_result"
|
||||
set_target_state "$name" 0 0
|
||||
set_processed_probe_at "$name" "fault" 0
|
||||
set_processed_probe_at "$name" "stats" 0
|
||||
}
|
||||
|
||||
update_target_faults() {
|
||||
local name="$1"
|
||||
local label="$2"
|
||||
local target="$3"
|
||||
local result_file="$RUN_DIR/${name}_last_result"
|
||||
local success="0"
|
||||
local rtt=""
|
||||
local detail=""
|
||||
local probe_at=""
|
||||
|
||||
[ -n "$target" ] || {
|
||||
reset_target_state "$name"
|
||||
return 0
|
||||
}
|
||||
|
||||
get_target_state "$name"
|
||||
|
||||
if [ -f "$result_file" ]; then
|
||||
success="$(sed -n '1p' "$result_file" 2>/dev/null)"
|
||||
rtt="$(sed -n '2p' "$result_file" 2>/dev/null)"
|
||||
detail="$(sed -n '3p' "$result_file" 2>/dev/null)"
|
||||
probe_at="$(sed -n '4p' "$result_file" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
get_processed_probe_at "$name" "fault"
|
||||
if [ -n "$probe_at" ] && [ "$probe_at" = "$processed_probe_at" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$success" = "1" ]; then
|
||||
if [ "$fault_open" = "1" ]; then
|
||||
log_event "恢复" "$label" "目标 ${target} 已恢复响应,当前延迟 ${rtt:-0} ms"
|
||||
fi
|
||||
|
||||
set_target_state "$name" 0 0
|
||||
set_processed_probe_at "$name" "fault" "${probe_at:-0}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
fail_count=$((fail_count + 1))
|
||||
if [ -n "$detail" ]; then
|
||||
log_event "丢包" "$label" "目标 ${target} 本次探测失败(连续第 ${fail_count} 次):${detail}"
|
||||
else
|
||||
log_event "丢包" "$label" "目标 ${target} 本次探测失败(连续第 ${fail_count} 次)"
|
||||
fi
|
||||
fault_open=1
|
||||
|
||||
set_target_state "$name" "$fail_count" "$fault_open"
|
||||
set_processed_probe_at "$name" "fault" "${probe_at:-0}"
|
||||
}
|
||||
|
||||
calc_stats() {
|
||||
local name="$1"
|
||||
local total_samples=0
|
||||
local success_count=0
|
||||
local loss_count=0
|
||||
local rtt_sum="0"
|
||||
local min_rtt=""
|
||||
local max_rtt=""
|
||||
local avg="0.0"
|
||||
local min="0.0"
|
||||
local max="0.0"
|
||||
local loss_rate="0.0"
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
total_samples="$domestic_total_samples"
|
||||
success_count="$domestic_success_count"
|
||||
loss_count="$domestic_loss_count"
|
||||
rtt_sum="$domestic_rtt_sum"
|
||||
min_rtt="$domestic_min_rtt"
|
||||
max_rtt="$domestic_max_rtt"
|
||||
;;
|
||||
foreign)
|
||||
total_samples="$foreign_total_samples"
|
||||
success_count="$foreign_success_count"
|
||||
loss_count="$foreign_loss_count"
|
||||
rtt_sum="$foreign_rtt_sum"
|
||||
min_rtt="$foreign_min_rtt"
|
||||
max_rtt="$foreign_max_rtt"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$success_count" -gt 0 ]; then
|
||||
avg="$(awk -v sum="$rtt_sum" -v count="$success_count" 'BEGIN { printf "%.1f", (sum + 0) / count }')"
|
||||
min="$(awk -v value="${min_rtt:-0}" 'BEGIN { printf "%.1f", value + 0 }')"
|
||||
max="$(awk -v value="${max_rtt:-0}" 'BEGIN { printf "%.1f", value + 0 }')"
|
||||
fi
|
||||
|
||||
if [ "$total_samples" -gt 0 ]; then
|
||||
loss_rate="$(awk -v loss="$loss_count" -v total="$total_samples" 'BEGIN { printf "%.1f", loss * 100.0 / total }')"
|
||||
fi
|
||||
|
||||
printf '{"avg":"%s","min":"%s","max":"%s","loss_rate":"%s","loss_count":%d,"count":%d,"samples":%d}' \
|
||||
"$avg" "$min" "$max" "$loss_rate" "$loss_count" "$success_count" "$total_samples"
|
||||
}
|
||||
|
||||
update_cumulative_stats() {
|
||||
local name="$1"
|
||||
local target="$2"
|
||||
local result_file="$RUN_DIR/${name}_last_result"
|
||||
local success="0"
|
||||
local rtt=""
|
||||
local probe_at=""
|
||||
local total_samples=0
|
||||
local success_count=0
|
||||
local loss_count=0
|
||||
local rtt_sum="0"
|
||||
local min_rtt=""
|
||||
local max_rtt=""
|
||||
|
||||
[ -n "$target" ] || return 0
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
total_samples="$domestic_total_samples"
|
||||
success_count="$domestic_success_count"
|
||||
loss_count="$domestic_loss_count"
|
||||
rtt_sum="$domestic_rtt_sum"
|
||||
min_rtt="$domestic_min_rtt"
|
||||
max_rtt="$domestic_max_rtt"
|
||||
;;
|
||||
foreign)
|
||||
total_samples="$foreign_total_samples"
|
||||
success_count="$foreign_success_count"
|
||||
loss_count="$foreign_loss_count"
|
||||
rtt_sum="$foreign_rtt_sum"
|
||||
min_rtt="$foreign_min_rtt"
|
||||
max_rtt="$foreign_max_rtt"
|
||||
;;
|
||||
esac
|
||||
|
||||
total_samples=$((total_samples + 1))
|
||||
|
||||
if [ -f "$result_file" ]; then
|
||||
success="$(sed -n '1p' "$result_file" 2>/dev/null)"
|
||||
rtt="$(sed -n '2p' "$result_file" 2>/dev/null)"
|
||||
probe_at="$(sed -n '4p' "$result_file" 2>/dev/null)"
|
||||
fi
|
||||
|
||||
get_processed_probe_at "$name" "stats"
|
||||
if [ -n "$probe_at" ] && [ "$probe_at" = "$processed_probe_at" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$success" = "1" ]; then
|
||||
rtt="${rtt:-0}"
|
||||
success_count=$((success_count + 1))
|
||||
rtt_sum="$(awk -v sum="$rtt_sum" -v value="$rtt" 'BEGIN { printf "%.3f", (sum + 0) + (value + 0) }')"
|
||||
|
||||
if [ -z "$min_rtt" ]; then
|
||||
min_rtt="$rtt"
|
||||
else
|
||||
min_rtt="$(awk -v current="$min_rtt" -v value="$rtt" 'BEGIN { printf "%.3f", ((value + 0) < (current + 0)) ? (value + 0) : (current + 0) }')"
|
||||
fi
|
||||
|
||||
if [ -z "$max_rtt" ]; then
|
||||
max_rtt="$rtt"
|
||||
else
|
||||
max_rtt="$(awk -v current="$max_rtt" -v value="$rtt" 'BEGIN { printf "%.3f", ((value + 0) > (current + 0)) ? (value + 0) : (current + 0) }')"
|
||||
fi
|
||||
else
|
||||
loss_count=$((loss_count + 1))
|
||||
fi
|
||||
|
||||
case "$name" in
|
||||
domestic)
|
||||
domestic_total_samples="$total_samples"
|
||||
domestic_success_count="$success_count"
|
||||
domestic_loss_count="$loss_count"
|
||||
domestic_rtt_sum="$rtt_sum"
|
||||
domestic_min_rtt="$min_rtt"
|
||||
domestic_max_rtt="$max_rtt"
|
||||
;;
|
||||
foreign)
|
||||
foreign_total_samples="$total_samples"
|
||||
foreign_success_count="$success_count"
|
||||
foreign_loss_count="$loss_count"
|
||||
foreign_rtt_sum="$rtt_sum"
|
||||
foreign_min_rtt="$min_rtt"
|
||||
foreign_max_rtt="$max_rtt"
|
||||
;;
|
||||
esac
|
||||
|
||||
set_processed_probe_at "$name" "stats" "${probe_at:-0}"
|
||||
}
|
||||
|
||||
while true; do
|
||||
ensure_runtime_state
|
||||
START_TIME="$(cat "$START_TIME_FILE" 2>/dev/null || echo "")"
|
||||
UPDATED_AT="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
NOW_TS="$(date '+%s' 2>/dev/null || echo 0)"
|
||||
DOMESTIC_TARGET=""
|
||||
FOREIGN_TARGET=""
|
||||
FOREIGN_PROXY_TYPE="socks5h"
|
||||
FOREIGN_PROXY_HOST="127.0.0.1"
|
||||
FOREIGN_PROXY_PORT="7891"
|
||||
DOMESTIC_INTERVAL="1"
|
||||
FOREIGN_INTERVAL="5"
|
||||
|
||||
if [ -f "$RUN_DIR/config" ]; then
|
||||
DOMESTIC_TARGET="$(sed -n '1p' "$RUN_DIR/config")"
|
||||
FOREIGN_TARGET="$(sed -n '2p' "$RUN_DIR/config")"
|
||||
FOREIGN_PROXY_TYPE="$(sed -n '3p' "$RUN_DIR/config")"
|
||||
FOREIGN_PROXY_HOST="$(sed -n '4p' "$RUN_DIR/config")"
|
||||
FOREIGN_PROXY_PORT="$(sed -n '5p' "$RUN_DIR/config")"
|
||||
DOMESTIC_INTERVAL="$(sed -n '6p' "$RUN_DIR/config")"
|
||||
FOREIGN_INTERVAL="$(sed -n '7p' "$RUN_DIR/config")"
|
||||
fi
|
||||
|
||||
FOREIGN_PROXY_TYPE="socks5h"
|
||||
[ -n "$FOREIGN_PROXY_HOST" ] || FOREIGN_PROXY_HOST="127.0.0.1"
|
||||
[ -n "$FOREIGN_PROXY_PORT" ] || FOREIGN_PROXY_PORT="7891"
|
||||
[ -n "$FOREIGN_INTERVAL" ] || FOREIGN_INTERVAL="5"
|
||||
DOMESTIC_INTERVAL="$(normalize_interval "$DOMESTIC_INTERVAL")"
|
||||
FOREIGN_INTERVAL="$(normalize_interval "$FOREIGN_INTERVAL")"
|
||||
|
||||
child_pids=""
|
||||
if [ -n "$DOMESTIC_TARGET" ]; then
|
||||
if should_probe_now "$domestic_last_probe_at" "$DOMESTIC_INTERVAL" "$NOW_TS"; then
|
||||
do_ping "$DOMESTIC_TARGET" "domestic" &
|
||||
child_pids="$!"
|
||||
domestic_last_probe_at="$NOW_TS"
|
||||
fi
|
||||
else
|
||||
domestic_last_probe_at=0
|
||||
reset_target_state "domestic"
|
||||
fi
|
||||
|
||||
if [ -n "$FOREIGN_TARGET" ]; then
|
||||
if should_probe_now "$foreign_last_probe_at" "$FOREIGN_INTERVAL" "$NOW_TS"; then
|
||||
do_proxy_curl "$FOREIGN_TARGET" "$FOREIGN_PROXY_TYPE" "$FOREIGN_PROXY_HOST" "$FOREIGN_PROXY_PORT" "foreign" &
|
||||
child_pids="${child_pids:+$child_pids }$!"
|
||||
foreign_last_probe_at="$NOW_TS"
|
||||
fi
|
||||
else
|
||||
foreign_last_probe_at=0
|
||||
reset_target_state "foreign"
|
||||
fi
|
||||
|
||||
[ -n "$child_pids" ] && wait $child_pids 2>/dev/null
|
||||
child_pids=""
|
||||
|
||||
update_target_faults "domestic" "国内" "$DOMESTIC_TARGET"
|
||||
update_target_faults "foreign" "国外" "$FOREIGN_TARGET"
|
||||
update_cumulative_stats "domestic" "$DOMESTIC_TARGET"
|
||||
update_cumulative_stats "foreign" "$FOREIGN_TARGET"
|
||||
|
||||
DOMESTIC_STATS="$(calc_stats "domestic")"
|
||||
FOREIGN_STATS="$(calc_stats "foreign")"
|
||||
|
||||
printf '{"start_time":"%s","updated_at":"%s","domestic":%s,"foreign":%s}\n' \
|
||||
"$START_TIME" "$UPDATED_AT" "$DOMESTIC_STATS" "$FOREIGN_STATS" > "$STATUS_TMP" && \
|
||||
mv -f "$STATUS_TMP" "$STATUS_FILE"
|
||||
|
||||
sleep 1
|
||||
done
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"luci-app-pingpacket": {
|
||||
"description": "Grant access to luci-app-pingpacket",
|
||||
"read": {
|
||||
"uci": [
|
||||
"pingpacket"
|
||||
]
|
||||
},
|
||||
"write": {
|
||||
"uci": [
|
||||
"pingpacket"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=Web UI for QEMU Virtual Machine Simple (VMS)
|
||||
LUCI_DEPENDS:=+qemu-vms +ttyd +usbutils +pciutils
|
||||
LUCI_DEPENDS:=@TARGET_x86_64 +qemu-vms +ttyd +usbutils +pciutils
|
||||
PKG_LICENSE:=GPLv3
|
||||
PKG_VERSION:=0.0.1
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
+6
-6
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=qemu-vms
|
||||
PKG_VERSION:=0.0.1
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/package.mk
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
PKGARCH:=all
|
||||
TITLE:=QEMU Virtual Machines Simple (VMS)
|
||||
DEPENDS:= +qemu-x86_64-softmmu \
|
||||
+qemu-bridge-helper \
|
||||
+minicom \
|
||||
+socat
|
||||
DEPENDS:= @TARGET_x86_64 \
|
||||
+qemu-x86_64-softmmu \
|
||||
+qemu-bridge-helper \
|
||||
+minicom \
|
||||
+socat
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
|
||||
Reference in new issue
Block a user