mirror of
https://github.com/kenzok8/small-package
synced 2025-01-08 13:27:36 +08:00
update 2023-02-13 16:51:11
This commit is contained in:
parent
051fdc835c
commit
707b1d0a9d
340
homeproxy/LICENSE
Normal file
340
homeproxy/LICENSE
Normal file
@ -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.
|
24
homeproxy/Makefile
Normal file
24
homeproxy/Makefile
Normal file
@ -0,0 +1,24 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy
|
||||
PKG_VERSION:=1.0-dev-testing1
|
||||
|
||||
LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:= \
|
||||
+sing-box \
|
||||
+@SING_BOX_BUILD_GVISOR \
|
||||
+curl
|
||||
|
||||
define Package/luci-app-homeproxy/conffiles
|
||||
/etc/config/homeproxy
|
||||
/etc/homeproxy/certs/
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
6
homeproxy/README
Normal file
6
homeproxy/README
Normal file
@ -0,0 +1,6 @@
|
||||
TODO:
|
||||
- Check bugs in ACL, config generator etc.
|
||||
- Allow use redirect/tproxy mode for custom routing
|
||||
- Subscription page slow response with a large number of nodes
|
||||
- Refactor nft rules
|
||||
- Any other improvements
|
268
homeproxy/htdocs/luci-static/resources/homeproxy.js
Normal file
268
homeproxy/htdocs/luci-static/resources/homeproxy.js
Normal file
@ -0,0 +1,268 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require baseclass';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
return baseclass.extend({
|
||||
dns_strategy: {
|
||||
'': _('Default'),
|
||||
'prefer_ipv4': _('Prefer IPv4'),
|
||||
'prefer_ipv6': _('Prefer IPv6'),
|
||||
'ipv4_only': _('IPv4 only'),
|
||||
'ipv6_only': _('IPv6 only')
|
||||
},
|
||||
|
||||
shadowsocks_encrypt_methods: [
|
||||
/* Stream */
|
||||
'none',
|
||||
/* AEAD */
|
||||
'aes-128-gcm',
|
||||
'aes-192-gcm',
|
||||
'aes-256-gcm',
|
||||
'chacha20-ietf-poly1305',
|
||||
'xchacha20-ietf-poly1305',
|
||||
/* AEAD 2022 */
|
||||
'2022-blake3-aes-128-gcm',
|
||||
'2022-blake3-aes-256-gcm',
|
||||
'2022-blake3-chacha20-poly1305'
|
||||
],
|
||||
|
||||
tls_cipher_suites: [
|
||||
'TLS_RSA_WITH_AES_128_CBC_SHA',
|
||||
'TLS_RSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_RSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_RSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_AES_128_GCM_SHA256',
|
||||
'TLS_AES_256_GCM_SHA384',
|
||||
'TLS_CHACHA20_POLY1305_SHA256',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
|
||||
'TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256',
|
||||
'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256'
|
||||
],
|
||||
|
||||
tls_versions: [
|
||||
'1.0',
|
||||
'1.1',
|
||||
'1.2',
|
||||
'1.3'
|
||||
],
|
||||
|
||||
calcStringMD5: function(e) {
|
||||
/* Thanks to https://stackoverflow.com/a/41602636 */
|
||||
function h(a, b) {
|
||||
var c, d, e, f, g;
|
||||
e = a & 2147483648;
|
||||
f = b & 2147483648;
|
||||
c = a & 1073741824;
|
||||
d = b & 1073741824;
|
||||
g = (a & 1073741823) + (b & 1073741823);
|
||||
return c & d ? g ^ 2147483648 ^ e ^ f : c | d ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e ^ f : g ^ e ^ f;
|
||||
}
|
||||
function k(a, b, c, d, e, f, g) { a = h(a, h(h(b & c | ~b & d, e), g)); return h(a << f | a >>> 32 - f, b); }
|
||||
function l(a, b, c, d, e, f, g) { a = h(a, h(h(b & d | c & ~d, e), g)); return h(a << f | a >>> 32 - f, b); }
|
||||
function m(a, b, d, c, e, f, g) { a = h(a, h(h(b ^ d ^ c, e), g)); return h(a << f | a >>> 32 - f, b); }
|
||||
function n(a, b, d, c, e, f, g) { a = h(a, h(h(d ^ (b | ~c), e), g)); return h(a << f | a >>> 32 - f, b); }
|
||||
function p(a) {
|
||||
var b = '', d = '';
|
||||
for (var c = 0; 3 >= c; c++) d = a >>> 8 * c & 255, d = '0' + d.toString(16), b += d.substr(d.length - 2, 2);
|
||||
return b;
|
||||
}
|
||||
|
||||
var f = [], q, r, s, t, a, b, c, d;
|
||||
e = function(a) {
|
||||
a = a.replace(/\r\n/g, '\n');
|
||||
for (var b = '', d = 0; d < a.length; d++) {
|
||||
var c = a.charCodeAt(d);
|
||||
128 > c ? b += String.fromCharCode(c) : (127 < c && 2048 > c ? b += String.fromCharCode(c >> 6 | 192) :
|
||||
(b += String.fromCharCode(c >> 12 | 224), b += String.fromCharCode(c >> 6 & 63 | 128)),
|
||||
b += String.fromCharCode(c & 63 | 128))
|
||||
}
|
||||
return b;
|
||||
}(e);
|
||||
f = function(b) {
|
||||
var c = b.length, a = c + 8;
|
||||
for (var d = 16 * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;)
|
||||
a = (g - g % 4) / 4, f = g % 4 * 8, e[a] |= b.charCodeAt(g) << f, g++;
|
||||
a = (g - g % 4) / 4; e[a] |= 128 << g % 4 * 8; e[d - 2] = c << 3; e[d - 1] = c >>> 29;
|
||||
return e;
|
||||
}(e);
|
||||
a = 1732584193;
|
||||
b = 4023233417;
|
||||
c = 2562383102;
|
||||
d = 271733878;
|
||||
|
||||
for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d,
|
||||
a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710),
|
||||
c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966),
|
||||
a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426),
|
||||
c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313),
|
||||
a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879),
|
||||
c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134),
|
||||
a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195),
|
||||
c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329),
|
||||
a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664),
|
||||
c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994),
|
||||
a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083),
|
||||
c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448),
|
||||
a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606),
|
||||
c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501),
|
||||
a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512),
|
||||
c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562),
|
||||
a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833),
|
||||
c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740),
|
||||
a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353),
|
||||
c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656),
|
||||
a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074),
|
||||
c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189),
|
||||
a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461),
|
||||
c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645),
|
||||
a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415),
|
||||
c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241),
|
||||
a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690),
|
||||
c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497),
|
||||
a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552),
|
||||
c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649),
|
||||
a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917),
|
||||
c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745),
|
||||
a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t);
|
||||
return (p(a) + p(b) + p(c) + p(d)).toLowerCase();
|
||||
},
|
||||
|
||||
decodeBase64Str: function(str) {
|
||||
if (!str)
|
||||
return null;
|
||||
|
||||
/* Thanks to luci-app-ssr-plus */
|
||||
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var padding = (4 - str.length % 4) % 4;
|
||||
if (padding)
|
||||
str = str + Array(padding + 1).join('=');
|
||||
|
||||
return decodeURIComponent(Array.prototype.map.call(atob(str), (c) => {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
},
|
||||
|
||||
getBuiltinFeatures: function() {
|
||||
var callGetSingBoxFeatures = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'singbox_get_features',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return L.resolveDefault(callGetSingBoxFeatures(), {});
|
||||
},
|
||||
|
||||
loadDefaultLabel: function(uciconfig, ucisection) {
|
||||
var label = uci.get(uciconfig, ucisection, 'label');
|
||||
if (label)
|
||||
return label;
|
||||
else
|
||||
return ucisection;
|
||||
},
|
||||
|
||||
loadModalTitle: function(title, addtitle, uciconfig, ucisection) {
|
||||
var label = uci.get(uciconfig, ucisection, 'label');
|
||||
return label ? title + ' » ' + label : addtitle;
|
||||
},
|
||||
|
||||
renderSectionAdd: function(section, extra_class) {
|
||||
var el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
|
||||
nameEl = el.querySelector('.cbi-section-create-name');
|
||||
ui.addValidator(nameEl, 'uciname', true, (v) => {
|
||||
var button = el.querySelector('.cbi-section-create > .cbi-button-add');
|
||||
var uciconfig = section.uciconfig || section.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');
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
uploadCertificate: function(option, type, filename, ev) {
|
||||
var callWriteCertificate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'certificate_write',
|
||||
params: ['filename'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return ui.uploadFile(String.format('/tmp/homeproxy_certificate.tmp', filename), ev.target)
|
||||
.then(L.bind((btn, res) => {
|
||||
return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => {
|
||||
if (ret.result === true)
|
||||
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size)));
|
||||
else {
|
||||
ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error)));
|
||||
}
|
||||
});
|
||||
}, this, ev.target))
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message)) });
|
||||
},
|
||||
|
||||
validateBase64Key: function(length, section_id, value) {
|
||||
/* Thanks to luci-proto-wireguard */
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format('non-empty value');
|
||||
else if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
|
||||
return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateUniqueValue: function(uciconfig, ucisection, ucioption, section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
|
||||
var duplicate = false;
|
||||
uci.sections(uciconfig, ucisection, (res) => {
|
||||
if (res['.name'] !== section_id)
|
||||
if (res[ucioption] === value)
|
||||
duplicate = true
|
||||
});
|
||||
if (duplicate)
|
||||
return _('Expecting: %s').format(_('unique value'));
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
validateUUID: function(section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null)
|
||||
return _('Expecting: %s').format(_('valid uuid'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
990
homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js
Normal file
990
homeproxy/htdocs/luci-static/resources/view/homeproxy/client.js
Normal file
@ -0,0 +1,990 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require form';
|
||||
'require network';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require validation';
|
||||
'require view';
|
||||
|
||||
'require homeproxy as hp';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
var callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callReadDomainList = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'acllist_read',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callWriteDomainList = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'acllist_write',
|
||||
params: ['type', 'content'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => {
|
||||
var isRunning = false;
|
||||
try {
|
||||
isRunning = res['homeproxy']['instances']['sing-box']['running'];
|
||||
} catch (e) { }
|
||||
return isRunning;
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(isRunning) {
|
||||
var spanTemp = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
|
||||
var renderHTML;
|
||||
if (isRunning) {
|
||||
renderHTML = spanTemp.format('green', _('HomeProxy'), _('RUNNING'));
|
||||
} else {
|
||||
renderHTML = spanTemp.format('red', _('HomeProxy'), _('NOT RUNNING'));
|
||||
}
|
||||
|
||||
return renderHTML;
|
||||
}
|
||||
|
||||
function validatePortRange(section_id, value) {
|
||||
if (section_id && value) {
|
||||
value = value.match(/^(\d+)?\:(\d+)?$/);
|
||||
if (value && (value[1] || value[2])) {
|
||||
if (!value[1])
|
||||
value[1] = 0;
|
||||
else if (!value[2])
|
||||
value[2] = 65535;
|
||||
|
||||
if (value[1] < value[2] && value[2] <= 65535)
|
||||
return true;
|
||||
}
|
||||
|
||||
return _('Expecting: %s').format( _('valid port range (port1:port2)'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
var stubValidator = {
|
||||
factory: validation,
|
||||
apply: function(type, value, args) {
|
||||
if (value != null)
|
||||
this.value = value;
|
||||
|
||||
return validation.types[type].apply(this, args);
|
||||
},
|
||||
assert: function(condition) {
|
||||
return !!condition;
|
||||
}
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('homeproxy'),
|
||||
network.getHostHints()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var m, s, o, ss, so;
|
||||
var hosts = data[1]?.hosts;
|
||||
|
||||
m = new form.Map('homeproxy', _('HomeProxy'),
|
||||
_('The modern ImmortalWrt proxy platform for ARM64/AMD64.'));
|
||||
|
||||
s = m.section(form.TypedSection);
|
||||
s.render = function () {
|
||||
poll.add(function () {
|
||||
return L.resolveDefault(getServiceStatus()).then((res) => {
|
||||
var view = document.getElementById('service_status');
|
||||
view.innerHTML = renderStatus(res);
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data...'))
|
||||
]);
|
||||
}
|
||||
|
||||
/* Cache all configured proxy nodes, they will be called multiple times */
|
||||
var proxy_nodes = {};
|
||||
uci.sections(data[0], 'node', (res) => {
|
||||
proxy_nodes[res['.name']] =
|
||||
String.format('[%s] %s', res.type, res.label || (stubValidator.apply('ip6addr', res.address || '') ?
|
||||
`[${res.address}]` : res.address) + ':' + res.port);
|
||||
});
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy');
|
||||
|
||||
s.tab('routing', _('Routing Settings'));
|
||||
|
||||
o = s.taboption('routing', form.ListValue, 'main_node', _('Main node'));
|
||||
o.value('nil', _('Disable'));
|
||||
for (var i in proxy_nodes)
|
||||
o.value(i, proxy_nodes[i]);
|
||||
o.default = 'nil';
|
||||
o.depends({'routing_mode': 'custom', '!reverse': true});
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('routing', form.ListValue, 'main_udp_node', _('Main UDP node'));
|
||||
o.value('nil', _('Disable'));
|
||||
o.value('same', _('Same as main node'));
|
||||
for (var i in proxy_nodes)
|
||||
o.value(i, proxy_nodes[i]);
|
||||
o.default = 'nil';
|
||||
o.depends({'routing_mode': 'custom', '!reverse': true});
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('routing', form.Flag, 'ipv6_support', _('IPv6 support'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
o.depends({'routing_mode': 'custom', '!reverse': true});
|
||||
|
||||
o = s.taboption('routing', form.ListValue, 'routing_mode', _('Routing mode'));
|
||||
o.value('gfwlist', _('GFWList'));
|
||||
o.value('bypass_mainland_china', _('Bypass mainland China'));
|
||||
o.value('proxy_mainland_china', _('Only proxy mainland China'));
|
||||
o.value('custom', _('Custom routing'));
|
||||
o.value('global', _('Global'));
|
||||
o.default = 'bypass_mainland_china';
|
||||
o.rmempty = false;
|
||||
o.onchange = function(ev, section_id, value) {
|
||||
if (section_id && value === 'custom')
|
||||
this.map.save(null, true);
|
||||
}
|
||||
|
||||
o = s.taboption('routing', form.Value, 'routing_port', _('Routing ports'),
|
||||
_('Specify target port(s) that get proxied. Multiple ports must be separated by commas.'));
|
||||
o.value('all', _('All ports'));
|
||||
o.value('common', _('Common ports only (bypass P2P traffic)'));
|
||||
o.default = 'common';
|
||||
o.rmempty = false;
|
||||
o.depends({'routing_mode': 'custom', '!reverse': true});
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id && value !== 'all' && value !== 'common') {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('valid port value'));
|
||||
|
||||
var ports = [];
|
||||
for (var i of value.split(',')) {
|
||||
if (!stubValidator.apply('port', i))
|
||||
return _('Expecting: %s').format(_('valid port value'));
|
||||
if (ports.includes(i))
|
||||
return _('Port %s alrealy exists!').format(i);
|
||||
ports = ports.concat(i);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.taboption('routing', form.Value, 'dns_server', _('DNS server'),
|
||||
_('You can only have one server set. Custom DNS server format as plain IPv4/IPv6.'));
|
||||
o.value('wan', _('Use DNS server from WAN'));
|
||||
o.value('1.1.1.1', _('CloudFlare Public DNS (1.1.1.1)'));
|
||||
o.value('208.67.222.222', _('Cisco Public DNS (208.67.222.222)'));
|
||||
o.value('8.8.8.8', _('Google Public DNS (8.8.8.8)'));
|
||||
o.value('', '---');
|
||||
o.value('223.5.5.5', _('Aliyun Public DNS (223.5.5.5)'));
|
||||
o.value('119.29.29.29', _('Tencent Public DNS (119.29.29.29)'));
|
||||
o.value('114.114.114.114', _('Xinfeng Public DNS (114.114.114.114)'));
|
||||
o.default = '8.8.8.8';
|
||||
o.rmempty = false;
|
||||
o.depends({'routing_mode': 'custom', '!reverse': true});
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id && !['local', 'wan'].includes(value)) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
else if (!stubValidator.apply('ipaddr', value))
|
||||
return _('Expecting: %s').format(_('valid IP address'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Regular mode ACL settings start */
|
||||
s.tab('control', _('Access Control'));
|
||||
|
||||
o = s.taboption('control', form.SectionValue, '_control', form.NamedSection, 'control', 'homeproxy');
|
||||
o.depends({'routing_mode': 'custom', '!reverse': true});
|
||||
ss = o.subsection;
|
||||
|
||||
/* Interface control start */
|
||||
ss.tab('interface', _('Interface Control'));
|
||||
|
||||
so = ss.taboption('interface', widgets.DeviceSelect, 'listen_interfaces', _('Listen interfaces'),
|
||||
_('Only process traffic from specific interfaces. Leave empty for all.'));
|
||||
so.multiple = true;
|
||||
so.noaliases = true;
|
||||
|
||||
so = ss.taboption('interface', widgets.DeviceSelect, 'bind_interface', _('Bind interface'),
|
||||
_('Bind outbound traffic to specific interface. Leave empty to auto detect.'));
|
||||
so.multiple = false;
|
||||
so.noaliases = true;
|
||||
/* Interface control end */
|
||||
|
||||
/* LAN IP policy start */
|
||||
ss.tab('lan_ip_policy', _('LAN IP Policy'));
|
||||
|
||||
var ipaddrs = {}, ip6addrs = {};
|
||||
Object.keys(hosts).forEach(function(mac) {
|
||||
var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
|
||||
|
||||
for (var i = 0; i < addrs.length; i++)
|
||||
ipaddrs[addrs[i]] = hosts[mac].name || mac;
|
||||
});
|
||||
Object.keys(hosts).forEach(function(mac) {
|
||||
var addrs = L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6);
|
||||
|
||||
for (var i = 0; i < addrs.length; i++)
|
||||
ip6addrs[addrs[i]] = hosts[mac].name || mac;
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.ListValue, 'lan_proxy_mode', _('Proxy filter mode'));
|
||||
so.value('disabled', _('Disable'));
|
||||
so.value('listed_only', _('Proxy listed only'));
|
||||
so.value('except_listed', _('Proxy all except listed'));
|
||||
so.default = 'disabled';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_direct_mac_addrs', _('Direct MAC addresses'));
|
||||
so.datatype = 'macaddr';
|
||||
so.depends('lan_proxy_mode', 'except_listed');
|
||||
Object.keys(hosts).forEach(function(mac) {
|
||||
var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
|
||||
so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_direct_ipv4_ips', _('Direct IPv4 IP-s'));
|
||||
so.datatype = 'or(ip4addr, cidr4)';
|
||||
so.depends('lan_proxy_mode', 'except_listed');
|
||||
L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
|
||||
so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_direct_ipv6_ips', _('Direct IPv6 IP-s'));
|
||||
so.datatype = 'or(ip6addr, cidr6)';
|
||||
so.depends('lan_proxy_mode', 'except_listed');
|
||||
L.sortedKeys(ip6addrs, null, 'addr').forEach(function(ipv6) {
|
||||
so.value(ipv6, '%s (%s)'.format(ipv6, ip6addrs[ipv6]));
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_proxy_mac_addrs', _('Proxy MAC addresses'));
|
||||
so.datatype = 'macaddr';
|
||||
so.depends('lan_proxy_mode', 'listed_only');
|
||||
Object.keys(hosts).forEach(function(mac) {
|
||||
var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
|
||||
so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_proxy_ipv4_ips', _('Proxy IPv4 IP-s'));
|
||||
so.datatype = 'or(ip4addr, cidr4)';
|
||||
so.depends('lan_proxy_mode', 'listed_only');
|
||||
L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
|
||||
so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_proxy_ipv6_ips', _('Proxy IPv6 IP-s'));
|
||||
so.datatype = 'or(ip6addr, cidr6)';
|
||||
so.depends('lan_proxy_mode', 'listed_only');
|
||||
L.sortedKeys(ip6addrs, null, 'addr').forEach(function(ipv6) {
|
||||
so.value(ipv6, '%s (%s)'.format(ipv6, ip6addrs[ipv6]));
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_gaming_mode_mac_addrs', _('Gaming mode MAC addresses'));
|
||||
so.datatype = 'macaddr';
|
||||
Object.keys(hosts).forEach(function(mac) {
|
||||
var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
|
||||
so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_gaming_mode_ipv4_ips', _('Gaming mode IPv4 IP-s'));
|
||||
so.datatype = 'or(ip4addr, cidr4)';
|
||||
L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
|
||||
so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_gaming_mode_ipv6_ips', _('Gaming mode IPv6 IP-s'));
|
||||
so.datatype = 'or(ip6addr, cidr6)';
|
||||
L.sortedKeys(ip6addrs, null, 'addr').forEach(function(ipv6) {
|
||||
so.value(ipv6, '%s (%s)'.format(ipv6, ip6addrs[ipv6]));
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_global_proxy_mac_addrs', _('Global proxy MAC addresses'));
|
||||
so.datatype = 'macaddr';
|
||||
Object.keys(hosts).forEach(function(mac) {
|
||||
var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
|
||||
so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_global_proxy_ipv4_ips', _('Global proxy IPv4 IP-s'));
|
||||
so.datatype = 'or(ip4addr, cidr4)';
|
||||
L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
|
||||
so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
|
||||
});
|
||||
|
||||
so = ss.taboption('lan_ip_policy', form.DynamicList, 'lan_global_proxy_ipv6_ips', _('Global proxy IPv6 IP-s'));
|
||||
so.datatype = 'or(ip6addr, cidr6)';
|
||||
L.sortedKeys(ip6addrs, null, 'addr').forEach(function(ipv6) {
|
||||
so.value(ipv6, '%s (%s)'.format(ipv6, ip6addrs[ipv6]));
|
||||
});
|
||||
/* LAN IP policy end */
|
||||
|
||||
/* WAN IP policy start */
|
||||
ss.tab('wan_ip_policy', _('WAN IP Policy'));
|
||||
|
||||
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_proxy_ipv4_ips', _('Proxy IPv4 IP-s'));
|
||||
so.datatype = 'or(ip4addr, cidr4)';
|
||||
|
||||
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_proxy_ipv6_ips', _('Proxy IPv6 IP-s'));
|
||||
so.datatype = 'or(ip6addr, cidr6)';
|
||||
|
||||
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_direct_ipv4_ips', _('Direct IPv4 IP-s'));
|
||||
so.datatype = 'or(ip4addr, cidr4)';
|
||||
|
||||
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_direct_ipv6_ips', _('Direct IPv6 IP-s'));
|
||||
so.datatype = 'or(ip6addr, cidr6)';
|
||||
/* WAN IP policy end */
|
||||
|
||||
/* Proxy domain list start */
|
||||
ss.tab('proxy_domain_list', _('Proxy Domain List'));
|
||||
|
||||
so = ss.taboption('proxy_domain_list', form.TextValue, '_proxy_domain_list');
|
||||
so.rows = 10;
|
||||
so.monospace = true;
|
||||
so.datatype = 'hostname';
|
||||
so.load = function(section_id) {
|
||||
return L.resolveDefault(callReadDomainList('proxy_list')).then((res) => {
|
||||
return res.content;
|
||||
}, {});
|
||||
}
|
||||
so.write = function(section_id, value) {
|
||||
return callWriteDomainList('proxy_list', value);
|
||||
}
|
||||
so.remove = function(section_id, value) {
|
||||
return callWriteDomainList('proxy_list', '');
|
||||
}
|
||||
so.validate = function(section_id, value) {
|
||||
if (section_id && value) {
|
||||
for (var i of value.split('\n')) {
|
||||
if (i && !stubValidator.apply('hostname', i))
|
||||
return _('Expecting: %s').format(_('valid hostname'));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
/* Proxy domain list end */
|
||||
|
||||
/* Direct domain list start */
|
||||
ss.tab('direct_domain_list', _('Direct Domain List'));
|
||||
|
||||
so = ss.taboption('direct_domain_list', form.TextValue, '_direct_domain_list');
|
||||
so.rows = 10;
|
||||
so.monospace = true;
|
||||
so.datatype = 'hostname';
|
||||
so.load = function(section_id) {
|
||||
return L.resolveDefault(callReadDomainList('direct_list')).then((res) => {
|
||||
return res.content;
|
||||
}, {});
|
||||
}
|
||||
so.write = function(section_id, value) {
|
||||
return callWriteDomainList('direct_list', value);
|
||||
}
|
||||
so.remove = function(section_id, value) {
|
||||
return callWriteDomainList('direct_list', '');
|
||||
}
|
||||
so.validate = function(section_id, value) {
|
||||
if (section_id && value) {
|
||||
for (var i of value.split('\n')) {
|
||||
if (i && !stubValidator.apply('hostname', i))
|
||||
return _('Expecting: %s').format(_('valid hostname'));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
/* Direct domain list end */
|
||||
/* Regular mode ACL settings end */
|
||||
|
||||
/* Custom routing settings start */
|
||||
/* Routing settings start */
|
||||
o = s.taboption('routing', form.SectionValue, '_routing', form.NamedSection, 'routing', 'homeproxy');
|
||||
o.depends('routing_mode', 'custom');
|
||||
|
||||
ss = o.subsection;
|
||||
so = ss.option(form.Flag, 'sniff_override', _('Override destination'),
|
||||
_('Override the connection destination address with the sniffed domain.'));
|
||||
so.default = so.enabled;
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.ListValue, 'default_outbound', _('Default outbound'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('nil', _('Disable'));
|
||||
this.value('direct-out', _('Direct'));
|
||||
this.value('block-out', _('Block'));
|
||||
uci.sections(data[0], 'routing_node', (res) => {
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.default = 'nil';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(widgets.DeviceSelect, 'default_interface', _('Default interface'),
|
||||
_('Bind outbound connections to the specified NIC by default.<br/>Auto detect if leave empty.'));
|
||||
so.multiple = false;
|
||||
so.noaliases = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'tcpip_stack', _('TCP/IP stack'),
|
||||
_('TCP/IP stack.'));
|
||||
so.value('gvisor', _('gVisor'));
|
||||
so.value('system', _('System'));
|
||||
so.default = 'gvisor';
|
||||
so.rmempty = false;
|
||||
so.onchange = function(ev, section_id, value) {
|
||||
var desc = ev.target.nextElementSibling;
|
||||
if (value === 'gvisor')
|
||||
desc.innerHTML = _('Based on google/gvisor (recommended).');
|
||||
else
|
||||
desc.innerHTML = _('Less compatibility and sometimes better performance.');
|
||||
}
|
||||
|
||||
so = ss.option(form.Flag, 'endpoint_independent_nat', _('Enable endpoint-independent NAT'),
|
||||
_('Performance may degrade slightly, so it is not recommended to enable on when it is not needed.'));
|
||||
so.default = so.enabled;
|
||||
so.depends('tcpip_stack', 'gvisor');
|
||||
so.rmempty = false;
|
||||
/* Routing settings end */
|
||||
|
||||
/* Routing nodes start */
|
||||
s.tab('routing_node', _('Routing Nodes'));
|
||||
o = s.taboption('routing_node', form.SectionValue, '_routing_node', form.GridSection, 'routing_node');
|
||||
o.depends('routing_mode', 'custom');
|
||||
|
||||
ss = o.subsection;
|
||||
ss.addremove = true;
|
||||
ss.sortable = true;
|
||||
ss.nodescriptions = true;
|
||||
ss.modaltitle = L.bind(hp.loadModalTitle, this, _('Routing node'), _('Add a routing node'), data[0]);
|
||||
ss.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
ss.renderSectionAdd = L.bind(hp.renderSectionAdd, this, ss);
|
||||
|
||||
so = ss.option(form.Value, 'label', _('Label'));
|
||||
so.load = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'routing_node', 'label');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
||||
so.default = so.enabled;
|
||||
so.rmempty = false;
|
||||
so.editable = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'node', _('Node'),
|
||||
_('Outbound node'));
|
||||
for (var i in proxy_nodes)
|
||||
so.value(i, proxy_nodes[i]);
|
||||
so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'routing_node', 'node');
|
||||
so.editable = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'domain_strategy', _('Domain strategy'),
|
||||
_('If set, the server domain name will be resolved to IP before connecting.<br/>dns.strategy will be used if empty.'));
|
||||
for (var i in hp.dns_strategy)
|
||||
so.value(i, hp.dns_strategy[i]);
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(widgets.DeviceSelect, 'bind_interface', _('Bind interface'),
|
||||
_('The network interface to bind to.'));
|
||||
so.multiple = false;
|
||||
so.noaliases = true;
|
||||
so.depends('outbound', '');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'outbound', _('Outbound'),
|
||||
_('The tag of the upstream outbound.<br/>Other dial fields will be ignored when enabled.'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('', _('Direct'));
|
||||
uci.sections(data[0], 'routing_node', (res) => {
|
||||
if (res['.name'] !== section_id && res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.validate = function(section_id, value) {
|
||||
if (section_id && value) {
|
||||
var node = this.map.lookupOption('node', section_id)[0].formvalue(section_id);
|
||||
|
||||
var conflict = false;
|
||||
uci.sections(data[0], 'routing_node', (res) => {
|
||||
if (res['.name'] !== section_id)
|
||||
if (res.outbound === section_id && res['.name'] == value)
|
||||
conflict = true;
|
||||
});
|
||||
if (conflict)
|
||||
return _('Recursive outbound detected!');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
/* Routing nodes end */
|
||||
|
||||
/* Routing rules start */
|
||||
s.tab('routing_rule', _('Routing Rules'));
|
||||
o = s.taboption('routing_rule', form.SectionValue, '_routing_rule', form.GridSection, 'routing_rule');
|
||||
o.depends('routing_mode', 'custom');
|
||||
|
||||
ss = o.subsection;
|
||||
ss.addremove = true;
|
||||
ss.sortable = true;
|
||||
ss.nodescriptions = true;
|
||||
ss.modaltitle = L.bind(hp.loadModalTitle, this, _('Routing rule'), _('Add a routing rule'), data[0]);
|
||||
ss.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
ss.renderSectionAdd = L.bind(hp.renderSectionAdd, this, ss);
|
||||
|
||||
so = ss.option(form.Value, 'label', _('Label'));
|
||||
so.load = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'routing_rule', 'label');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
||||
so.default = so.enabled;
|
||||
so.rmempty = false;
|
||||
so.editable = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'ip_version', _('IP version'),
|
||||
_('4 or 6. Not limited if empty.'));
|
||||
so.value('4', _('IPv4'));
|
||||
so.value('6', _('IPv6'));
|
||||
so.value('', _('Both'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'mode', _('Mode'),
|
||||
_('The default rule uses the following matching logic:<br/>' +
|
||||
'<code>(domain || domain_suffix || domain_keyword || domain_regex || geosite || geoip || ip_cidr)</code> &&<br/>' +
|
||||
'<code>(source_geoip || source_ip_cidr)</code> &&<br/>' +
|
||||
'<code>other fields</code>.'));
|
||||
so.value('default', _('Default'));
|
||||
so.default = 'default';
|
||||
so.rmempty = false;
|
||||
so.readonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'invert', _('Invert'),
|
||||
_('Invert match result.'));
|
||||
so.default = so.disabled;
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'network', _('Network'));
|
||||
so.value('tcp', _('TCP'));
|
||||
so.value('udp', _('UDP'));
|
||||
so.value('', _('Both'));
|
||||
|
||||
so = ss.option(form.MultiValue, 'protocol', _('Protocol'),
|
||||
_('Sniffed protocol, see <a target="_blank" href="https://sing-box.sagernet.org/configuration/route/sniff/">Sniff</a> for details.'));
|
||||
so.value('http', _('HTTP'));
|
||||
so.value('tls', _('TLS'));
|
||||
so.value('quic', _('QUIC'));
|
||||
so.value('stun', _('STUN'));
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain', _('Domain name'),
|
||||
_('Match full domain.'));
|
||||
so.datatype = 'hostname';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain_suffix', _('Domain suffix'),
|
||||
_('Match domain suffix.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain_keyword', _('Domain keyword'),
|
||||
_('Match domain using keyword.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain_regex', _('Domain regex'),
|
||||
_('Match domain using regular expression.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'geosite', _('Geosite'),
|
||||
_('Match geosite.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'source_geoip', _('Source GeoIP'),
|
||||
_('Match source geoip.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'geoip', _('GeoIP'),
|
||||
_('Match geoip.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'source_ip_cidr', _('Source IP CIDR'),
|
||||
_('Match source ip cidr.'));
|
||||
so.datatype = 'or(cidr, ipaddr)';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'ip_cidr', _('IP CIDR'),
|
||||
_('Match ip cidr.'));
|
||||
so.datatype = 'or(cidr, ipaddr)';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'source_port', _('Source port'),
|
||||
_('Match source port.'));
|
||||
so.datatype = 'port';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'source_port_range', _('Source port range'),
|
||||
_('Match source port range. Format as START:/:END/START:END.'));
|
||||
so.validate = validatePortRange;
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'port', _('Port'),
|
||||
_('Match port.'));
|
||||
so.datatype = 'port';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'port_range', _('Port range'),
|
||||
_('Match port range. Format as START:/:END/START:END.'));
|
||||
so.validate = validatePortRange;
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'process_name', _('Process name'),
|
||||
_('Match process name.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'process_path', _('Process path'),
|
||||
_('Match process path.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'user', _('User'),
|
||||
_('Match user name.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'outbound', _('Outbound'),
|
||||
_('Tag of the target outbound.'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('direct-out', _('Direct'));
|
||||
this.value('block-out', _('Block'));
|
||||
uci.sections(data[0], 'routing_node', (res) => {
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.rmempty = false;
|
||||
so.editable = true;
|
||||
/* Routing rules end */
|
||||
|
||||
/* DNS settings start */
|
||||
s.tab('dns', _('DNS Settings'));
|
||||
o = s.taboption('dns', form.SectionValue, '_dns', form.NamedSection, 'dns', 'homeproxy');
|
||||
o.depends('routing_mode', 'custom');
|
||||
|
||||
ss = o.subsection;
|
||||
so = ss.option(form.ListValue, 'dns_strategy', _('DNS strategy'),
|
||||
_('The DNS strategy for resolving the domain name in the address.'));
|
||||
for (var i in hp.dns_strategy)
|
||||
if (i)
|
||||
so.value(i, hp.dns_strategy[i]);
|
||||
so.default = 'prefer_ipv4';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.ListValue, 'default_server', _('Default DNS server'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('block-dns', _('Block DNS queries'));
|
||||
uci.sections(data[0], 'dns_server', (res) => {
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.default = 'default-dns';
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.Flag, 'disable_cache', _('Disable DNS cache'));
|
||||
so.default = so.disabled;
|
||||
|
||||
so = ss.option(form.Flag, 'disable_cache_expire', _('Disable cache expire'));
|
||||
so.default = so.disabled;
|
||||
so.depends('disable_cache', '0');
|
||||
/* DNS settings end */
|
||||
|
||||
/* DNS servers start */
|
||||
s.tab('dns_server', _('DNS Servers'));
|
||||
o = s.taboption('dns_server', form.SectionValue, '_dns_server', form.GridSection, 'dns_server');
|
||||
o.depends('routing_mode', 'custom');
|
||||
|
||||
ss = o.subsection;
|
||||
ss.addremove = true;
|
||||
ss.sortable = true;
|
||||
ss.nodescriptions = true;
|
||||
ss.modaltitle = L.bind(hp.loadModalTitle, this, _('DNS server'), _('Add a DNS server'), data[0]);
|
||||
ss.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
ss.renderSectionAdd = L.bind(hp.renderSectionAdd, this, ss);
|
||||
|
||||
so = ss.option(form.Value, 'label', _('Label'));
|
||||
so.load = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'dns_server', 'label');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
||||
so.default = so.enabled;
|
||||
so.rmempty = false;
|
||||
so.editable = true;
|
||||
|
||||
so = ss.option(form.Value, 'address', _('Address'),
|
||||
_('The address of the dns server. Support UDP, TCP, DoT, DoH and RCode.'));
|
||||
so.rmempty = false;
|
||||
|
||||
so = ss.option(form.ListValue, 'address_resolver', _('Address resolver'),
|
||||
_('Tag of a another server to resolve the domain name in the address. Required if address contains domain.'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('', _('None'));
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
uci.sections(data[0], 'dns_server', (res) => {
|
||||
if (res['.name'] !== section_id && res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.validate = function(section_id, value) {
|
||||
if (section_id && value) {
|
||||
var conflict = false;
|
||||
uci.sections(data[0], 'dns_server', (res) => {
|
||||
if (res['.name'] !== section_id)
|
||||
if (res.address_resolver === section_id && res['.name'] == value)
|
||||
conflict = true;
|
||||
});
|
||||
if (conflict)
|
||||
return _('Recursive resolver detected!');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'address_strategy', _('Address strategy'),
|
||||
_('The domain strategy for resolving the domain name in the address. dns.strategy will be used if empty.'));
|
||||
for (var i in hp.dns_strategy)
|
||||
so.value(i, hp.dns_strategy[i]);
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'resolve_strategy', _('Resolve strategy'),
|
||||
_('Default domain strategy for resolving the domain names.'));
|
||||
for (var i in hp.dns_strategy)
|
||||
so.value(i, hp.dns_strategy[i]);
|
||||
|
||||
so = ss.option(form.ListValue, 'outbound', _('Outbound'),
|
||||
_('Tag of an outbound for connecting to the dns server.'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('direct-out', _('Direct'));
|
||||
uci.sections(data[0], 'routing_node', (res) => {
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.default = 'direct-out';
|
||||
so.rmempty = false;
|
||||
so.editable = true;
|
||||
/* DNS servers end */
|
||||
|
||||
/* DNS rules start */
|
||||
s.tab('dns_rule', _('DNS Rules'));
|
||||
o = s.taboption('dns_rule', form.SectionValue, '_dns_rule', form.GridSection, 'dns_rule');
|
||||
o.depends('routing_mode', 'custom');
|
||||
|
||||
ss = o.subsection;
|
||||
ss.addremove = true;
|
||||
ss.sortable = true;
|
||||
ss.nodescriptions = true;
|
||||
ss.modaltitle = L.bind(hp.loadModalTitle, this, _('DNS rule'), _('Add a DNS rule'), data[0]);
|
||||
ss.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
ss.renderSectionAdd = L.bind(hp.renderSectionAdd, this, ss);
|
||||
|
||||
so = ss.option(form.Value, 'label', _('Label'));
|
||||
so.load = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
so.validate = L.bind(hp.validateUniqueValue, this, data[0], 'dns_rule', 'label');
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'enabled', _('Enable'));
|
||||
so.default = so.enabled;
|
||||
so.rmempty = false;
|
||||
so.editable = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'mode', _('Mode'),
|
||||
_('The default rule uses the following matching logic:<br/>' +
|
||||
'<code>(domain || domain_suffix || domain_keyword || domain_regex || geosite || ip_cidr)</code> &&<br/>' +
|
||||
'<code>(source_geoip || source_ip_cidr)</code> &&<br/>' +
|
||||
'<code>other fields</code>.'));
|
||||
so.value('default', _('Default'));
|
||||
so.default = 'default';
|
||||
so.rmempty = false;
|
||||
so.readonly = true;
|
||||
|
||||
so = ss.option(form.Flag, 'invert', _('Invert'),
|
||||
_('Invert match result.'));
|
||||
so.default = so.disabled;
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'network', _('Network'));
|
||||
so.value('tcp', _('TCP'));
|
||||
so.value('udp', _('UDP'));
|
||||
so.value('', _('Both'));
|
||||
|
||||
so = ss.option(form.MultiValue, 'protocol', _('Protocol'),
|
||||
_('Sniffed protocol, see <a target="_blank" href="https://sing-box.sagernet.org/configuration/route/sniff/">Sniff</a> for details.'));
|
||||
so.value('http', _('HTTP'));
|
||||
so.value('tls', _('TLS'));
|
||||
so.value('quic', _('QUIC'));
|
||||
so.value('dns', _('DNS'));
|
||||
so.value('stun', _('STUN'));
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain', _('Domain name'),
|
||||
_('Match full domain.'));
|
||||
so.datatype = 'hostname';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain_suffix', _('Domain suffix'),
|
||||
_('Match domain suffix.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain_keyword', _('Domain keyword'),
|
||||
_('Match domain using keyword.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'domain_regex', _('Domain regex'),
|
||||
_('Match domain using regular expression.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'geosite', _('Geosite'),
|
||||
_('Match geosite.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'source_geoip', _('Source GeoIP'),
|
||||
_('Match source geoip.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'source_ip_cidr', _('Source IP CIDR'),
|
||||
_('Match source ip cidr.'));
|
||||
so.datatype = 'or(cidr, ipaddr)';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'ip_cidr', _('IP CIDR'),
|
||||
_('Match ip cidr.'));
|
||||
so.datatype = 'or(cidr, ipaddr)';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'source_port', _('Source port'),
|
||||
_('Match source port.'));
|
||||
so.datatype = 'port';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'source_port_range', _('Source port range'),
|
||||
_('Match source port range. Format as START:/:END/START:END.'));
|
||||
so.validate = validatePortRange;
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'port', _('Port'),
|
||||
_('Match port.'));
|
||||
so.datatype = 'port';
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'port_range', _('Port range'),
|
||||
_('Match port range. Format as START:/:END/START:END.'));
|
||||
so.validate = validatePortRange;
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'process_name', _('Process name'),
|
||||
_('Match process name.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'process_path', _('Process path'),
|
||||
_('Match process path.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.DynamicList, 'user', _('User'),
|
||||
_('Match user name.'));
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.MultiValue, 'outbound', _('Outbound'),
|
||||
_('Match outbound.'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('direct-out', _('Direct'));
|
||||
this.value('block-out', _('Block'));
|
||||
uci.sections(data[0], 'routing_node', (res) => {
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.modalonly = true;
|
||||
|
||||
so = ss.option(form.ListValue, 'server', _('Server'),
|
||||
_('Tag of the target dns server.'));
|
||||
so.load = function(section_id) {
|
||||
delete this.keylist;
|
||||
delete this.vallist;
|
||||
|
||||
this.value('default-dns', _('Default DNS (issued by WAN)'));
|
||||
this.value('block-dns', _('Block DNS queries'));
|
||||
uci.sections(data[0], 'dns_server', (res) => {
|
||||
if (res.enabled === '1')
|
||||
this.value(res['.name'], res.label);
|
||||
});
|
||||
|
||||
return this.super('load', section_id);
|
||||
}
|
||||
so.rmempty = false;
|
||||
so.editable = true;
|
||||
|
||||
so = ss.option(form.Flag, 'dns_disable_cache', _('Disable dns cache'),
|
||||
_('Disable cache and save cache in this query.'));
|
||||
so.default = so.disabled;
|
||||
so.modalonly = true;
|
||||
/* DNS rules end */
|
||||
/* Custom routing settings end */
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
1151
homeproxy/htdocs/luci-static/resources/view/homeproxy/node.js
Normal file
1151
homeproxy/htdocs/luci-static/resources/view/homeproxy/node.js
Normal file
File diff suppressed because it is too large
Load Diff
502
homeproxy/htdocs/luci-static/resources/view/homeproxy/server.js
Normal file
502
homeproxy/htdocs/luci-static/resources/view/homeproxy/server.js
Normal file
@ -0,0 +1,502 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require form';
|
||||
'require uci';
|
||||
'require view';
|
||||
|
||||
'require homeproxy as hp';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
uci.load('homeproxy'),
|
||||
hp.getBuiltinFeatures()
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('homeproxy', _('Edit servers'));
|
||||
|
||||
s = m.section(form.NamedSection, 'server', 'homeproxy', _('Global settings'));
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'auto_firewall', _('Auto configure firewall'));
|
||||
o.default = o.enabled;
|
||||
|
||||
s = m.section(form.GridSection, 'server');
|
||||
s.addremove = true;
|
||||
s.sortable = true;
|
||||
s.nodescriptions = true;
|
||||
s.modaltitle = L.bind(hp.loadModalTitle, this, _('Server'), _('Add a server'), data[0]);
|
||||
s.sectiontitle = L.bind(hp.loadDefaultLabel, this, data[0]);
|
||||
s.renderSectionAdd = L.bind(hp.renderSectionAdd, this, s);
|
||||
|
||||
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], 'server', 'label');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.enabled;
|
||||
o.rmempty = false;
|
||||
o.editable = true;
|
||||
|
||||
o = s.option(form.ListValue, 'type', _('Type'));
|
||||
o.value('http', _('HTTP'));
|
||||
if (data[1].with_quic) {
|
||||
o.value('hysteria', _('Hysteria'));
|
||||
o.value('naive', _('NaïveProxy'));
|
||||
}
|
||||
o.value('shadowsocks', _('Shadowsocks'));
|
||||
o.value('socks', _('Socks'));
|
||||
o.value('trojan', _('Trojan'));
|
||||
o.value('vmess', _('VMess'));
|
||||
o.rmempty = false;
|
||||
o.onchange = function(ev, section_id, value) {
|
||||
var tls_element = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
|
||||
if (value === 'hysteria') {
|
||||
var event = document.createEvent('HTMLEvents');
|
||||
event.initEvent('change', true, true);
|
||||
|
||||
tls_element.checked = true;
|
||||
tls_element.dispatchEvent(event);
|
||||
tls_element.disabled = true;
|
||||
} else
|
||||
tls_element.disabled = null;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'port', _('Port'),
|
||||
_('The port must be unique.'));
|
||||
o.datatype = 'port';
|
||||
o.validate = L.bind(hp.validateUniqueValue, this, data[0], 'server', 'port');
|
||||
|
||||
o = s.option(form.Value, 'username', _('Username'));
|
||||
o.depends('type', 'http');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'socks');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'password', _('Password'));
|
||||
o.password = true;
|
||||
o.depends('type', 'http');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.depends('type', 'socks');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends({'type': 'shadowtls', 'shadowtls_version': '2'});
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
var type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
|
||||
if (type === 'shadowsocks') {
|
||||
var encmode = this.map.lookupOption('shadowsocks_encrypt_method', section_id)[0].formvalue(section_id);
|
||||
if (encmode === 'none')
|
||||
return true;
|
||||
else if (encmode === '2022-blake3-aes-128-gcm')
|
||||
return hp.validateBase64Key(24, section_id, value);
|
||||
else if (['2022-blake3-aes-256-gcm', '2022-blake3-chacha20-poly1305'].includes(encmode))
|
||||
return hp.validateBase64Key(44, section_id, value);
|
||||
}
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
/* Hysteria config start */
|
||||
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
|
||||
o.value('udp');
|
||||
/* WeChat-Video / FakeTCP are unsupported by sing-box currently
|
||||
o.value('wechat-video');
|
||||
o.value('faketcp');
|
||||
*/
|
||||
o.default = 'udp';
|
||||
o.depends('type', 'hysteria');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_down_mbps', _('Max download speed'),
|
||||
_('Max download speed in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_up_mbps', _('Max upload speed'),
|
||||
_('Max upload speed in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'hysteria_auth_type', _('Authentication type'));
|
||||
o.value('disabled', _('Disable'));
|
||||
o.value('base64', _('Base64'));
|
||||
o.value('string', _('String'));
|
||||
o.default = 'disabled';
|
||||
o.depends('type', 'hysteria');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_auth_payload', _('Authentication payload'));
|
||||
o.depends({'type': 'hysteria', 'hysteria_auth_type': 'base64'});
|
||||
o.depends({'type': 'hysteria', 'hysteria_auth_type': 'string'});
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_obfs_password', _('Obfuscate password'));
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_recv_window_conn', _('QUIC stream receive window'),
|
||||
_('The QUIC stream-level flow control window for receiving data.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '67108864';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_recv_window_client', _('QUIC connection receive window'),
|
||||
_('The QUIC connection-level flow control window for receiving data.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '15728640';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_max_conn_client', _('QUIC maximum concurrent bidirectional streams'),
|
||||
_('The maximum number of QUIC concurrent bidirectional streams that a peer is allowed to open.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1024';
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'hysteria_disable_mtu_discovery', _('Disable Path MTU discovery'),
|
||||
_('Disables Path MTU Discovery (RFC 8899). Packets will then be at most 1252 (IPv4) / 1232 (IPv6) bytes in size.'));
|
||||
o.default = o.disabled;
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
/* Hysteria config end */
|
||||
|
||||
/* Shadowsocks config */
|
||||
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
||||
for (var i of hp.shadowsocks_encrypt_methods)
|
||||
o.value(i);
|
||||
o.default = 'aes-128-gcm';
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.modalonly = true;
|
||||
|
||||
/* ShadowTLS config */
|
||||
o = s.option(form.ListValue, 'shadowtls_version', _('ShadowTLS version'));
|
||||
o.value('1', _('v1'));
|
||||
o.value('2', _('v2'));
|
||||
o.default = '1';
|
||||
o.depends('type', 'shadowtls');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
/* VMess config start */
|
||||
o = s.option(form.Value, 'uuid', _('UUID'));
|
||||
o.depends('type', 'vmess');
|
||||
o.validate = hp.validateUUID;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'vmess_alterid', _('Alter ID'),
|
||||
_('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
/* VMess config end */
|
||||
|
||||
/* Transport config start */
|
||||
o = s.option(form.ListValue, 'transport', _('Transport'),
|
||||
_('No TCP transport, plain HTTP is merged into the HTTP transport.'));
|
||||
o.value('', _('None'));
|
||||
o.value('grpc', _('gRPC'));
|
||||
o.value('http', _('HTTP'));
|
||||
o.value('quic', _('QUIC'));
|
||||
o.value('ws', _('WebSocket'));
|
||||
o.onchange = function(ev, section_id, value) {
|
||||
var desc = this.map.findElement('id', 'cbid.homeproxy.%s.transport'.format(section_id)).nextElementSibling;
|
||||
if (value === 'http')
|
||||
desc.innerHTML = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
|
||||
else if (value === 'quic')
|
||||
desc.innerHTML = _('No additional encryption support: It\'s basically duplicate encryption.');
|
||||
else
|
||||
desc.innerHTML = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
|
||||
}
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
|
||||
/* gRPC config */
|
||||
o = s.option(form.Value, 'grpc_servicename', _('gRPC service name'));
|
||||
o.depends({'type': 'trojan', 'transport': 'grpc'});
|
||||
o.depends({'type': 'vmess', 'transport': 'grpc'});
|
||||
o.modalonly = true;
|
||||
|
||||
/* HTTP config start */
|
||||
o = s.option(form.DynamicList, 'http_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends({'type': 'trojan', 'transport': 'http'});
|
||||
o.depends({'type': 'vmess', 'transport': 'http'});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_path', _('Path'));
|
||||
o.depends({'type': 'trojan', 'transport': 'http'});
|
||||
o.depends({'type': 'vmess', 'transport': 'http'});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_method', _('Method'));
|
||||
o.depends({'type': 'trojan', 'transport': 'http'});
|
||||
o.depends({'type': 'vmess', 'transport': 'http'});
|
||||
o.modalonly = true;
|
||||
/* HTTP config end */
|
||||
|
||||
/* WebSocket config start */
|
||||
o = s.option(form.Value, 'ws_host', _('Host'));
|
||||
o.depends({'type': 'trojan', 'transport': 'ws', 'tls': '0'});
|
||||
o.depends({'type': 'vmess', 'transport': 'ws', 'tls': '0'});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'ws_path', _('Path'));
|
||||
o.depends({'type': 'trojan', 'transport': 'ws'});
|
||||
o.depends({'type': 'vmess', 'transport': 'ws'});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'websocket_early_data', _('Early data'),
|
||||
_('Allowed payload size is in the request.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2048';
|
||||
o.depends({'type': 'trojan', 'transport': 'ws'});
|
||||
o.depends({'type': 'vmess', 'transport': 'ws'});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'websocket_early_data_header', _('Early data header name'),
|
||||
_('Early data is sent in path instead of header by default.') +
|
||||
'<br/>' +
|
||||
_('To be compatible with Xray-core, set this to <code>Sec-WebSocket-Protocol</code>.'));
|
||||
o.default = 'Sec-WebSocket-Protocol';
|
||||
o.depends({'type': 'trojan', 'transport': 'ws'});
|
||||
o.depends({'type': 'vmess', 'transport': 'ws'});
|
||||
o.modalonly = true;
|
||||
/* WebSocket config end */
|
||||
|
||||
/* Transport config end */
|
||||
|
||||
/* TLS config start */
|
||||
o = s.option(form.Flag, 'tls', _('TLS'));
|
||||
o.default = o.disabled;
|
||||
o.depends('type', 'http');
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vmess');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_sni', _('TLS SNI'),
|
||||
_('Used to verify the hostname on the returned certificates unless insecure is given.'));
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_alpn', _('TLS ALPN'),
|
||||
_('List of supported application level protocols, in order of preference.'));
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'),
|
||||
_('The minimum TLS version that is acceptable.'));
|
||||
o.value('', _('default'));
|
||||
for (var i of hp.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'),
|
||||
_('The maximum TLS version that is acceptable.'));
|
||||
o.value('', _('default'));
|
||||
for (var i of hp.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.MultiValue, 'tls_cipher_suites', _('Cipher suites'),
|
||||
_('The elliptic curves that will be used in an ECDHE handshake, in preference order. If empty, the default will be used.'));
|
||||
for (var i of hp.tls_cipher_suites)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.optional = true;
|
||||
o.modalonly = true;
|
||||
|
||||
if (data[1].with_acme) {
|
||||
o = s.option(form.Flag, 'tls_acme', _('Enable ACME'),
|
||||
_('Use ACME TLS certificate issuer.'));
|
||||
o.default = o.disabled;
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_acme_domain', _('Domains'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_dsn', _('Default server name'),
|
||||
_('Server name to use when choosing a certificate if the ClientHello\'s ServerName field is empty.'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_email', _('Email'),
|
||||
_('The email address to use when creating or selecting an existing ACME server account.'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format('non-empty value');
|
||||
else if (!value.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/))
|
||||
return _('Expecting: %s').format('valid email address');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_provider', _('CA provider'),
|
||||
_('The ACME CA provider to use.'));
|
||||
o.value('letsencrypt', _('Let\'s Encrypt'));
|
||||
o.value('zerossl', _('ZeroSSL'));
|
||||
o.depends('tls_acme', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_dhc', _('Disable HTTP challenge'));
|
||||
o.default = o.disabled;
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_dtac', _('Disable TLS ALPN challenge'));
|
||||
o.default = o.disabled;
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ahp', _('Alternative HTTP port'),
|
||||
_('The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a listener for the HTTP challenge.'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_atp', _('Alternative TLS port'),
|
||||
_('The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to succeed.'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_external_account', _('External Account Binding'),
|
||||
_('EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known by the CA.' +
|
||||
'<br/>External account bindings are "used to associate an ACME account with an existing account in a non-ACME system, such as a CA customer database.'));
|
||||
o.default = o.disabled;
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ea_keyid', _('External account key ID'));
|
||||
o.depends('tls_acme_external_account', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ea_mackey', _('External account MAC key'));
|
||||
o.depends('tls_acme_external_account', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'tls_cert_path', _('Certificate path'),
|
||||
_('The server public key, in PEM format.'));
|
||||
o.value('/etc/homeproxy/certs/server_publickey.pem');
|
||||
o.depends({'tls': '1', 'tls_acme': null});
|
||||
o.depends({'tls': '1', 'tls_acme': '0'});
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Button, '_upload_cert', _('Upload certificate'),
|
||||
_('<strong>Save your configuration before uploading files!</strong>'));
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = _('Upload...');
|
||||
o.depends({'tls': '1', 'tls_cert_path': '/etc/homeproxy/certs/server_publickey.pem'});
|
||||
o.onclick = L.bind(hp.uploadCertificate, this, _('certificate'), 'server_publickey');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_key_path', _('Key path'),
|
||||
_('The server private key, in PEM format.'));
|
||||
o.value('/etc/homeproxy/certs/server_privatekey.pem');
|
||||
o.depends({'tls': '1', 'tls_acme': null});
|
||||
o.depends({'tls': '1', 'tls_acme': '0'});
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Button, '_upload_key', _('Upload key'),
|
||||
_('<strong>Save your configuration before uploading files!</strong>'));
|
||||
o.inputstyle = 'action';
|
||||
o.inputtitle = _('Upload...');
|
||||
o.depends({'tls': '1', 'tls_key_path': '/etc/homeproxy/certs/server_privatekey.pem'});
|
||||
o.onclick = L.bind(hp.uploadCertificate, this, _('private key'), 'server_privatekey');
|
||||
o.modalonly = true;
|
||||
/* TLS config end */
|
||||
|
||||
/* Extra settings start */
|
||||
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
|
||||
_('Enable tcp fast open for listener.'));
|
||||
o.default = o.disabled;
|
||||
o.depends({'network': 'udp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'),
|
||||
_('Enable UDP fragmentation.'));
|
||||
o.default = o.disabled;
|
||||
o.depends({'network': 'tcp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'sniff_override', _('Override destination'),
|
||||
_('Override the connection destination address with the sniffed domain.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.ListValue, 'domain_strategy', _('Domain strategy'),
|
||||
_('If set, the requested domain name will be resolved to IP before routing.'));
|
||||
for (var i in hp.dns_strategy)
|
||||
o.value(i, hp.dns_strategy[i])
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'proxy_protocol', _('Proxy protocol'),
|
||||
_('Parse Proxy Protocol in the connection header.'));
|
||||
o.default = o.disabled;
|
||||
o.depends({'network': 'udp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'proxy_protocol_accept_no_header', _('Accept no header'),
|
||||
_('Accept connections without Proxy Protocol header.'));
|
||||
o.default = o.disabled;
|
||||
o.depends('proxy_protocol', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'network', _('Network'));
|
||||
o.value('tcp', _('TCP'));
|
||||
o.value('udp', _('UDP'));
|
||||
o.value('', _('Both'));
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.modalonly = true;
|
||||
/* Extra settings end */
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
191
homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js
Normal file
191
homeproxy/htdocs/luci-static/resources/view/homeproxy/status.js
Normal file
@ -0,0 +1,191 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
/* Thanks to luci-app-aria2 */
|
||||
var css = ' \
|
||||
#log_textarea { \
|
||||
padding: 10px; \
|
||||
text-align: left; \
|
||||
} \
|
||||
#log_textarea pre { \
|
||||
padding: .5rem; \
|
||||
word-break: break-all; \
|
||||
margin: 0; \
|
||||
} \
|
||||
.description { \
|
||||
background-color: #33ccff; \
|
||||
}';
|
||||
|
||||
var hp_dir = '/var/run/homeproxy';
|
||||
|
||||
function getResVersion(self, type) {
|
||||
var callResVersion = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'resources_get_version',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var callResUpdate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'resources_update',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return L.resolveDefault(callResVersion(type), {}).then((res) => {
|
||||
var spanTemp = E('div', { 'style': 'cbi-value-field' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return L.resolveDefault(callResUpdate(type), {}).then((res) => {
|
||||
switch (res.status) {
|
||||
case 0:
|
||||
self.description = _('Successfully updated.');
|
||||
break;
|
||||
case 1:
|
||||
self.description = _('Update failed.');
|
||||
break;
|
||||
case 2:
|
||||
self.description = _('Already in updating.');
|
||||
break;
|
||||
case 3:
|
||||
self.description = _('Already at the latest version.');
|
||||
break;
|
||||
default:
|
||||
self.description = _('Unknown error.');
|
||||
break;
|
||||
}
|
||||
|
||||
return self.map.reset();
|
||||
});
|
||||
})
|
||||
}, [ _('Check update') ]),
|
||||
' ',
|
||||
E('strong', { 'style': (res.error ? 'color:red' : 'color:green') },
|
||||
[ res.error ? 'not found' : res.version ]),
|
||||
]);
|
||||
|
||||
self.default = spanTemp;
|
||||
});
|
||||
}
|
||||
|
||||
function getRuntimeLog(name) {
|
||||
var callLogClean = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'log_clean',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
var log_textarea = E('div', { 'id': 'log_textarea' },
|
||||
E('img', {
|
||||
'src': L.resource(['icons/loading.gif']),
|
||||
'alt': _('Loading'),
|
||||
'style': 'vertical-align:middle'
|
||||
}, _('Collecting data...'))
|
||||
);
|
||||
|
||||
var log;
|
||||
poll.add(L.bind(function() {
|
||||
return fs.read_direct(`${hp_dir}/${name.toLowerCase()}.log`, 'text')
|
||||
.then(function(res) {
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
res.trim() || _('Log is empty.')
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
}).catch(function(err) {
|
||||
if (err.toString().includes('NotFoundError'))
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
_('Log file does not exist.')
|
||||
]);
|
||||
else
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
_('Unknown error: %s').format(err)
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
});
|
||||
}));
|
||||
|
||||
return E([
|
||||
E('style', [ css ]),
|
||||
E('div', {'class': 'cbi-map'}, [
|
||||
E('h3', {'name': 'content'}, [
|
||||
_('%s log').format(name),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function() {
|
||||
return L.resolveDefault(callLogClean(name.toLowerCase()), {});
|
||||
})
|
||||
}, [ _('Clean log') ])
|
||||
]),
|
||||
E('div', {'class': 'cbi-section'}, [
|
||||
log_textarea,
|
||||
E('div', {'style': 'text-align:right'},
|
||||
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('homeproxy');
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_geoip_version', _('GeoIP version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'geoip') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_geosite_version', _('GeoSite version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'geosite') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'china_ip4') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'china_ip6') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_gfw_list_version', _('GFW list version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'gfw_list') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_china_list_version', _('China list version'));
|
||||
o.cfgvalue = function() { return getResVersion(this, 'china_list') };
|
||||
o.rawhtml = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_homeproxy_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, 'HomeProxy');
|
||||
|
||||
o = s.option(form.DummyValue, '_sing-box_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, 'sing-box');
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
53
homeproxy/root/etc/config/homeproxy
Normal file
53
homeproxy/root/etc/config/homeproxy
Normal file
@ -0,0 +1,53 @@
|
||||
|
||||
config homeproxy 'infra'
|
||||
option __warning 'DO NOT EDIT THIS SECTION, OR YOU ARE ON YOUR OWN!'
|
||||
option common_port '22,53,80,143,443,465,587,853,993,995,8080,8443,9418'
|
||||
option redirect_port '5331'
|
||||
option tproxy_port '5332'
|
||||
option dns_port '5333'
|
||||
option tun_name 'singtun0'
|
||||
|
||||
option table_mark '100'
|
||||
option self_mark '100'
|
||||
option tproxy_mark '101'
|
||||
option tun_mark '102'
|
||||
|
||||
config homeproxy 'config'
|
||||
option main_node 'nil'
|
||||
option main_udp_node 'same'
|
||||
option ipv6_support '1'
|
||||
option routing_mode 'gfwlist'
|
||||
option routing_port 'common'
|
||||
option dns_server '8.8.8.8'
|
||||
|
||||
config homeproxy 'control'
|
||||
option lan_proxy_mode 'disabled'
|
||||
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.12.0/22'
|
||||
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/22'
|
||||
list wan_proxy_ipv4_ips '149.154.164.0/22'
|
||||
list wan_proxy_ipv4_ips '149.154.172.0/22'
|
||||
|
||||
config homeproxy 'routing'
|
||||
option sniff_override '1'
|
||||
option default_outbound 'direct-out'
|
||||
|
||||
config homeproxy 'dns'
|
||||
option dns_strategy 'prefer_ipv4'
|
||||
option disable_cache '0'
|
||||
option disable_cache_expire '0'
|
||||
option default_server 'local-dns'
|
||||
|
||||
config homeproxy 'subscription'
|
||||
option dns_strategy 'prefer_ipv4'
|
||||
option default_server 'local-dns'
|
||||
option disable_cache '0'
|
||||
option disable_cache_expire '0'
|
||||
|
||||
config homeproxy 'server'
|
||||
option enabled '0'
|
||||
option auto_firewall '0'
|
||||
|
4116
homeproxy/root/etc/homeproxy/resources/china_ip4.txt
Normal file
4116
homeproxy/root/etc/homeproxy/resources/china_ip4.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
homeproxy/root/etc/homeproxy/resources/china_ip4.ver
Normal file
1
homeproxy/root/etc/homeproxy/resources/china_ip4.ver
Normal file
@ -0,0 +1 @@
|
||||
20230209
|
1640
homeproxy/root/etc/homeproxy/resources/china_ip6.txt
Normal file
1640
homeproxy/root/etc/homeproxy/resources/china_ip6.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
homeproxy/root/etc/homeproxy/resources/china_ip6.ver
Normal file
1
homeproxy/root/etc/homeproxy/resources/china_ip6.ver
Normal file
@ -0,0 +1 @@
|
||||
20230209
|
66086
homeproxy/root/etc/homeproxy/resources/china_list.txt
Normal file
66086
homeproxy/root/etc/homeproxy/resources/china_list.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
homeproxy/root/etc/homeproxy/resources/china_list.ver
Normal file
1
homeproxy/root/etc/homeproxy/resources/china_list.ver
Normal file
@ -0,0 +1 @@
|
||||
202302082210
|
6134
homeproxy/root/etc/homeproxy/resources/gfw_list.txt
Normal file
6134
homeproxy/root/etc/homeproxy/resources/gfw_list.txt
Normal file
File diff suppressed because it is too large
Load Diff
1
homeproxy/root/etc/homeproxy/resources/gfw_list.ver
Normal file
1
homeproxy/root/etc/homeproxy/resources/gfw_list.ver
Normal file
@ -0,0 +1 @@
|
||||
202302082210
|
19
homeproxy/root/etc/homeproxy/scripts/clean_log.sh
Executable file
19
homeproxy/root/etc/homeproxy/scripts/clean_log.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
NAME="homeproxy"
|
||||
|
||||
log_max_size="10" #KB
|
||||
main_log_file="/var/run/$NAME/$NAME.log"
|
||||
sing_log_file="/var/run/$NAME/sing-box.log"
|
||||
|
||||
while true; do
|
||||
for i in "$main_log_file" "$sing_log_file"; do
|
||||
[ -s "$i" ] || continue
|
||||
[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
|
||||
done
|
||||
|
||||
sleep 180
|
||||
done
|
463
homeproxy/root/etc/homeproxy/scripts/firewall_post.ut
Executable file
463
homeproxy/root/etc/homeproxy/scripts/firewall_post.ut
Executable file
@ -0,0 +1,463 @@
|
||||
#!/usr/bin/utpl
|
||||
|
||||
{%-
|
||||
'use strict';
|
||||
|
||||
import { readfile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
function array_to_nftstr(array) {
|
||||
if (type(array) !== 'array')
|
||||
return array;
|
||||
|
||||
return join(', ', array);
|
||||
}
|
||||
|
||||
/* Misc config */
|
||||
const resources_dir = '/etc/homeproxy/resources';
|
||||
|
||||
/* UCI config start */
|
||||
const cfgname = 'homeproxy';
|
||||
const uci = cursor();
|
||||
uci.load(cfgname);
|
||||
|
||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china';
|
||||
let outbound_node, outbound_udp_node, routing_port,
|
||||
self_mark, redirect_port, tproxy_port, tproxy_mark,
|
||||
ipv6_support, listen_interfaces, tun_name, tun_mark;
|
||||
|
||||
const control_options = [ "lan_proxy_mode",
|
||||
"lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips",
|
||||
"lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips",
|
||||
"lan_gaming_mode_mac_addrs", "lan_gaming_mode_ipv4_ips", "lan_gaming_mode_ipv6_ips",
|
||||
"lan_global_proxy_mac_addrs", "lan_global_proxy_ipv4_ips", "lan_global_proxy_ipv6_ips",
|
||||
"wan_proxy_ipv4_ips", "wan_proxy_ipv6_ips", "wan_direct_ipv4_ips", "wan_direct_ipv6_ips" ];
|
||||
const control_info = {};
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
if (outbound_node !== 'nil') {
|
||||
self_mark = uci.get(cfgname, 'infra', 'self_mark') || '100';
|
||||
redirect_port = uci.get(cfgname, 'infra', 'redirect_port') || '5331';
|
||||
|
||||
outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
|
||||
if (outbound_udp_node !== 'nil') {
|
||||
tproxy_port = uci.get(cfgname, 'infra', 'tproxy_port') || '5332';
|
||||
tproxy_mark = uci.get(cfgname, 'infra', 'tproxy_mark') || '101';
|
||||
}
|
||||
}
|
||||
|
||||
routing_port = uci.get(cfgname, 'config', 'routing_port') || 'common';
|
||||
if (routing_port === 'common')
|
||||
routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,993,995,8080,8443,9418';
|
||||
|
||||
ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0';
|
||||
listen_interfaces = uci.get(cfgname, 'control', 'listen_interfaces');
|
||||
for (let i in control_options)
|
||||
control_info[i] = uci.get(cfgname, 'control', i);
|
||||
} else {
|
||||
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
|
||||
tun_mark = uci.get(cfgname, 'infra', 'tun_mark') || '102';
|
||||
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
||||
}
|
||||
/* UCI config end */
|
||||
-%}
|
||||
|
||||
{# Reserved addresses -#}
|
||||
set homeproxy_local_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
0.0.0.0/8,
|
||||
10.0.0.0/8,
|
||||
100.64.0.0/10,
|
||||
127.0.0.0/8,
|
||||
169.254.0.0/16,
|
||||
172.16.0.0/12,
|
||||
192.0.0.0/24,
|
||||
192.0.2.0/24,
|
||||
192.31.196.0/24,
|
||||
192.52.193.0/24,
|
||||
192.88.99.0/24,
|
||||
192.168.0.0/16,
|
||||
192.175.48.0/24,
|
||||
198.18.0.0/15,
|
||||
198.51.100.0/24,
|
||||
203.0.113.0/24,
|
||||
224.0.0.0/4,
|
||||
240.0.0.0/4
|
||||
}
|
||||
}
|
||||
{% if (routing_mode === 'custom' || ipv6_support === '1'): %}
|
||||
set homeproxy_local_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
::/128,
|
||||
::1/128,
|
||||
::ffff:0:0/96,
|
||||
100::/64,
|
||||
64:ff9b::/96,
|
||||
2001::/32,
|
||||
2001:10::/28,
|
||||
2001:20::/28,
|
||||
2001:db8::/28,
|
||||
2002::/16,
|
||||
fc00::/7,
|
||||
fe80::/10,
|
||||
ff00::/8
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
set homeproxy_gfw_list_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_gfw_list_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
{% endif %}
|
||||
{% elif (routing_mode in ['bypass_mainland_china', 'proxy_mainland_china']): %}
|
||||
set homeproxy_mainland_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
{% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), '\n')): %}
|
||||
{{ cnip4 }},
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_mainland_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = {
|
||||
{% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), '\n')): %}
|
||||
{{ cnip6 }},
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
set homeproxy_proxy_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_proxy_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{# Regular mode -#}
|
||||
{# TCP redirect -#}
|
||||
chain homeproxy_redirect_proxy {
|
||||
meta l4proto tcp counter redirect to :{{ redirect_port }}
|
||||
}
|
||||
|
||||
chain homeproxy_redirect {
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (routing_port !== 'all'): %}
|
||||
tcp dport != { {{ routing_port }} } counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['lan_proxy_mode'] === 'listed_only'): %}
|
||||
{% if (control_info['lan_proxy_mac_addrs']): %}
|
||||
ether saddr != { {{ array_to_nftstr(control_info['lan_proxy_mac_addrs']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (control_info['lan_proxy_ipv4_ips']): %}
|
||||
ip saddr != { {{ array_to_nftstr(control_info['lan_proxy_ipv4_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['lan_proxy_ipv6_ips']): %}
|
||||
ip6 saddr != { {{ array_to_nftstr(control_info['lan_proxy_ipv6_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% elif (control_info['lan_proxy_mode'] === 'except_listed'): %}
|
||||
{% if (control_info['lan_direct_mac_addrs']): %}
|
||||
ether saddr { {{ array_to_nftstr(control_info['lan_direct_mac_addrs']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (control_info['lan_proxy_ipv4_ips']): %}
|
||||
ip saddr { {{ array_to_nftstr(control_info['lan_direct_ipv4_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['lan_direct_ipv6_ips']): %}
|
||||
ip6 saddr { {{ array_to_nftstr(control_info['lan_direct_ipv6_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['wan_proxy_ipv4_ips']): %}
|
||||
ip daddr { {{ array_to_nftstr(control_info['wan_proxy_ipv4_ips']) }} } counter goto homeproxy_redirect_proxy
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['wan_proxy_ipv6_ips']): %}
|
||||
ip6 daddr { {{ array_to_nftstr(control_info['wan_proxy_ipv6_ips']) }} } counter goto homeproxy_redirect_proxy
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_proxy_addr_v4 counter goto homeproxy_redirect_proxy
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_proxy_addr_v6 counter goto homeproxy_redirect_proxy
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['lan_global_proxy_mac_addrs']): %}
|
||||
ether saddr { {{ array_to_nftstr(control_info['lan_global_proxy_mac_addrs']) }} } counter goto homeproxy_redirect_proxy
|
||||
{% endif %}
|
||||
{% if (control_info['lan_global_proxy_ipv4_ips']): %}
|
||||
ip saddr { {{ array_to_nftstr(control_info['lan_global_proxy_ipv4_ips']) }} } counter goto homeproxy_redirect_proxy
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['lan_global_proxy_ipv6_ips']): %}
|
||||
ip6 saddr { {{ array_to_nftstr(control_info['lan_global_proxy_ipv6_ips']) }} } counter goto homeproxy_redirect_proxy
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['wan_direct_ipv4_ips']): %}
|
||||
ip daddr { {{ array_to_nftstr(control_info['wan_direct_ipv4_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['wan_direct_ipv6_ips']): %}
|
||||
ip6 daddr { {{ array_to_nftstr(control_info['wan_direct_ipv6_ips']) }} } counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
ip daddr != @homeproxy_gfw_list_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif %}
|
||||
{% elif (routing_mode === 'bypass_mainland_china'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
meta l4proto tcp counter goto homeproxy_redirect_proxy
|
||||
}
|
||||
|
||||
chain homeproxy_dstnat_redir {
|
||||
{% if (listen_interfaces): %}
|
||||
meta iifname != { {{ array_to_nftstr(listen_interfaces) }} } counter return
|
||||
{% endif %}
|
||||
|
||||
meta l4proto tcp jump homeproxy_redirect
|
||||
}
|
||||
|
||||
chain homeproxy_output_redir {
|
||||
type nat hook output priority filter -105; policy accept
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
|
||||
}
|
||||
|
||||
chain dstnat {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_dstnat_redir
|
||||
}
|
||||
{% if (outbound_udp_node !== 'nil'): %}
|
||||
{# UDP tproxy #}
|
||||
chain homeproxy_mangle_tproxy {
|
||||
meta l4proto udp mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept
|
||||
{% if (ipv6_support === '1'): %}
|
||||
meta l4proto udp mark set {{ tproxy_mark }} tproxy ip6 to [::]:{{ tproxy_port }} counter accept
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_mark {
|
||||
meta l4proto udp mark set {{ tproxy_mark }} counter accept
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_prerouting {
|
||||
{% if (listen_interfaces): %}
|
||||
meta iifname != { {{ array_to_nftstr(listen_interfaces) }}, lo } counter return
|
||||
{% endif %}
|
||||
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (control_info['lan_proxy_mode'] === 'listed_only'): %}
|
||||
{% if (control_info['lan_proxy_mac_addrs']): %}
|
||||
ether saddr != { {{ array_to_nftstr(control_info['lan_proxy_mac_addrs']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (control_info['lan_proxy_ipv4_ips']): %}
|
||||
ip saddr != { {{ array_to_nftstr(control_info['lan_proxy_ipv4_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['lan_proxy_ipv6_ips']): %}
|
||||
ip6 saddr != { {{ array_to_nftstr(control_info['lan_proxy_ipv6_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% elif (control_info['lan_proxy_mode'] === 'except_listed'): %}
|
||||
{% if (control_info['lan_direct_mac_addrs']): %}
|
||||
ether saddr { {{ array_to_nftstr(control_info['lan_direct_mac_addrs']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (control_info['lan_proxy_ipv4_ips']): %}
|
||||
ip saddr { {{ array_to_nftstr(control_info['lan_direct_ipv4_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['lan_direct_ipv6_ips']): %}
|
||||
ip6 saddr { {{ array_to_nftstr(control_info['lan_direct_ipv6_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['wan_proxy_ipv4_ips']): %}
|
||||
ip daddr { {{ array_to_nftstr(control_info['wan_proxy_ipv4_ips']) }} } counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['wan_proxy_ipv6_ips']): %}
|
||||
ip6 daddr { {{ array_to_nftstr(control_info['wan_proxy_ipv6_ips']) }} } counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_proxy_addr_v4 counter goto homeproxy_mangle_tproxy
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_proxy_addr_v6 counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['lan_global_proxy_mac_addrs']): %}
|
||||
ether saddr { {{ array_to_nftstr(control_info['lan_global_proxy_mac_addrs']) }} } counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
{% if (control_info['lan_global_proxy_ipv4_ips']): %}
|
||||
ip saddr { {{ array_to_nftstr(control_info['lan_global_proxy_ipv4_ips']) }} } counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['lan_global_proxy_ipv6_ips']): %}
|
||||
ip6 saddr { {{ array_to_nftstr(control_info['lan_global_proxy_ipv6_ips']) }} } counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['wan_direct_ipv4_ips']): %}
|
||||
ip daddr { {{ array_to_nftstr(control_info['wan_direct_ipv4_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['wan_direct_ipv6_ips']): %}
|
||||
ip6 daddr { {{ array_to_nftstr(control_info['wan_direct_ipv6_ips']) }} } counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
ip daddr != @homeproxy_gfw_list_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% endif %}
|
||||
{% elif (routing_mode === 'bypass_mainland_china'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% endif %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['lan_gaming_mode_mac_addrs']): %}
|
||||
ether saddr { {{ array_to_nftstr(control_info['lan_gaming_mode_mac_addrs']) }} } counter goto homeproxy_mangle_tproxy
|
||||
ether saddr { {{ array_to_nftstr(control_info['lan_gaming_mode_mac_addrs']) }} } counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
{% if (control_info['lan_gaming_mode_ipv4_ips']): %}
|
||||
ip saddr { {{ array_to_nftstr(control_info['lan_gaming_mode_ipv4_ips']) }} } counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['lan_gaming_mode_ipv6_ips']): %}
|
||||
ip6 saddr { {{ array_to_nftstr(control_info['lan_gaming_mode_ipv6_ips']) }} } counter goto homeproxy_mangle_tproxy
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_port !== 'all'): %}
|
||||
udp dport != { {{ routing_port }} } counter return
|
||||
{% endif %}
|
||||
|
||||
meta l4proto udp counter goto homeproxy_mangle_tproxy
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_output {
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (routing_port !== 'all'): %}
|
||||
udp dport != { {{ routing_port }} } counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['wan_proxy_ipv4_ips']): %}
|
||||
ip daddr { {{ array_to_nftstr(control_info['wan_proxy_ipv4_ips']) }} } counter goto homeproxy_mangle_mark
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['wan_proxy_ipv6_ips']): %}
|
||||
ip6 daddr { {{ array_to_nftstr(control_info['wan_proxy_ipv6_ips']) }} } counter goto homeproxy_mangle_mark
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_proxy_addr_v4 counter goto homeproxy_mangle_mark
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_proxy_addr_v6 counter goto homeproxy_mangle_mark
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info['wan_direct_ipv4_ips']): %}
|
||||
ip daddr { {{ array_to_nftstr(control_info['wan_direct_ipv4_ips']) }} } counter return
|
||||
{% endif %}
|
||||
{% if (ipv6_support === '1' && control_info['wan_direct_ipv6_ips']): %}
|
||||
ip6 daddr { {{ array_to_nftstr(control_info['wan_direct_ipv6_ips']) }} } counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode === 'gfwlist'): %}
|
||||
ip daddr != @homeproxy_gfw_list_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif %}
|
||||
{% elif (routing_mode === 'bypass_mainland_china'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
meta l4proto udp counter goto homeproxy_mangle_mark
|
||||
}
|
||||
|
||||
chain mangle_prerouting {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_prerouting
|
||||
}
|
||||
|
||||
chain mangle_output {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
|
||||
}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{# TUN mode -#}
|
||||
chain homeproxy_mangle {
|
||||
meta l4proto { tcp, udp } iifname {{ tun_name }} counter return
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
meta l4proto { tcp, udp } counter mark set {{ tun_mark }}
|
||||
}
|
||||
|
||||
chain mangle_prerouting {
|
||||
meta nfproto { ipv4, ipv6 } jump homeproxy_mangle
|
||||
}
|
||||
|
||||
chain mangle_output {
|
||||
meta nfproto { ipv4, ipv6 } jump homeproxy_mangle
|
||||
}
|
||||
{% endif %}
|
43
homeproxy/root/etc/homeproxy/scripts/firewall_pre.ut
Executable file
43
homeproxy/root/etc/homeproxy/scripts/firewall_pre.ut
Executable file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/utpl -S
|
||||
|
||||
{%-
|
||||
import { cursor } from 'uci';
|
||||
|
||||
const cfgname = 'homeproxy';
|
||||
const uci = cursor();
|
||||
uci.load(cfgname);
|
||||
|
||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china';
|
||||
let outbound_node = 'nil', tun_name;
|
||||
if (routing_mode === 'custom') {
|
||||
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
||||
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
|
||||
}
|
||||
|
||||
const server_enabled = uci.get(cfgname, 'server', 'enabled');
|
||||
let auto_firewall = '0';
|
||||
if (server_enabled === '1')
|
||||
auto_firewall = uci.get(cfgname, 'server', 'auto_firewall') || '0';
|
||||
|
||||
-%}
|
||||
|
||||
{% if (outbound_node !== 'nil'): %}
|
||||
chain forward {
|
||||
oifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun forward"
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{% if (auto_firewall === '1'): %}
|
||||
chain input {
|
||||
{%
|
||||
uci.foreach(cfgname, 'server', (s) => {
|
||||
if (s.enabled !== '1')
|
||||
return;
|
||||
|
||||
let proto = s.network || '{ tcp, udp }';
|
||||
printf(' meta l4proto %s th dport %s counter accept comment "!%s: accept server %s"\n',
|
||||
proto, s.port, cfgname, s['.name']);
|
||||
});
|
||||
%}
|
||||
}
|
||||
{% endif %}
|
680
homeproxy/root/etc/homeproxy/scripts/generate_sing-box.uc
Executable file
680
homeproxy/root/etc/homeproxy/scripts/generate_sing-box.uc
Executable file
@ -0,0 +1,680 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import { executeCommand, isEmpty, strToInt, removeBlankAttrs, validateHostname } from 'homeproxy';
|
||||
import { HP_DIR, RUN_DIR } from 'homeproxy';
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
const uciinfra = 'infra',
|
||||
ucimain = 'config',
|
||||
ucicontrol = 'control';
|
||||
|
||||
const ucidnssetting = 'dns',
|
||||
ucidnsserver = 'dns_server',
|
||||
ucidnsrule = 'dns_rule';
|
||||
|
||||
const uciroutingsetting = 'routing',
|
||||
uciroutingnode = 'routing_node',
|
||||
uciroutingrule = 'routing_rule';
|
||||
|
||||
const ucinode = 'node',
|
||||
uciserver = 'server';
|
||||
|
||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
|
||||
const server_enabled = uci.get(uciconfig, uciserver, 'enabled') || '0';
|
||||
|
||||
let wan_dns = executeCommand('ifstatus wan | jsonfilter -e \'@["dns-server"][0]\'');
|
||||
if (wan_dns.exitcode === 0 && trim(wan_dns.stdout))
|
||||
wan_dns = trim(wan_dns.stdout);
|
||||
else
|
||||
wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '8.8.8.8' : '114.114.114.114';
|
||||
|
||||
const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333';
|
||||
|
||||
let main_node, main_udp_node, dedicated_udp_node, ipv6_support, default_outbound, default_interface,
|
||||
dns_server, dns_strategy, dns_default_server, dns_disable_cache, dns_disable_cache_expire,
|
||||
wan_proxy_ips, proxy_domain_list, wan_direct_ips, direct_domain_list,
|
||||
redirect_port, tproxy_port, self_mark, sniff_override, tun_name, tcpip_stack, endpoint_independent_nat;
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
||||
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
||||
redirect_port = uci.get(uciconfig, uciinfra, 'redirect_port') || '5331';
|
||||
tproxy_port = uci.get(uciconfig, uciinfra, 'tproxy_port') || '5332';
|
||||
self_mark = uci.get(uciconfig, uciinfra, 'self_mark') || '100';
|
||||
|
||||
ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0';
|
||||
default_interface = uci.get(uciconfig, ucicontrol, 'bind_interface');
|
||||
|
||||
dedicated_udp_node = !isEmpty(main_udp_node) && !(main_udp_node in ['same', main_node]);
|
||||
|
||||
dns_server = uci.get(uciconfig, ucimain, 'dns_server');
|
||||
if (isEmpty(dns_server) || dns_server === 'wan')
|
||||
dns_server = wan_dns;
|
||||
|
||||
for (let i in ['wan_proxy_ipv4_ips', 'wan_proxy_ipv6_ips']) {
|
||||
const proxy_ips = uci.get(uciconfig, ucicontrol, i);
|
||||
if (length(proxy_ips)) {
|
||||
if (!wan_proxy_ips)
|
||||
wan_proxy_ips = [];
|
||||
map(proxy_ips, (v) => push(wan_proxy_ips, v));
|
||||
}
|
||||
}
|
||||
|
||||
for (let i in ['wan_direct_ipv4_ips', 'wan_direct_ipv6_ips']) {
|
||||
const direct_ips = uci.get(uciconfig, ucicontrol, i);
|
||||
if (length(direct_ips)) {
|
||||
if (!wan_direct_ips)
|
||||
wan_direct_ips = [];
|
||||
map(direct_ips, (v) => push(wan_direct_ips, v));
|
||||
}
|
||||
}
|
||||
|
||||
proxy_domain_list = trim(readfile(HP_DIR + '/resources/proxy_list.txt'));
|
||||
direct_domain_list = trim(readfile(HP_DIR + '/resources/direct_list.txt'));
|
||||
if (proxy_domain_list)
|
||||
proxy_domain_list = split(proxy_domain_list, /[\r\n]/);
|
||||
if (direct_domain_list)
|
||||
direct_domain_list = split(direct_domain_list, /[\r\n]/);
|
||||
} else {
|
||||
/* DNS settings */
|
||||
dns_strategy = uci.get(uciconfig, ucidnssetting, 'dns_strategy');
|
||||
dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server');
|
||||
dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache');
|
||||
dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
|
||||
|
||||
/* Routing settings */
|
||||
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
|
||||
default_interface = uci.get(uciconfig, uciroutingsetting, 'default_interface');
|
||||
sniff_override = uci.get(uciconfig, uciroutingsetting, 'sniff_override');
|
||||
tun_name = uci.get(uciconfig, uciinfra, 'tun_name') || 'singtun0';
|
||||
tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'gvisor';
|
||||
endpoint_independent_nat = uci.get(uciconfig, uciroutingsetting, 'endpoint_independent_nat');
|
||||
}
|
||||
/* UCI config end */
|
||||
|
||||
/* Config helper start */
|
||||
function generate_outbound(node) {
|
||||
if (type(node) !== 'object' || isEmpty(node))
|
||||
return null;
|
||||
|
||||
const outbound = {
|
||||
type: node.type,
|
||||
tag: 'cfg-' + node['.name'] + '-out',
|
||||
|
||||
server: (node.type !== 'direct') ? node.address : null,
|
||||
server_port: (node.type !== 'direct') ? int(node.port) : null,
|
||||
|
||||
username: node.username,
|
||||
password: node.password,
|
||||
|
||||
/* Direct */
|
||||
override_address: (node.type === 'direct') ? node.address : null,
|
||||
override_port: (node.type === 'direct') ? node.port : null,
|
||||
proxy_protocol: strToInt(node.proxy_protocol),
|
||||
/* Hysteria */
|
||||
up_mbps: strToInt(node.hysteria_down_mbps),
|
||||
down_mbps: strToInt(node.hysteria_down_mbps),
|
||||
obfs: node.hysteria_bofs_password,
|
||||
auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null,
|
||||
auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null,
|
||||
recv_window_conn: strToInt(node.hysteria_recv_window_conn),
|
||||
recv_window: strToInt(node.hysteria_revc_window),
|
||||
disable_mtu_discovery: (node.hysteria_disable_mtu_discovery === '1') || null,
|
||||
/* Shadowsocks */
|
||||
method: node.shadowsocks_encrypt_method || node.shadowsocksr_encrypt_method,
|
||||
plugin: node.shadowsocks_plugin,
|
||||
plugin_opts: node.shadowsocks_plugin_opts,
|
||||
/* ShadowsocksR */
|
||||
protocol: node.shadowsocksr_protocol,
|
||||
protocol_param: node.shadowsocksr_protocol_param,
|
||||
obfs: node.shadowsocksr_obfs,
|
||||
obfs_param: node.shadowsocksr_obfs_param,
|
||||
/* ShadowTLS / Socks */
|
||||
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : null),
|
||||
/* VLESS / VMess */
|
||||
uuid: node.uuid,
|
||||
alter_id: node.vmess_alterid,
|
||||
security: node.vmess_encrypt,
|
||||
global_padding: node.vmess_global_padding ? (node.vmess_global_padding === '1') : null,
|
||||
authenticated_length: node.vmess_authenticated_length ? (node.vmess_authenticated_length === '1') : null,
|
||||
packet_encoding: node.packet_encoding,
|
||||
/* WireGuard */
|
||||
system_interface: (node.type === 'wireguard') || null,
|
||||
interface_name: (node.type === 'wireguard') ? "singwg-cfg-" + node['.name'] + "-out" : null,
|
||||
local_address: node.wireguard_local_address,
|
||||
private_key: node.wireguard_private_key,
|
||||
peer_public_key: node.wireguard_peer_public_key,
|
||||
pre_shared_key: node.wireguard_pre_shared_key,
|
||||
mtu: node.wireguard_mtu,
|
||||
|
||||
multiplex: (node.multiplex === '1') ? {
|
||||
enabled: true,
|
||||
protocol: node.multiplex_protocol,
|
||||
max_connections: node.multiplex_max_connections,
|
||||
min_streams: node.multiplex_min_streams,
|
||||
max_streams: node.multiplex_max_streams
|
||||
} : null,
|
||||
tls: (node.tls === '1') ? {
|
||||
enabled: true,
|
||||
server_name: node.tls_sni,
|
||||
insecure: (node.tls_insecure === '1'),
|
||||
alpn: node.tls_alpn,
|
||||
min_version: node.tls_min_version,
|
||||
max_version: node.tls_max_version,
|
||||
cipher_suites: node.tls_cipher_suites,
|
||||
certificate_path: node.tls_cert_path,
|
||||
ech: (node.enable_ech === '1') ? {
|
||||
enabled: true,
|
||||
dynamic_record_sizing_disabled: (node.tls_ech_tls_disable_drs === '1'),
|
||||
pq_signature_schemes_enabled: (node.tls_ech_enable_pqss === '1'),
|
||||
config: node.tls_ech_config
|
||||
} : null,
|
||||
utls: !isEmpty(node.tls_utls) ? {
|
||||
enabled: true,
|
||||
fingerprint: node.tls_utls
|
||||
} : null
|
||||
} : null,
|
||||
transport: !isEmpty(node.transport) ? {
|
||||
type: node.transport,
|
||||
host: node.http_host || node.ws_host,
|
||||
path: node.http_path || node.ws_path,
|
||||
method: node.http_method,
|
||||
max_early_data: node.websocket_early_data,
|
||||
early_data_header_name: node.websocket_early_data_header,
|
||||
service_name: node.grpc_servicename
|
||||
} : null,
|
||||
udp_over_tcp: (node.udp_over_tcp === '1') || null,
|
||||
tcp_fast_open: (node.tcp_fast_open === '1') || null,
|
||||
udp_fragment: (node.udp_fragment === '1') || null
|
||||
};
|
||||
|
||||
return outbound;
|
||||
}
|
||||
|
||||
function get_outbound(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
if (cfg in ['direct-out', 'black-out'])
|
||||
return cfg;
|
||||
else {
|
||||
const node = uci.get(uciconfig, cfg, 'node');
|
||||
if (isEmpty(node))
|
||||
die(sprintf("%s's node is missing, please check your configuration.", cfg));
|
||||
else
|
||||
return 'cfg-' + node + '-out';
|
||||
}
|
||||
}
|
||||
|
||||
function get_resolver(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
if (cfg in ['default-dns', 'block-dns'])
|
||||
return cfg;
|
||||
else
|
||||
return 'cfg-' + cfg + '-dns';
|
||||
}
|
||||
|
||||
function parse_port(strport) {
|
||||
if (type(strport) !== 'array' || isEmpty(strport))
|
||||
return null;
|
||||
|
||||
let ports = [];
|
||||
for (let i in strport)
|
||||
push(ports, int(i));
|
||||
|
||||
return ports;
|
||||
|
||||
}
|
||||
/* Config helper end */
|
||||
|
||||
const config = {};
|
||||
|
||||
/* Log */
|
||||
config.log = {
|
||||
disabled: false,
|
||||
level: 'warn',
|
||||
output: RUN_DIR + '/sing-box.log',
|
||||
timestamp: true
|
||||
};
|
||||
|
||||
/* DNS start */
|
||||
/* Default settings */
|
||||
config.dns = {
|
||||
servers: [
|
||||
{
|
||||
tag: 'default-dns',
|
||||
address: wan_dns,
|
||||
detour: 'direct-out'
|
||||
},
|
||||
{
|
||||
tag: 'block-dns',
|
||||
address: 'rcode://name_error'
|
||||
}
|
||||
],
|
||||
rules: [],
|
||||
strategy: dns_strategy,
|
||||
disable_cache: (dns_disable_cache === '1'),
|
||||
disable_expire: (dns_disable_cache_expire === '1')
|
||||
};
|
||||
|
||||
if (!isEmpty(main_node)) {
|
||||
/* Avoid DNS loop */
|
||||
const main_node_addr = uci.get(uciconfig, main_node, 'address');
|
||||
if (validateHostname(main_node_addr))
|
||||
push(config.dns.rules, {
|
||||
domain: main_node_addr,
|
||||
server: 'default-dns'
|
||||
});
|
||||
|
||||
if (dedicated_udp_node) {
|
||||
const main_udp_node_addr = uci.get(uciconfig, main_udp_node, 'address');
|
||||
if (validateHostname(main_udp_node_addr))
|
||||
push(config.dns.rules, {
|
||||
domain: main_udp_node_addr,
|
||||
server: 'default-dns'
|
||||
});
|
||||
}
|
||||
|
||||
if (direct_domain_list)
|
||||
push(config.dns.rules, {
|
||||
domain_keyword: direct_domain_list,
|
||||
server: 'default-dns'
|
||||
});
|
||||
|
||||
if (isEmpty(config.dns.rules))
|
||||
config.dns.rules = null;
|
||||
|
||||
let default_final_dns = 'default-dns';
|
||||
/* Main DNS */
|
||||
if (dns_server !== wan_dns) {
|
||||
push(config.dns.servers, {
|
||||
tag: 'main-dns',
|
||||
address: dns_server,
|
||||
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null,
|
||||
detour: 'main-out'
|
||||
});
|
||||
|
||||
default_final_dns = 'main-dns';
|
||||
}
|
||||
|
||||
config.dns.final = default_final_dns;
|
||||
} else if (!isEmpty(default_outbound)) {
|
||||
/* DNS servers */
|
||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
push(config.dns.servers, {
|
||||
tag: 'cfg-' + cfg['.name'] + '-dns',
|
||||
address: cfg.address,
|
||||
address: cfg.address,
|
||||
address_resolver: get_resolver(cfg.address_resolver),
|
||||
address_strategy: cfg.address_strategy,
|
||||
strategy: cfg.resolve_strategy,
|
||||
detour: get_outbound(cfg.outbound)
|
||||
});
|
||||
});
|
||||
|
||||
/* DNS rules */
|
||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
push(config.dns.rules, {
|
||||
invert: cfg.invert,
|
||||
network: cfg.network,
|
||||
protocol: cfg.protocol,
|
||||
domain: cfg.domain,
|
||||
domain_suffix: cfg.domain_suffix,
|
||||
domain_keyword: cfg.domain_keyword,
|
||||
domain_regex: cfg.domain_regex,
|
||||
geosite: cfg.geosite,
|
||||
source_geoip: cfg.source_geoip,
|
||||
source_ip_cidr: cfg.source_ip_cidr,
|
||||
source_port: parse_port(cfg.source_port),
|
||||
source_port_range: cfg.source_port_range,
|
||||
port: parse_port(cfg.port),
|
||||
port_range: cfg.port_range,
|
||||
process_name: cfg.process_name,
|
||||
process_path: cfg.process_path,
|
||||
user: cfg.user,
|
||||
invert: (cfg.invert === '1'),
|
||||
outbound: get_outbound(cfg.outbound),
|
||||
server: get_resolver(cfg.server),
|
||||
disable_cache: (cfg.disable_cache === '1')
|
||||
});
|
||||
});
|
||||
|
||||
if (isEmpty(config.dns.rules))
|
||||
config.dns.rules = null;
|
||||
|
||||
config.dns.final = get_resolver(dns_default_server);
|
||||
}
|
||||
/* DNS end */
|
||||
|
||||
/* Inbound start */
|
||||
config.inbounds = [];
|
||||
|
||||
if (!isEmpty(main_node) || !isEmpty(default_outbound)) {
|
||||
push(config.inbounds, {
|
||||
type: 'direct',
|
||||
tag: 'dns-in',
|
||||
listen: '::',
|
||||
listen_port: int(dns_port)
|
||||
});
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
push(config.inbounds, {
|
||||
type: 'redirect',
|
||||
tag: 'redirect-in',
|
||||
|
||||
listen: '::',
|
||||
listen_port: int(redirect_port),
|
||||
sniff: true,
|
||||
sniff_override_destination: true
|
||||
});
|
||||
|
||||
if (!isEmpty(main_udp_node))
|
||||
push(config.inbounds, {
|
||||
type: 'tproxy',
|
||||
tag: 'tproxy-in',
|
||||
|
||||
listen: "::",
|
||||
listen_port: int(tproxy_port),
|
||||
network: 'udp',
|
||||
sniff: true,
|
||||
sniff_override_destination: true
|
||||
});
|
||||
} else {
|
||||
push(config.inbounds, {
|
||||
type: 'tun',
|
||||
tag: 'tun-in',
|
||||
|
||||
interface_name: tun_name,
|
||||
inet4_address: '172.19.0.1/30',
|
||||
inet6_address: 'fdfe:dcba:9876::1/126',
|
||||
mtu: 9000,
|
||||
auto_route: false,
|
||||
endpoint_independent_nat: (endpoint_independent_nat === '1') || null,
|
||||
stack: tcpip_stack,
|
||||
sniff: true,
|
||||
sniff_override_destination: (sniff_override === '1'),
|
||||
domain_strategy: dns_strategy
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (server_enabled === '1')
|
||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
push(config.inbounds, {
|
||||
type: cfg.type,
|
||||
tag: 'cfg-' + cfg['.name'] + '-in',
|
||||
|
||||
listen: '::',
|
||||
listen_port: strToInt(cfg.port),
|
||||
tcp_fast_open: (cfg.tcp_fast_open === '1') || null,
|
||||
udp_fragment: (cfg.udp_fragment === '1') || null,
|
||||
sniff: true,
|
||||
sniff_override_destination: (cfg.sniff_override === '1'),
|
||||
domain_strategy: cfg.domain_strategy,
|
||||
proxy_protocol: (cfg.proxy_protocol === '1') || null,
|
||||
proxy_protocol_accept_no_header: (cfg.proxy_protocol_accept_no_header === '1') || null,
|
||||
network: cfg.network,
|
||||
|
||||
/* Hysteria */
|
||||
up_mbps: strToInt(cfg.hysteria_up_mbps),
|
||||
down_mbps: strToInt(cfg.hysteria_down_mbps),
|
||||
obfs: cfg.hysteria_obfs_password,
|
||||
auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
|
||||
auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
|
||||
recv_window_conn: strToInt(cfg.hysteria_recv_window_conn),
|
||||
recv_window_client: strToInt(cfg.hysteria_revc_window_client),
|
||||
max_conn_client: strToInt(cfg.hysteria_max_conn_client),
|
||||
disable_mtu_discovery: (cfg.hysteria_disable_mtu_discovery === '1') || null,
|
||||
|
||||
/* Shadowsocks */
|
||||
method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
|
||||
password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
|
||||
|
||||
/* ShadowTLS */
|
||||
version: (cfg.type === 'shadowtls') ? strToInt(cfg.shadowtls_version) : null,
|
||||
|
||||
/* HTTP / Socks / Trojan / VMess */
|
||||
users: (cfg.type !== 'shadowsocks') ? [
|
||||
{
|
||||
name: (cfg.type in ['trojan', 'vmess']) ? 'cfg-' + cfg['.name'] + '-server' : null,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
uuid: cfg.uuid,
|
||||
alterId: strToInt(cfg.vmess_alterid)
|
||||
}
|
||||
] : null,
|
||||
|
||||
tls: (cfg.tls === '1') ? {
|
||||
enabled: true,
|
||||
server_name: cfg.tls_sni,
|
||||
alpn: cfg.tls_alpn,
|
||||
min_version: cfg.tls_min_version,
|
||||
max_version: cfg.tls_max_version,
|
||||
cipher_suites: cfg.tls_cipher_suites,
|
||||
certificate_path: cfg.tls_cert_path,
|
||||
key_path: cfg.tls_key_path,
|
||||
acme: (cfg.tls_acme === '1') ? {
|
||||
domain: cfg.tls_acme_domains,
|
||||
data_directory: HP_DIR + '/certs',
|
||||
default_server_name: cfg.tls_acme_dsn,
|
||||
email: cfg.tls_acme_email,
|
||||
provider: cfg.tls_acme_provider,
|
||||
disable_http_challenge: (cfg.tls_acme_dhc === '1'),
|
||||
disable_tls_alpn_challenge: (cfg.tls_acme_dtac === '1'),
|
||||
alternative_http_port: strToInt(cfg.tls_acme_ahp),
|
||||
alternative_tls_port: strToInt(cfg.tls_acme_atp),
|
||||
external_account: (cfg.tls_acme_external_account === '1') ? {
|
||||
key_id: cfg.tls_acme_ea_keyid,
|
||||
mac_key: cfg.tls_acme_ea_mackey
|
||||
} : null
|
||||
} : null
|
||||
} : null,
|
||||
|
||||
transport: !isEmpty(cfg.transport) ? {
|
||||
type: cfg.transport,
|
||||
host: cfg.http_host || cfg.ws_host,
|
||||
path: cfg.http_path || cfg.ws_path,
|
||||
method: cfg.http_method,
|
||||
max_early_data: cfg.websocket_early_data,
|
||||
early_data_header_name: cfg.websocket_early_data_header,
|
||||
service_name: cfg.grpc_servicename
|
||||
} : null
|
||||
});
|
||||
});
|
||||
|
||||
/* Inbound end */
|
||||
|
||||
/* Outbound start */
|
||||
/* Default outbounds */
|
||||
config.outbounds = [
|
||||
{
|
||||
type: "direct",
|
||||
tag: "direct-out",
|
||||
},
|
||||
{
|
||||
type: "block",
|
||||
tag: "block-out"
|
||||
},
|
||||
{
|
||||
type: "dns",
|
||||
tag: "dns-out"
|
||||
}
|
||||
];
|
||||
|
||||
/* Main outbounds */
|
||||
if (!isEmpty(main_node)) {
|
||||
config.outbounds[0].routing_mark = int(self_mark);
|
||||
|
||||
const main_node_cfg = uci.get_all(uciconfig, main_node) || {};
|
||||
push(config.outbounds, generate_outbound(main_node_cfg));
|
||||
config.outbounds[length(config.outbounds)-1].routing_mark = int(self_mark);
|
||||
config.outbounds[length(config.outbounds)-1].tag = 'main-out';
|
||||
|
||||
if (dedicated_udp_node) {
|
||||
const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {};
|
||||
push(config.outbounds, generate_outbound(main_udp_node_cfg));
|
||||
config.outbounds[length(config.outbounds)-1].routing_mark = int(self_mark);
|
||||
config.outbounds[length(config.outbounds)-1].tag = 'main-udp-out';
|
||||
}
|
||||
|
||||
} else if (!isEmpty(default_outbound))
|
||||
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
const outbound = uci.get_all(uciconfig, cfg.node) || {};
|
||||
push(config.outbounds, generate_outbound(outbound));
|
||||
config.outbounds[length(config.outbounds)-1].domain_strategy = cfg.domain_strategy;
|
||||
config.outbounds[length(config.outbounds)-1].bind_interface = cfg.bind_interface;
|
||||
config.outbounds[length(config.outbounds)-1].detour = get_outbound(cfg.outbound);
|
||||
});
|
||||
/* Outbound end */
|
||||
|
||||
/* Routing rules start */
|
||||
/* Default settings */
|
||||
if (!isEmpty(main_node) || !isEmpty(default_outbound))
|
||||
config.route = {
|
||||
geoip: {
|
||||
path: HP_DIR + '/resources/geoip.db',
|
||||
download_url: 'https://github.com/1715173329/sing-geoip/releases/latest/download/geoip.db',
|
||||
download_detour: get_outbound(default_outbound) || (routing_mode !== 'proxy_mainland_china' && !isEmpty(main_node)) ? 'main-out' : 'direct-out'
|
||||
},
|
||||
geosite: {
|
||||
path: HP_DIR + '/resources/geosite.db',
|
||||
download_url: 'https://github.com/1715173329/sing-geosite/releases/latest/download/geosite.db',
|
||||
download_detour: get_outbound(default_outbound) || (routing_mode !== 'proxy_mainland_china' && !isEmpty(main_node)) ? 'main-out' : 'direct-out'
|
||||
},
|
||||
rules: [
|
||||
{
|
||||
inbound: 'dns-in',
|
||||
outbound: 'dns-out'
|
||||
},
|
||||
{
|
||||
protocol: 'dns',
|
||||
outbound: 'dns-out'
|
||||
}
|
||||
],
|
||||
auto_detect_interface: isEmpty(default_interface) ? true : null,
|
||||
default_interface: default_interface
|
||||
};
|
||||
|
||||
if (!isEmpty(main_node)) {
|
||||
/* Routing rules */
|
||||
/* Proxy list */
|
||||
if (length(proxy_domain_list) || length(wan_proxy_ips)) {
|
||||
push(config.route.rules, {
|
||||
domain_keyword: proxy_domain_list,
|
||||
ip_cidr: wan_proxy_ips,
|
||||
network: dedicated_udp_node ? 'tcp' : null,
|
||||
outbound: 'main-out'
|
||||
});
|
||||
|
||||
if (dedicated_udp_node) {
|
||||
push(config.route.rules, {
|
||||
domain_keyword: proxy_domain_list,
|
||||
ip_cidr: wan_proxy_ips,
|
||||
network: 'udp',
|
||||
outbound: 'main-udp-out'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* Direct list */
|
||||
if (length(direct_domain_list) || length(wan_direct_ips))
|
||||
push(config.route.rules, {
|
||||
domain_keyword: direct_domain_list,
|
||||
ip_cidr: wan_direct_ips,
|
||||
outbound: 'direct-out'
|
||||
});
|
||||
|
||||
let routing_geosite;
|
||||
if (routing_mode === 'gfwlist') {
|
||||
routing_geosite = [ 'gfw', 'greatfire' ];
|
||||
|
||||
push(config.route.rules, {
|
||||
geosite: routing_geosite,
|
||||
network: dedicated_udp_node ? 'tcp' : null,
|
||||
outbound: 'main-out'
|
||||
});
|
||||
} else if (routing_mode in ['bypass_mainland_china', 'proxy_mainland_china']) {
|
||||
/* Check CN traffic, in case of dirty nftset table */
|
||||
push(config.route.rules, {
|
||||
geosite: [ 'cn' ],
|
||||
geoip: [ 'cn' ],
|
||||
invert: (routing_mode === 'proxy_mainland_china') ? true : null,
|
||||
outbound: 'direct-out'
|
||||
});
|
||||
}
|
||||
|
||||
/* Main UDP out */
|
||||
if (dedicated_udp_node)
|
||||
push(config.route.rules, {
|
||||
geosite: routing_geosite,
|
||||
network: 'udp',
|
||||
outbound: 'main-udp-out'
|
||||
});
|
||||
|
||||
config.route.final = (routing_mode === 'gfwlist') ? 'direct-out' : 'main-out';
|
||||
} else if (!isEmpty(default_outbound)) {
|
||||
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return null;
|
||||
|
||||
push(config.route.rules, {
|
||||
invert: cfg.invert,
|
||||
ip_version: cfg.ip_version,
|
||||
network: cfg.network,
|
||||
protocol: cfg.protocol,
|
||||
domain: cfg.domain,
|
||||
domain_suffix: cfg.domain_suffix,
|
||||
domain_keyword: cfg.domain_keyword,
|
||||
domain_regex: cfg.domain_regex,
|
||||
geosite: cfg.geosite,
|
||||
source_geoip: cfg.source_geoip,
|
||||
geoip: cfg.geoip,
|
||||
source_ip_cidr: cfg.source_ip_cidr,
|
||||
ip_cidr: cfg.ip_cidr,
|
||||
source_port: parse_port(cfg.source_port),
|
||||
source_port_range: cfg.source_port_range,
|
||||
port: parse_port(cfg.port),
|
||||
port_range: cfg.port_range,
|
||||
process_name: cfg.process_name,
|
||||
process_path: cfg.process_path,
|
||||
user: cfg.user,
|
||||
invert: (cfg.invert === '1'),
|
||||
outbound: get_outbound(cfg.outbound)
|
||||
});
|
||||
});
|
||||
|
||||
config.route.final = get_outbound(default_outbound);
|
||||
}
|
||||
/* Routing rules end */
|
||||
|
||||
system('mkdir -p ' + RUN_DIR);
|
||||
writefile(RUN_DIR + '/sing-box.json', sprintf('%.J\n', removeBlankAttrs(config)));
|
213
homeproxy/root/etc/homeproxy/scripts/homeproxy.uc
Normal file
213
homeproxy/root/etc/homeproxy/scripts/homeproxy.uc
Normal file
@ -0,0 +1,213 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
import { mkstemp } from 'fs';
|
||||
import { urldecode, urldecode_params } from 'luci.http';
|
||||
|
||||
/* Global variables start */
|
||||
export const HP_DIR = '/etc/homeproxy';
|
||||
export const RUN_DIR = '/var/run/homeproxy';
|
||||
/* Global variables end */
|
||||
|
||||
/* Utilities start */
|
||||
/* Kanged from luci-app-commands */
|
||||
export function shellQuote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
};
|
||||
|
||||
export function isBinary(str) {
|
||||
for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off))
|
||||
if (byte <= 8 || (byte >= 14 && byte <= 31))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export function executeCommand(...args) {
|
||||
let outfd = mkstemp();
|
||||
let errfd = mkstemp();
|
||||
|
||||
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
|
||||
|
||||
outfd.seek(0);
|
||||
errfd.seek(0);
|
||||
|
||||
const stdout = outfd.read(1024 * 512) ?? '';
|
||||
const stderr = errfd.read(1024 * 512) ?? '';
|
||||
|
||||
outfd.close();
|
||||
errfd.close();
|
||||
|
||||
const binary = isBinary(stdout);
|
||||
|
||||
return {
|
||||
command: join(' ', args),
|
||||
stdout: binary ? null : stdout,
|
||||
stderr,
|
||||
exitcode,
|
||||
binary
|
||||
};
|
||||
};
|
||||
|
||||
export function calcStringMD5(str) {
|
||||
if (!str || type(str) !== 'string')
|
||||
return null;
|
||||
|
||||
const output = executeCommand(`/bin/echo -n ${shellQuote(str)} | /usr/bin/md5sum | /usr/bin/awk '{print $1}'`) || {};
|
||||
return trim(output.stdout);
|
||||
};
|
||||
|
||||
export function CURL(url) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
const output = executeCommand(`/usr/bin/curl -fsL --connect-timeout '10' --retry '3' ${shellQuote(url)}`) || {};
|
||||
return trim(output.stdout);
|
||||
};
|
||||
/* Utilities end */
|
||||
|
||||
/* String helper start */
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
|
||||
export function strToInt(str) {
|
||||
return !isEmpty(str) ? int(str) || null : null;
|
||||
};
|
||||
|
||||
export function removeBlankAttrs(res) {
|
||||
let content;
|
||||
|
||||
if (type(res) === 'object') {
|
||||
content = {};
|
||||
map(keys(res), (k) => {
|
||||
if (type(res[k]) in ['array', 'object'])
|
||||
content[k] = removeBlankAttrs(res[k]);
|
||||
else if (res[k] !== null && res[k] !== '')
|
||||
content[k] = res[k];
|
||||
});
|
||||
} else if (type(res) === 'array') {
|
||||
content = [];
|
||||
map(res, (k, i) => {
|
||||
if (type(k) in ['array', 'object'])
|
||||
push(content, removeBlankAttrs(k));
|
||||
else if (k !== null && k !== '')
|
||||
push(content, k);
|
||||
});
|
||||
} else
|
||||
return res;
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export function validateHostname(hostname) {
|
||||
return (match(hostname, /^[a-zA-Z0-9_]+$/) != null ||
|
||||
(match(hostname, /^[a-zA-Z0-9_][a-zA-Z0-9_%-.]*[a-zA-Z0-9]$/) &&
|
||||
match(hostname, /[^0-9.]/)));
|
||||
};
|
||||
|
||||
export function validation(datatype, data) {
|
||||
if (!datatype || !data)
|
||||
return null;
|
||||
|
||||
const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`);
|
||||
return ret;
|
||||
};
|
||||
/* String helper end */
|
||||
|
||||
/* String parser start */
|
||||
export function decodeBase64Str(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
str = trim(str);
|
||||
str = replace(str, '_', '/');
|
||||
str = replace(str, '-', '+');
|
||||
|
||||
const padding = length(str) % 4;
|
||||
if (padding)
|
||||
str = str + substr('====', padding);
|
||||
|
||||
return b64dec(str);
|
||||
};
|
||||
|
||||
export function parseURL(url) {
|
||||
if (type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
const services = {
|
||||
http: '80',
|
||||
https: '443'
|
||||
};
|
||||
|
||||
const objurl = {};
|
||||
|
||||
objurl.href = url;
|
||||
|
||||
url = replace(url, /#(.+)$/, (_, val) => {
|
||||
objurl.hash = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /^(\w[A-Za-z0-9\+\-\.]+):/, (_, val) => {
|
||||
objurl.protocol = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /\?(.+)/, (_, val) => {
|
||||
objurl.search = val;
|
||||
objurl.searchParams = urldecode_params(val);
|
||||
return '';
|
||||
});
|
||||
|
||||
url = replace(url, /^\/\/([^\/]+)/, (_, val) => {
|
||||
val = replace(val, /^([^@]+)@/, (_, val) => {
|
||||
objurl.userinfo = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
val = replace(val, /:(\d+)$/, (_, val) => {
|
||||
objurl.port = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (validation('ip4addr', val) === 0 ||
|
||||
validation('ip6addr', replace(val, /\[|\]/g, '')) === 0 ||
|
||||
validation('hostname', val) === 0)
|
||||
objurl.hostname = val;
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
objurl.pathname = url ?? '/';
|
||||
|
||||
if (!objurl.protocol || !objurl.hostname)
|
||||
return null;
|
||||
|
||||
if (objurl.userinfo) {
|
||||
objurl.userinfo = replace(objurl.userinfo, /:([^:]+)$/, (_, val) => {
|
||||
objurl.password = val;
|
||||
return '';
|
||||
});
|
||||
|
||||
if (match(objurl.userinfo, /^[A-Za-z0-9\+\-\_\.]+$/)) {
|
||||
objurl.username = objurl.userinfo;
|
||||
delete objurl.userinfo;
|
||||
} else {
|
||||
delete objurl.userinfo;
|
||||
delete objurl.password;
|
||||
}
|
||||
};
|
||||
|
||||
if (!objurl.port)
|
||||
objurl.port = services[objurl.protocol];
|
||||
|
||||
objurl.host = objurl.hostname + (objurl.port ? `:${objurl.port}` : '');
|
||||
objurl.origin = `${objurl.protocol}://${objurl.host}`;
|
||||
|
||||
return objurl;
|
||||
};
|
||||
/* String parser start */
|
12
homeproxy/root/etc/homeproxy/scripts/update_crond.sh
Executable file
12
homeproxy/root/etc/homeproxy/scripts/update_crond.sh
Executable file
@ -0,0 +1,12 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
SCRIPTS_DIR="/etc/homeproxy/scripts"
|
||||
|
||||
for i in "geoip" "geosite" "china_ip4" "china_ip6" "gfw_list" "china_list"; do
|
||||
"$SCRIPTS_DIR"/update_resources.sh "$i"
|
||||
done
|
||||
|
||||
"$SCRIPTS_DIR"/update_subscriptions.uc
|
155
homeproxy/root/etc/homeproxy/scripts/update_resources.sh
Executable file
155
homeproxy/root/etc/homeproxy/scripts/update_resources.sh
Executable file
@ -0,0 +1,155 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
NAME="homeproxy"
|
||||
|
||||
RESOURCES_DIR="/etc/$NAME/resources"
|
||||
mkdir -p "$RESOURCES_DIR"
|
||||
|
||||
RUN_DIR="/var/run/$NAME"
|
||||
LOG_PATH="$RUN_DIR/$NAME.log"
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
log() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") $*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
set_lock() {
|
||||
local act="$1"
|
||||
local type="$2"
|
||||
|
||||
local lock="$RUN_DIR/update_resources-$type.lock"
|
||||
if [ "$act" = "set" ]; then
|
||||
if [ -e "$lock" ]; then
|
||||
log "[$(to_upper "$type")] A task is already running."
|
||||
exit 2
|
||||
else
|
||||
touch "$lock"
|
||||
fi
|
||||
elif [ "$act" = "remove" ]; then
|
||||
rm -f "$lock"
|
||||
fi
|
||||
}
|
||||
|
||||
to_upper() {
|
||||
echo -e "$1" | tr "[a-z]" "[A-Z]"
|
||||
}
|
||||
|
||||
check_geodata_update() {
|
||||
local geotype="$1"
|
||||
local georepo="$2"
|
||||
local curl="curl --connect-timeout 5 -fsSL"
|
||||
|
||||
set_lock "set" "$geotype"
|
||||
|
||||
local geodata_ver="$($curl "https://api.github.com/repos/$georepo/releases/latest" | jsonfilter -e "@.tag_name")"
|
||||
if [ -z "$geodata_ver" ]; then
|
||||
log "[$(to_upper "$geotype")] Failed to get the latest version, please retry later."
|
||||
|
||||
set_lock "remove" "$geotype"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local local_geodata_ver="$(cat "$RESOURCES_DIR/$geotype.ver" 2>"/dev/null" || echo "NOT FOUND")"
|
||||
if [ "$local_geodata_ver" = "$geodata_ver" ]; then
|
||||
log "[$(to_upper "$geotype")] Current version: $geodata_ver."
|
||||
log "[$(to_upper "$geotype")] You're already at the latest version."
|
||||
|
||||
set_lock "remove" "$geotype"
|
||||
return 3
|
||||
else
|
||||
log "[$(to_upper "$geotype")] Local version: $local_geodata_ver, latest version: $geodata_ver."
|
||||
fi
|
||||
|
||||
local geodata_hash
|
||||
$curl "https://github.com/$georepo/releases/download/$geodata_ver/$geotype.db" -o "$RUN_DIR/$geotype.db"
|
||||
geodata_hash="$($curl "https://github.com/$georepo/releases/download/$geodata_ver/$geotype.db.sha256sum" | awk '{print $1}')"
|
||||
if ! echo -e "$geodata_hash $RUN_DIR/$geotype.db" | sha256sum -s -c -; then
|
||||
rm -f "$RUN_DIR/$geotype.db"
|
||||
log "[$(to_upper "$geotype")] Update failed."
|
||||
|
||||
set_lock "remove" "$geotype"
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -f "$RUN_DIR/$geotype.db" "$RESOURCES_DIR/$geotype.db"
|
||||
echo -e "$geodata_ver" > "$RESOURCES_DIR/$geotype.ver"
|
||||
log "[$(to_upper "$geotype")] Successfully updated."
|
||||
|
||||
set_lock "remove" "$geotype"
|
||||
return 0
|
||||
}
|
||||
|
||||
check_list_update() {
|
||||
local listtype="$1"
|
||||
local listrepo="$2"
|
||||
local listref="$3"
|
||||
local listname="$4"
|
||||
local curl="curl --connect-timeout 5 -fsSL"
|
||||
|
||||
set_lock "set" "$listtype"
|
||||
|
||||
local list_info="$($curl "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname")"
|
||||
local list_sha="$(echo -e "$list_info" | jsonfilter -e "@[0].sha")"
|
||||
local list_ver="$(echo -e "$list_info" | jsonfilter -e "@[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."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
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."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
return 3
|
||||
else
|
||||
log "[$(to_upper "$listtype")] Local version: $local_list_ver, latest version: $list_ver."
|
||||
fi
|
||||
|
||||
$curl "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -o "$RUN_DIR/$listname"
|
||||
if [ ! -s "$RUN_DIR/$listname" ]; then
|
||||
rm -f "$RUN_DIR/$listname"
|
||||
log "[$(to_upper "$listtype")] Update failed."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
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."
|
||||
|
||||
set_lock "remove" "$listtype"
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"geoip")
|
||||
check_geodata_update "$1" "1715173329/sing-geoip"
|
||||
;;
|
||||
"geosite")
|
||||
check_geodata_update "$1" "1715173329/sing-geosite"
|
||||
;;
|
||||
"china_ip4")
|
||||
check_list_update "$1" "gaoyifan/china-operator-ip" "ip-lists" "china.txt"
|
||||
;;
|
||||
"china_ip6")
|
||||
check_list_update "$1" "gaoyifan/china-operator-ip" "ip-lists" "china6.txt"
|
||||
;;
|
||||
"gfw_list")
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
|
||||
;;
|
||||
"china_list")
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt"
|
||||
;;
|
||||
*)
|
||||
echo -e "Usage: $0 <geoip / geosite / china_ip4 / china_ip6 / gfw_list / china_list>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
526
homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc
Executable file
526
homeproxy/root/etc/homeproxy/scripts/update_subscriptions.uc
Executable file
@ -0,0 +1,526 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { open } from 'fs';
|
||||
import { connect } from 'ubus';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import { urldecode, urlencode, urldecode_params } from 'luci.http';
|
||||
import { init_action } from 'luci.sys';
|
||||
|
||||
import {
|
||||
calcStringMD5, CURL, executeCommand, decodeBase64Str,
|
||||
isEmpty, parseURL, removeBlankAttrs, validation,
|
||||
HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
const ucimain = 'config',
|
||||
ucinode = 'node',
|
||||
ucisubscription = 'subscription';
|
||||
|
||||
const allow_insecure = uci.get(uciconfig, ucisubscription, 'allow_insecure') || '0',
|
||||
filter_mode = uci.get(uciconfig, ucisubscription, 'filter_nodes') || 'disabled',
|
||||
filter_keywords = uci.get(uciconfig, ucisubscription, 'filter_keywords') || [],
|
||||
packet_encoding = uci.get(uciconfig, ucisubscription, 'packet_encoding') || 'xudp',
|
||||
subscription_urls = uci.get(uciconfig, ucisubscription, 'subscription_url') || [],
|
||||
via_proxy = uci.get(uciconfig, ucisubscription, 'update_via_proxy') || '0';
|
||||
|
||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainalnd_china';
|
||||
let main_node, main_udp_node;
|
||||
if (routing_mode !== 'custom') {
|
||||
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
||||
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
||||
}
|
||||
/* UCI config end */
|
||||
|
||||
/* String helper start */
|
||||
function filter_check(name) {
|
||||
if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
|
||||
return false;
|
||||
|
||||
let ret = false;
|
||||
for (let i in filter_keywords) {
|
||||
const patten = regexp(i);
|
||||
if (match(name, patten))
|
||||
ret = true;
|
||||
} if (filter_mode === 'whitelist')
|
||||
ret = !ret;
|
||||
|
||||
return ret
|
||||
}
|
||||
/* String helper end */
|
||||
|
||||
/* Common var start */
|
||||
const node_cache = {},
|
||||
node_result = [];
|
||||
|
||||
const ubus = connect();
|
||||
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
|
||||
/* Common var end */
|
||||
|
||||
/* Log */
|
||||
system(`mkdir -p ${RUN_DIR}`);
|
||||
function log(...args) {
|
||||
const logtime = trim(executeCommand('date "+%Y-%m-%d %H:%M:%S"').stdout);
|
||||
|
||||
const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
|
||||
logfile.write(`${logtime} [SUBSCRIBE] ${join(' ', args)}\n`);
|
||||
logfile.close();
|
||||
}
|
||||
|
||||
function parse_uri(uri) {
|
||||
let config;
|
||||
|
||||
if (type(uri) === 'object') {
|
||||
if (uri.nodetype === 'sip008') {
|
||||
config = {
|
||||
label: uri.remarks,
|
||||
type: 'shadowsocks',
|
||||
address: uri.server,
|
||||
port: uri.server_port,
|
||||
shadowsocks_encrypt_method: uri.method,
|
||||
password: uri.password,
|
||||
shadowsocks_plugin: uri.plugin,
|
||||
shadowsocks_plugin_opts: uri.plugin_opts
|
||||
};
|
||||
}
|
||||
} else if (type(uri) === 'string') {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
switch (uri[0]) {
|
||||
case 'hysteria':
|
||||
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
|
||||
const hysteria_url = parseURL('http://' + uri[1]),
|
||||
hysteria_params = hysteria_url.searchParams;
|
||||
|
||||
if (!sing_features.with_quic || (hysteria_params.protocol && hysteria_params.protocol !== 'udp')) {
|
||||
log(sprintf('Skipping unsupportedd %s node: %s.', 'hysteria', urldecode(hysteria_url.hash) || hysteria_url.hostname));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: urldecode(hysteria_url.hash),
|
||||
type: 'hysteria',
|
||||
address: hysteria_url.hostname,
|
||||
port: hysteria_url.port,
|
||||
hysteria_protocol: hysteria_params.protocol || 'udp',
|
||||
hysteria_auth_type: hysteria_params.auth ? 'string' : null,
|
||||
hysteria_auth_payload: hysteria_params.auth,
|
||||
hysteria_obfs_password: hysteria_params.obfsParam,
|
||||
hysteria_down_mbps: hysteria_params.downmbps,
|
||||
hysteria_up_mbps: hysteria_params.upmbps,
|
||||
tls: '1',
|
||||
tls_insecure: (hysteria_params.insecure in ['true', '1']) ? '1' : '0',
|
||||
tls_sni: hysteria_params.peer,
|
||||
tls_alpn: hysteria_params.alpn
|
||||
};
|
||||
|
||||
break;
|
||||
case 'ss':
|
||||
/* "Lovely" Shadowrocket format */
|
||||
const ss_suri = split(uri[1], '#');
|
||||
let ss_slabel = '';
|
||||
if (length(ss_suri) <= 2) {
|
||||
if (length(ss_suri) === 2)
|
||||
ss_slabel = '#' + urlencode(ss_suri[1]);
|
||||
if (decodeBase64Str(ss_suri[0]))
|
||||
uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel;
|
||||
}
|
||||
|
||||
/* Legacy format is not supported, it should be never appeared in modern subscriptions */
|
||||
/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
|
||||
|
||||
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
|
||||
const ss_url = parseURL('http://' + uri[1]);
|
||||
|
||||
let ss_userinfo = {};
|
||||
if (ss_url.username && ss_url.password)
|
||||
/* User info encoded with URIComponent */
|
||||
ss_userinfo = [ss_url.username, urldecode(ss_url.password)];
|
||||
else if (ss_url.username)
|
||||
/* User info encoded with base64 */
|
||||
ss_userinfo = split(decodeBase64Str(urldecode(ss_url.username)), ':');
|
||||
|
||||
let ss_plugin, ss_plugin_opts;
|
||||
if (ss_url.search && ss_url.searchParams.plugin) {
|
||||
const ss_plugin_info = split(ss_url.searchParams.plugin, ';');
|
||||
ss_plugin = ss_plugin_info[0];
|
||||
if (ss_plugin === 'simple-obfs')
|
||||
/* Fix non-standard plugin name */
|
||||
ss_plugin = 'obfs-local';
|
||||
ss_plugin_opts = slice(ss_plugin_info, 1) ? join(';', slice(ss_plugin_info, 1)) : null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: ss_url.hash ? urldecode(ss_url.hash) : null,
|
||||
type: 'shadowsocks',
|
||||
address: ss_url.hostname,
|
||||
port: ss_url.port,
|
||||
shadowsocks_encrypt_method: ss_userinfo[0],
|
||||
password: ss_userinfo[1],
|
||||
shadowsocks_plugin: ss_plugin,
|
||||
shadowsocks_plugin_opts: ss_plugin_opts
|
||||
};
|
||||
|
||||
break;
|
||||
case 'ssr':
|
||||
/* https://coderschool.cn/2498.html */
|
||||
uri = split(decodeBase64Str(uri[1]), '/');
|
||||
if (!uri)
|
||||
return null;
|
||||
|
||||
const userinfo = split(uri[0], ':'),
|
||||
ssr_params = urldecode_params(uri[1]);
|
||||
|
||||
if (!sing_features.with_shadowsocksr) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', 'ShadowsocksR', decodeBase64Str(ssr_params.remarks) || userinfo[1]));
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'ShadowsocksR'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: decodeBase64Str(ssr_params.remarks),
|
||||
type: 'shadowsocksr',
|
||||
address: userinfo[0],
|
||||
port: userinfo[1],
|
||||
shadowsocksr_encrypt_method: userinfo[3],
|
||||
password: decodeBase64Str(userinfo[5]),
|
||||
shadowsocksr_protocol: userinfo[2],
|
||||
shadowsocksr_protocol_param: decodeBase64Str(ssr_params.protoparam),
|
||||
shadowsocksr_obfs: userinfo[4],
|
||||
shadowsocksr_obfs_param: decodeBase64Str(ssr_params.obfsparam)
|
||||
};
|
||||
|
||||
break;
|
||||
case 'trojan':
|
||||
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
|
||||
const trojan_url = parseURL('http://' + uri[1]);
|
||||
|
||||
config = {
|
||||
label: trojan_url.hash ? urldecode(trojan_url.hash) : null,
|
||||
type: 'trojan',
|
||||
address: trojan_url.hostname,
|
||||
port: trojan_url.port,
|
||||
password: urldecode(trojan_url.username),
|
||||
tls: '1',
|
||||
tls_sni: trojan_url.searchParams ? trojan_url.searchParams.sni : null
|
||||
};
|
||||
|
||||
break;
|
||||
case 'vless':
|
||||
/* https://github.com/XTLS/Xray-core/discussions/716 */
|
||||
const vless_url = parseURL('http://' + uri[1]),
|
||||
vless_params = vless_url.searchParams;
|
||||
|
||||
/* Unsupported protocol */
|
||||
if (vless_params.type === 'kcp') {
|
||||
log(sprintf('Skipping sunsupported %s node: %s.', 'VLESS', urldecode(vless_url.hash) || vless_url.hostname));
|
||||
return null;
|
||||
} else if (vless_params.type === 'quic' && (vless_params.quicSecurity && vless_params.quicSecurity !== 'none' || !sing_features.with_quic)) {
|
||||
log(sprintf('Skipping sunsupported %s node: %s.', 'VLESS', urldecode(vless_url.hash) || vless_url.hostname));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
config = {
|
||||
label: vless_url.hash ? urldecode(vless_url.hash) : null,
|
||||
type: 'vless',
|
||||
address: vless_url.hostname,
|
||||
port: vless_url.port,
|
||||
uuid: vless_url.username,
|
||||
transport: (vless_params.type !== 'tcp') ? vless_params.type : null,
|
||||
tls: vless_params.security ? '1' : '0',
|
||||
tls_sni: vless_params.sni,
|
||||
tls_alpn: vless_params.alpn ? split(urldecode(vless_params.alpn), ',') : null,
|
||||
tls_utls: sing_features.with_utls ? vless_params.fp : null
|
||||
};
|
||||
switch(vless_params.type) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = vless_params.serviceName;
|
||||
break;
|
||||
case 'http':
|
||||
case 'tcp':
|
||||
if (config.transport === 'http' || vless_params.headerType === 'http') {
|
||||
config.http_host = vless_params.host ? split(urldecode(vless_params.host), ',') : null;
|
||||
config.http_path = vless_params.path ? urldecode(vless_params.path) : null;
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = (config.tls !== '1' && vless_params.host) ? urldecode(vless_params.host) : null;
|
||||
config.ws_path = vless_params.path ? urldecode(vless_params.path) : null;
|
||||
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
||||
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
||||
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
||||
config.ws_path = split(config.ws_path, '?ed=')[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case 'vmess':
|
||||
/* "Lovely" shadowrocket format */
|
||||
if (match(uri, /&/)) {
|
||||
log(sprintf('Skipping unsupported %s format.', 'VMess'));
|
||||
return null;
|
||||
}
|
||||
|
||||
/* https://github.com/2dust/v2rayN/wiki/%E5%88%86%E4%BA%AB%E9%93%BE%E6%8E%A5%E6%A0%BC%E5%BC%8F%E8%AF%B4%E6%98%8E(ver-2) */
|
||||
try {
|
||||
uri = json(decodeBase64Str(uri[1]));
|
||||
} catch(e) {
|
||||
log(sprintf('Skipping unsupported %s format.', 'VMess'));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (uri.v !== '2') {
|
||||
log(sprintf('Skipping unsupported %s format.', 'VMess'));
|
||||
return null;
|
||||
/* Unsupported protocol */
|
||||
} else if (uri.net === 'kcp') {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
|
||||
return null;
|
||||
} else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || uri.path || !sing_features.with_quic)) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
|
||||
if (!sing_features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
/*
|
||||
* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
|
||||
* else if (uri.aid && int(uri.aid) !== 0) {
|
||||
* log(sprintf('Skipping unsupported %s node: %s.', 'VMess', uri.ps || uri.add));
|
||||
* return null;
|
||||
* }
|
||||
*/
|
||||
|
||||
config = {
|
||||
label: uri.ps,
|
||||
type: 'vmess',
|
||||
address: uri.add,
|
||||
port: uri.port,
|
||||
uuid: uri.id,
|
||||
vmess_alterid: uri.aid,
|
||||
vmess_encrypt: uri.scy || 'auto',
|
||||
transport: (uri.net !== 'tcp') ? uri.net : null,
|
||||
tls: (uri.tls === 'tls') ? '1' : '0',
|
||||
tls_sni: uri.sni || uri.host,
|
||||
tls_alpn: uri.alpn ? split(uri.alpn, ',') : null
|
||||
};
|
||||
switch (uri.net) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = uri.path;
|
||||
break;
|
||||
case 'h2':
|
||||
case 'tcp':
|
||||
if (uri.net === 'h2' || uri.type === 'http') {
|
||||
config.transport = 'http';
|
||||
config.http_host = uri.host ? uri.host.split(',') : null;
|
||||
config.http_path = uri.path;
|
||||
}
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = (config.tls !== '1') ? uri.host : null;
|
||||
config.ws_path = uri.path;
|
||||
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
|
||||
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
|
||||
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
|
||||
config.ws_path = split(config.ws_path, '?ed=')[0];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(config)) {
|
||||
if (config.address)
|
||||
config.address = replace(config.address, /\[|\]/g, '');
|
||||
|
||||
if (validation('host', config.address) !== 0 || validation('port', config.port) !== 0) {
|
||||
log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL'));
|
||||
return null;
|
||||
} else if (!config.label)
|
||||
config.label = (validation('ip6addr', config.address) === 0 ?
|
||||
`[${config.address}]` : config.address) + ':' + config.port;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function main() {
|
||||
if (via_proxy !== '1') {
|
||||
log('Stopping service...');
|
||||
init_action('homeproxy', 'stop');
|
||||
}
|
||||
|
||||
for (let url in subscription_urls) {
|
||||
const res = CURL(url);
|
||||
if (!res) {
|
||||
log(sprintf('Failed to fetch resources from %s.', url));
|
||||
continue;
|
||||
}
|
||||
|
||||
const groupHash = calcStringMD5(url);
|
||||
node_cache[groupHash] = {};
|
||||
|
||||
push(node_result, []);
|
||||
const subindex = length(node_result) - 1;
|
||||
|
||||
let nodes;
|
||||
try {
|
||||
nodes = json(res).servers || json(res);
|
||||
if (nodes[0].server && nodes[0].method)
|
||||
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
|
||||
} catch(e) {
|
||||
nodes = decodeBase64Str(res);
|
||||
nodes = nodes ? split(trim(replace(nodes, / /g, '_')), '\n') : {};
|
||||
}
|
||||
|
||||
let count = 0;
|
||||
for (let node in nodes) {
|
||||
let config;
|
||||
if (!isEmpty(node))
|
||||
config = parse_uri(node);
|
||||
if (isEmpty(config))
|
||||
continue;
|
||||
|
||||
const label = config.label;
|
||||
config.label = null;
|
||||
const confHash = calcStringMD5(sprintf('%J', config)),
|
||||
nameHash = calcStringMD5(label);
|
||||
config.label = label;
|
||||
|
||||
if (filter_check(config.label))
|
||||
log(sprintf('Skipping blacklist node: %s.', config.label));
|
||||
else if (node_cache[groupHash][confHash] || node_cache[groupHash][nameHash])
|
||||
log(sprintf('Skipping duplicate node: %s.', config.label));
|
||||
else {
|
||||
if (config.tls === '1' && allow_insecure === '1')
|
||||
config.tls_insecure = '1';
|
||||
if (config.type in ['vless', 'vmess'])
|
||||
config.packet_encoding = packet_encoding;
|
||||
|
||||
config.grouphash = groupHash;
|
||||
push(node_result[subindex], config);
|
||||
node_cache[groupHash][confHash] = config;
|
||||
node_cache[groupHash][nameHash] = config;
|
||||
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
log(sprintf('Successfully fetched %s nodes of total %s from %s.', count, length(nodes), url));
|
||||
}
|
||||
|
||||
if (isEmpty(node_result)) {
|
||||
log('Failed to update subscriptions: no valid node found.');
|
||||
|
||||
if (via_proxy !== '1') {
|
||||
log('Starting service...');
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
let added = 0, removed = 0;
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
if (!cfg.grouphash)
|
||||
return null;
|
||||
|
||||
if (!node_cache[cfg.grouphash] || !node_cache[cfg.grouphash][cfg['.name']]) {
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
removed += 1;
|
||||
|
||||
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
|
||||
} else {
|
||||
map(keys(node_cache[cfg.grouphash][cfg['.name']]), (v) => {
|
||||
uci.set(uciconfig, cfg['.name'], v, node_cache[cfg.grouphash][cfg['.name']][v]);
|
||||
});
|
||||
node_cache[cfg.grouphash][cfg['.name']].isExisting = true;
|
||||
}
|
||||
});
|
||||
for (let nodes in node_result)
|
||||
map(nodes, (node) => {
|
||||
if (node.isExisting)
|
||||
return null;
|
||||
|
||||
const nameHash = calcStringMD5(node.label);
|
||||
uci.set(uciconfig, nameHash, 'node');
|
||||
map(keys(node), (v) => uci.set(uciconfig, nameHash, v, node[v]));
|
||||
|
||||
added += 1;
|
||||
log(sprintf('Adding node: %s.', node.label));
|
||||
});
|
||||
uci.commit();
|
||||
|
||||
let need_restart = (via_proxy !== '1');
|
||||
if (!isEmpty(main_node)) {
|
||||
const first_server = uci.get_first(uciconfig, ucinode);
|
||||
if (first_server) {
|
||||
if (!uci.get(uciconfig, main_node)) {
|
||||
uci.set(uciconfig, ucimain, 'main_node', first_server);
|
||||
need_restart = true;
|
||||
|
||||
log('Main node is gone, switching to the first node.');
|
||||
}
|
||||
|
||||
if (!isEmpty(main_udp_node) && main_udp_node !== 'same') {
|
||||
if (!uci.get(uciconfig, main_udp_node)) {
|
||||
uci.set(uciconfig, ucimain, 'main_udp_node', first_server);
|
||||
need_restart = true;
|
||||
|
||||
log('Main UDP node is gone, switching to the first node.');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uci.set(uciconfig, ucimain, 'main_node', 'nil');
|
||||
uci.set(uciconfig, ucimain, 'main_udp_node', 'nil');
|
||||
need_restart = true;
|
||||
|
||||
log('No available node, disable tproxy.');
|
||||
}
|
||||
}
|
||||
|
||||
if (need_restart) {
|
||||
uci.commit();
|
||||
log('Restarting service...');
|
||||
init_action('homeproxy', 'stop');
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
||||
|
||||
log(sprintf('%s nodes added, %s removed.', added, removed));
|
||||
log('Successfully updated subscriptions.');
|
||||
}
|
||||
|
||||
if (!isEmpty(subscription_urls))
|
||||
try {
|
||||
call(main);
|
||||
} catch(e) {
|
||||
log('[FATAL ERROR] An error occurred during updating subscriptions:');
|
||||
log(e);
|
||||
|
||||
log('Restarting service...');
|
||||
init_action('homeproxy', 'stop');
|
||||
init_action('homeproxy', 'start');
|
||||
}
|
232
homeproxy/root/etc/init.d/homeproxy
Executable file
232
homeproxy/root/etc/init.d/homeproxy
Executable file
@ -0,0 +1,232 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
USE_PROCD=1
|
||||
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
CONF="homeproxy"
|
||||
PROG="/usr/bin/sing-box"
|
||||
|
||||
HP_DIR="/etc/homeproxy"
|
||||
RUN_DIR="/var/run/homeproxy"
|
||||
LOG_PATH="$RUN_DIR/homeproxy.log"
|
||||
DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d"
|
||||
|
||||
log() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local routing_mode
|
||||
config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
|
||||
|
||||
local outbound_node
|
||||
if [ "$routing_mode" != "custom" ]; then
|
||||
config_get outbound_node "config" "main_node" "nil"
|
||||
else
|
||||
config_get outbound_node "routing" "default_outbound" "nil"
|
||||
fi
|
||||
|
||||
local server_enabled
|
||||
config_get_bool server_enabled "server" "enabled" "0"
|
||||
|
||||
if [ "$outbound_node" = "nil" ] && [ "$server_enabled" = "0" ]; then
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
ucode -S "$HP_DIR/scripts/generate_sing-box.uc" 2>>"$LOG_PATH"
|
||||
|
||||
local inbounds="$(jsonfilter -i "$RUN_DIR/sing-box.json" -e "@.inbounds[@.tag!='dns-in']" 2>"/dev/null")"
|
||||
if [ ! -e "$RUN_DIR/sing-box.json" ]; then
|
||||
log "Error: failed to generate configuration."
|
||||
exit 1
|
||||
elif [ -z "$inbounds" ]; then
|
||||
log "Error: no valid inbound found."
|
||||
exit 1
|
||||
elif ! "$PROG" check --config "$RUN_DIR/sing-box.json" 2>>"$LOG_PATH"; then
|
||||
log "Error: wrong configuration detected."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
# 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"
|
||||
echo "0 ${auto_update_time} * * * $HP_DIR/scripts/update_crond.sh" >> "/etc/crontabs/root"
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
|
||||
local ipv6_support
|
||||
config_get_bool ipv6_support "config" "ipv6_support" "0"
|
||||
|
||||
# DNSMasq rules
|
||||
local dns_port nftset_v6
|
||||
config_get dns_port "infra" "dns_port" "5333"
|
||||
[ "$ipv6_support" -eq "0" ] || nftset_v6=",6#inet#fw4#homeproxy_gfw_list_v6"
|
||||
mkdir -p "$DNSMASQ_DIR"
|
||||
echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf"
|
||||
if [ "$routing_mode" = "gfwlist" ]; then
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_gfw_list_v4$nftset_v6/g" \
|
||||
"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
|
||||
elif [ "$routing_mode" = "bypass_mainland_china" ]; then
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
|
||||
"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
|
||||
elif [ "$routing_mode" = "proxy_mainland_china" ]; then
|
||||
sed -r -e "s/full://g" -e "/:/d" -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
|
||||
"$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf"
|
||||
else
|
||||
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
|
||||
no-poll
|
||||
no-resolv
|
||||
server=127.0.0.1#$dns_port
|
||||
EOF
|
||||
fi
|
||||
|
||||
if [ "$routing_mode" != "custom" ]; then
|
||||
[ "$ipv6_support" -eq "0" ] || nftset_v6=",6#inet#fw4#homeproxy_proxy_addr_v6"
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_proxy_addr_v4$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 firewall
|
||||
local table_mark
|
||||
config_get table_mark "infra" "table_mark" "100"
|
||||
if [ "$routing_mode" != "custom" ]; then
|
||||
local outbound_udp_node
|
||||
config_get outbound_udp_node "config" "main_udp_node" "nil"
|
||||
if [ "$outbound_udp_node" != "nil" ]; then
|
||||
local tproxy_mark
|
||||
config_get tproxy_mark "infra" "tproxy_mark" "101"
|
||||
|
||||
ip rule add fwmark "$tproxy_mark" table "$table_mark"
|
||||
ip route add local 0.0.0.0/0 dev lo table "$table_mark"
|
||||
|
||||
if [ "$ipv6_support" -eq "1" ]; then
|
||||
ip -6 rule add fwmark "$tproxy_mark" table "$table_mark"
|
||||
ip -6 route add local ::/0 dev lo table "$table_mark"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
local tun_name tun_mark
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
config_get tun_mark "infra" "tun_mark" "102"
|
||||
|
||||
ip tuntap add mode tun user root name "$tun_name"
|
||||
ip link set "$tun_name" up
|
||||
|
||||
ip route replace default dev "$tun_name" table "$table_mark"
|
||||
ip rule add fwmark "$tun_mark" lookup "$table_mark"
|
||||
|
||||
ip -6 route replace default dev "$tun_name" table "$table_mark"
|
||||
ip -6 rule add fwmark "$tun_mark" lookup "$table_mark"
|
||||
fi
|
||||
|
||||
utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft"
|
||||
|
||||
fi
|
||||
|
||||
utpl -S "$HP_DIR/scripts/firewall_pre.ut" > "$RUN_DIR/fw4_pre.nft"
|
||||
fw4 reload >"/dev/null" 2>&1
|
||||
|
||||
procd_open_instance "sing-box"
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command run --config "$RUN_DIR/sing-box.json"
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
|
||||
procd_open_instance "log-cleaner"
|
||||
procd_set_param command "$HP_DIR/scripts/clean_log.sh"
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
|
||||
echo > "$RUN_DIR/sing-box.log"
|
||||
log "$(sing-box version | awk 'NR==1{print $1,$3}') started."
|
||||
}
|
||||
|
||||
service_started() {
|
||||
procd_set_config_changed firewall
|
||||
}
|
||||
|
||||
service_stopped() {
|
||||
sed -i "/$CONF/d" "/etc/crontabs/root" 2>"/dev/null"
|
||||
/etc/init.d/cron restart >"/dev/null" 2>&1
|
||||
|
||||
# Setup firewall
|
||||
# Load config
|
||||
config_load "$CONF"
|
||||
local table_mark tproxy_mark tun_mark tun_name
|
||||
config_get table_mark "infra" "table_mark" "100"
|
||||
config_get tproxy_mark "infra" "tproxy_mark" "101"
|
||||
config_get tun_mark "infra" "tun_mark" "102"
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
|
||||
# Tproxy
|
||||
ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
|
||||
ip route del local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null"
|
||||
ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
|
||||
ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null"
|
||||
|
||||
# TUN
|
||||
ip link set "$tun_name" down 2>"/dev/null"
|
||||
ip tuntap del mode tun name "$tun_name" 2>"/dev/null"
|
||||
|
||||
ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
|
||||
ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
|
||||
|
||||
ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
|
||||
ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
|
||||
|
||||
# Nftables rules
|
||||
for i in "homeproxy_dstnat_redir" "homeproxy_output_redir" \
|
||||
"homeproxy_redirect" "homeproxy_redirect_proxy" \
|
||||
"homeproxy_mangle_prerouting" "homeproxy_mangle_output" \
|
||||
"homeproxy_mangle_tproxy" "homeproxy_mangle_mark"; do
|
||||
nft flush chain inet fw4 "$i" 2>"/dev/null"
|
||||
nft delete chain inet fw4 "$i" 2>"/dev/null"
|
||||
done
|
||||
for i in "homeproxy_local_addr_v4" "homeproxy_local_addr_v6" \
|
||||
"homeproxy_gfw_list_v4" "homeproxy_gfw_list_v6" \
|
||||
"homeproxy_mainland_addr_v4" "homeproxy_mainland_addr_v6" \
|
||||
"homeproxy_proxy_addr_v4" "homeproxy_proxy_addr_v6"; do
|
||||
nft flush set inet fw4 "$i" 2>"/dev/null"
|
||||
nft delete set inet fw4 "$i" 2>"/dev/null"
|
||||
done
|
||||
echo > "$RUN_DIR/fw4_pre.nft"
|
||||
echo > "$RUN_DIR/fw4_post.nft"
|
||||
fw4 reload >"/dev/null" 2>&1
|
||||
|
||||
# Remove DNS hijack
|
||||
rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR"
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
|
||||
rm -f "$RUN_DIR/sing-box.json" "$RUN_DIR/sing-box.log"
|
||||
|
||||
log "Service stopped."
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
log "Reloading service..."
|
||||
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
}
|
19
homeproxy/root/etc/uci-defaults/luci-homeproxy
Normal file
19
homeproxy/root/etc/uci-defaults/luci-homeproxy
Normal file
@ -0,0 +1,19 @@
|
||||
#!/bin/sh
|
||||
|
||||
uci -q batch <<-EOF >"/dev/null"
|
||||
delete firewall.homeproxy_pre
|
||||
set firewall.homeproxy_pre=include
|
||||
set firewall.homeproxy_pre.type=nftables
|
||||
set firewall.homeproxy_pre.path="/var/run/homeproxy/fw4_pre.nft"
|
||||
set firewall.homeproxy_pre.position="table-pre"
|
||||
|
||||
delete firewall.homeproxy_post
|
||||
set firewall.homeproxy_post=include
|
||||
set firewall.homeproxy_post.type=nftables
|
||||
set firewall.homeproxy_post.path="/var/run/homeproxy/fw4_post.nft"
|
||||
set firewall.homeproxy_post.position="table-post"
|
||||
commit firewall
|
||||
EOF
|
||||
|
||||
rm -f /tmp/luci-indexcache
|
||||
exit 0
|
45
homeproxy/root/usr/share/luci/menu.d/luci-app-homeproxy.json
Normal file
45
homeproxy/root/usr/share/luci/menu.d/luci-app-homeproxy.json
Normal file
@ -0,0 +1,45 @@
|
||||
{
|
||||
"admin/services/homeproxy": {
|
||||
"title": "HomeProxy",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-homeproxy" ],
|
||||
"uci": { "homeproxy": true }
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/client": {
|
||||
"title": "Client Settings",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/client"
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/node": {
|
||||
"title": "Node Settings",
|
||||
"order": 15,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/node"
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/server": {
|
||||
"title": "Server Settings",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/server"
|
||||
}
|
||||
},
|
||||
"admin/services/homeproxy/status": {
|
||||
"title": "Service Status",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "homeproxy/status"
|
||||
}
|
||||
}
|
||||
}
|
23
homeproxy/root/usr/share/rpcd/acl.d/luci-app-homeproxy.json
Normal file
23
homeproxy/root/usr/share/rpcd/acl.d/luci-app-homeproxy.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"luci-app-homeproxy": {
|
||||
"description": "Grant access to homeproxy configuration",
|
||||
"read": {
|
||||
"file": {
|
||||
"/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ],
|
||||
"/var/run/homeproxy/homeproxy.log": [ "read" ],
|
||||
"/var/run/homeproxy/sing-box.log": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ],
|
||||
"luci.homeproxy": [ "*" ]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/tmp/homeproxy_certificate.tmp": [ "write" ]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
}
|
||||
}
|
||||
}
|
176
homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy
Executable file
176
homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy
Executable file
@ -0,0 +1,176 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { access, error, lstat, mkstemp, popen, readfile, writefile } from 'fs';
|
||||
|
||||
/* Kanged from ucode/luci */
|
||||
function shellquote(s) {
|
||||
return `'${replace(s, "'", "'\\''")}'`;
|
||||
}
|
||||
|
||||
const HP_DIR = '/etc/homeproxy';
|
||||
const RUN_DIR = '/var/run/homeproxy';
|
||||
|
||||
const methods = {
|
||||
acllist_read: {
|
||||
args: { type: 'type' },
|
||||
call: function(req) {
|
||||
if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
|
||||
return { content: null, error: 'illegal type' };
|
||||
|
||||
const filecontent = readfile(`${HP_DIR}/resources/${req.args?.type}.txt`);
|
||||
return { content: filecontent };
|
||||
}
|
||||
},
|
||||
acllist_write: {
|
||||
args: { type: 'type', content: 'content' },
|
||||
call: function(req) {
|
||||
if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
|
||||
return { result: false, error: 'illegal type' };
|
||||
|
||||
const file = `${HP_DIR}/resources/${req.args?.type}.txt`;
|
||||
let content = req.args?.content;
|
||||
|
||||
/* Sanitize content */
|
||||
if (content) {
|
||||
content = trim(content);
|
||||
content = replace(content, /\r\n?/g, '\n');
|
||||
if (!match(content, /\n$/))
|
||||
content += '\n';
|
||||
}
|
||||
|
||||
system(`mkdir -p ${HP_DIR}/resources`);
|
||||
writefile(file, content);
|
||||
system('env -i /etc/init.d/homeproxy restart >/dev/null &');
|
||||
|
||||
return { result: true };
|
||||
}
|
||||
},
|
||||
|
||||
certificate_write: {
|
||||
args: { filename: 'filename' },
|
||||
call: function(req) {
|
||||
const writeCertificate = function(filename, priv) {
|
||||
const tmpcert = '/tmp/homeproxy_certificate.tmp';
|
||||
const filestat = lstat(tmpcert);
|
||||
|
||||
if (!filestat || filestat.type !== 'file' || filestat.size <= 0) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'empty certificate file' };
|
||||
}
|
||||
|
||||
let filecontent = readfile(tmpcert);
|
||||
if (is_binary(filecontent)) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'illegal file type: binary' };
|
||||
}
|
||||
|
||||
/* Kanged from luci-proto-openconnect */
|
||||
const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
|
||||
end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
|
||||
lines = split(trim(filecontent), /[\r\n]/);
|
||||
let start = false, i;
|
||||
|
||||
for (i = 0; i < length(lines); i++) {
|
||||
if (match(lines[i], beg))
|
||||
start = true;
|
||||
else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!start || i < length(lines) - 1 || !match(lines[i], end)) {
|
||||
system(`rm -f ${tmpcert}`);
|
||||
return { result: false, error: 'this does not look like a correct PEM file' };
|
||||
}
|
||||
|
||||
/* Sanitize certificate */
|
||||
filecontent = trim(filecontent);
|
||||
filecontent = replace(filecontent, /\r\n?/g, '\n');
|
||||
if (!match(filecontent, /\n$/))
|
||||
filecontent += '\n';
|
||||
|
||||
system(`mkdir -p ${HP_DIR}/certs`);
|
||||
writefile(`${HP_DIR}/certs/${filename}.pem`, filecontent);
|
||||
system(`rm -f ${tmpcert}`);
|
||||
|
||||
return { result: true };
|
||||
};
|
||||
|
||||
const filename = req.args?.filename;
|
||||
switch (filename) {
|
||||
case 'client_ca':
|
||||
case 'server_publickey':
|
||||
return writeCertificate(filename, false);
|
||||
break;
|
||||
case 'server_privatekey':
|
||||
return writeCertificate(filename, true);
|
||||
break;
|
||||
default:
|
||||
return { result: false, error: 'illegal cerificate filename' };
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
log_clean: {
|
||||
args: { type: 'type' },
|
||||
call: function(req) {
|
||||
if (!(req.args?.type in ['homeproxy', 'sing-box']))
|
||||
return { result: false, error: 'illegal type' };
|
||||
|
||||
const filestat = lstat(`${RUN_DIR}/${req.args?.type}.log`);
|
||||
if (filestat)
|
||||
writefile(`${RUN_DIR}/${req.args?.type}.log`, '');
|
||||
return { result: true };
|
||||
}
|
||||
},
|
||||
|
||||
singbox_get_features: {
|
||||
call: function() {
|
||||
let features = {};
|
||||
|
||||
const fd = popen('/usr/bin/sing-box version');
|
||||
if (fd) {
|
||||
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
|
||||
let tags = match(trim(line), /Tags: (.*)/);
|
||||
if (!tags)
|
||||
continue;
|
||||
|
||||
for (let i in split(tags[1], ','))
|
||||
features[i] = true;
|
||||
}
|
||||
|
||||
fd.close();
|
||||
}
|
||||
|
||||
return features;
|
||||
}
|
||||
},
|
||||
|
||||
resources_get_version: {
|
||||
args: { type: 'type' },
|
||||
call: function(req) {
|
||||
const version = trim(readfile(`${HP_DIR}/resources/${req.args?.type}.ver`));
|
||||
return { version: version, error: error() };
|
||||
}
|
||||
},
|
||||
resources_update: {
|
||||
args: { type: 'type' },
|
||||
call: function(req) {
|
||||
if (req.args?.type) {
|
||||
const type = shellquote(req.args?.type);
|
||||
const exit_code = system(`${HP_DIR}/scripts/update_resources.sh ${type}`);
|
||||
return { status: exit_code };
|
||||
} else
|
||||
return { status: 255, error: 'illegal type' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.homeproxy': methods };
|
@ -67,7 +67,7 @@ plugins:
|
||||
- tag: cache
|
||||
type: cache
|
||||
args:
|
||||
size: 204800
|
||||
size: 20480
|
||||
lazy_cache_ttl: 259200
|
||||
dump_file: "/etc/mosdns/rule/cache.dump"
|
||||
dump_interval: 43200
|
||||
|
@ -4279,7 +4279,6 @@
|
||||
52audio.com
|
||||
52ayw.com
|
||||
52bar.com
|
||||
52bendi.com
|
||||
52biquge.com
|
||||
52bjd.com
|
||||
52bji.com
|
||||
@ -6172,7 +6171,6 @@
|
||||
88995799.com
|
||||
88999.com
|
||||
8899yyy.vip
|
||||
889su.com
|
||||
88bank.com
|
||||
88bx.com
|
||||
88cdn.com
|
||||
@ -7143,7 +7141,6 @@ a5.net
|
||||
a5idc.net
|
||||
a632079.me
|
||||
a67dy.com
|
||||
a6a1.com
|
||||
a6shi.com
|
||||
a7.com
|
||||
a8.com
|
||||
@ -18666,7 +18663,6 @@ diyinews.com
|
||||
diyishijian.com
|
||||
diyitech.com
|
||||
diyixiazai.com
|
||||
diyixitong.com
|
||||
diyiyou.com
|
||||
diyiyunshi.com
|
||||
diyiziti.com
|
||||
@ -21696,6 +21692,7 @@ fclouddns.net
|
||||
fcloudpaas.com
|
||||
fcnes.com
|
||||
fcpiao.com
|
||||
fcpowerup.com
|
||||
fcrc114.com
|
||||
fcsc.com
|
||||
fcvvip.com
|
||||
@ -26076,7 +26073,6 @@ hbsogdjt.com
|
||||
hbspcar.com
|
||||
hbsql.com
|
||||
hbssfw.com
|
||||
hbssspot.com
|
||||
hbsszx.com
|
||||
hbstars.com
|
||||
hbsti.com
|
||||
@ -28012,7 +28008,6 @@ huaxi.net
|
||||
huaxi100.com
|
||||
huaxia.com
|
||||
huaxia77.com
|
||||
huaxiaci.com
|
||||
huaxiaf.com
|
||||
huaxiald.com
|
||||
huaxiangdiao.com
|
||||
@ -32272,6 +32267,7 @@ jiuyingwangluo.com
|
||||
jiuyuehuyu.com
|
||||
jiuyuu.com
|
||||
jiuzhaigou-china.com
|
||||
jiuzhang.com
|
||||
jiuzheng.com
|
||||
jiuzhilan.com
|
||||
jiuzhinews.com
|
||||
@ -33479,7 +33475,6 @@ k2os.com
|
||||
k345.cc
|
||||
k366.com
|
||||
k369.com
|
||||
k3851.com
|
||||
k3887.com
|
||||
k38s0.xyz
|
||||
k4nz.com
|
||||
@ -34736,7 +34731,6 @@ kulemi.com
|
||||
kulengvps.com
|
||||
kuletco.com
|
||||
kuli.ren
|
||||
kuliqiang.com
|
||||
kuliwang.net
|
||||
kuman.com
|
||||
kuman56.com
|
||||
@ -36275,6 +36269,7 @@ linshang.com
|
||||
linshigong.com
|
||||
linsn.com
|
||||
linstitute.net
|
||||
lintcode.com
|
||||
lintey.com
|
||||
lintongrc.com
|
||||
linuo-paradigma.com
|
||||
@ -41176,7 +41171,6 @@ nsforce.net
|
||||
nshen.net
|
||||
nshzpks.com
|
||||
nsini.com
|
||||
nsisfans.com
|
||||
nsoad.com
|
||||
nsoft.vip
|
||||
nsrfww.com
|
||||
@ -47828,7 +47822,6 @@ shmulan.com
|
||||
shmusic.org
|
||||
shmxcz.org
|
||||
shmy365.com
|
||||
shmyapi.com
|
||||
shmylike.com
|
||||
shnaer.com
|
||||
shnb12315.com
|
||||
@ -51652,7 +51645,6 @@ tianshi2.net
|
||||
tianshiyiyuan.com
|
||||
tianshouzhi.com
|
||||
tianshuge.com
|
||||
tiansin.com
|
||||
tiantailaw.com
|
||||
tiantang6.com
|
||||
tiantangnian.com
|
||||
@ -53924,7 +53916,6 @@ uuzz.com
|
||||
uvexperience.com
|
||||
uvledtek.com
|
||||
uvov.com
|
||||
uvu.cc
|
||||
uw3c.com
|
||||
uw9.net
|
||||
uwa4d.com
|
||||
@ -54517,7 +54508,6 @@ vpscang.com
|
||||
vpsdawanjia.com
|
||||
vpsdx.com
|
||||
vpser.net
|
||||
vpsjie.com
|
||||
vpsjxw.com
|
||||
vpsmm.com
|
||||
vpsno.com
|
||||
@ -54688,7 +54678,6 @@ w0882.com
|
||||
w0lker.com
|
||||
w10a.com
|
||||
w10xitong.com
|
||||
w10xz.com
|
||||
w10zj.com
|
||||
w123w.com
|
||||
w1365.com
|
||||
@ -59631,7 +59620,6 @@ xunleige520.com
|
||||
xunleige88.com
|
||||
xunleiyy.com
|
||||
xunlew.com
|
||||
xunli.xyz
|
||||
xunliandata.com
|
||||
xunlong.net
|
||||
xunlong.tv
|
||||
@ -60576,7 +60564,6 @@ yhquan365.com
|
||||
yhqurl.com
|
||||
yhrcb.com
|
||||
yhres.com
|
||||
yhrtvu.com
|
||||
yhshapp.com
|
||||
yhsms.com
|
||||
yhspy.com
|
||||
@ -61607,7 +61594,6 @@ youluxe.com
|
||||
youmai.com
|
||||
youmaolu.com
|
||||
youme.im
|
||||
youmeng.me
|
||||
youmenr.com
|
||||
youmew.com
|
||||
youmi.net
|
||||
@ -61764,7 +61750,6 @@ youyannet.com
|
||||
youyeetoo.com
|
||||
youyegame.com
|
||||
youyi-game.com
|
||||
youyikeji666.com
|
||||
youyilm.com
|
||||
youyiqi.com
|
||||
youyiqiaogou.com
|
||||
@ -64243,7 +64228,6 @@ zhulang.com
|
||||
zhulanli.com
|
||||
zhuli999.com
|
||||
zhulincat.com
|
||||
zhuliudai.com
|
||||
zhulixiaolie.com
|
||||
zhulogic.com
|
||||
zhulong.com
|
||||
@ -64569,9 +64553,7 @@ zjgwyw.org
|
||||
zjgzcpa.com
|
||||
zjhangyin.com
|
||||
zjhcbank.com
|
||||
zjhee.com
|
||||
zjhejiang.com
|
||||
zjhim.com
|
||||
zjhnlianzhong.com
|
||||
zjhnrb.com
|
||||
zjhualing.com
|
||||
@ -65522,7 +65504,6 @@ zzhuanruan.com
|
||||
zzhybz.com
|
||||
zzidc.com
|
||||
zzit.org
|
||||
zzjc5.com
|
||||
zzjunzhi.com
|
||||
zzjxbg.com
|
||||
zzkiss000.com
|
||||
|
@ -1,29 +1,5 @@
|
||||
tracking.miui.com
|
||||
tracking.intl.miui.com
|
||||
api.intl.miui.com
|
||||
stat.xiaomi.com
|
||||
checkip.synology.com
|
||||
checkipv6.synology.com
|
||||
checkport.synology.com
|
||||
ddns.synology.com
|
||||
account.synology.com
|
||||
whatismyip.akamai.com
|
||||
ddns.synology.com
|
||||
checkip.synology.com
|
||||
checkip.dyndns.org
|
||||
teamviewer.com
|
||||
bing.com
|
||||
api.ipify.org
|
||||
epicgames.com
|
||||
emby.kyarucloud.moe
|
||||
ntp.aliyun.com
|
||||
ntp.tencent.com
|
||||
cn.ntp.org.cn
|
||||
ntp.ntsc.ac.cn
|
||||
keyword:sglong
|
||||
keyword:sgshort
|
||||
keyword:sgminorshort
|
||||
keyword:sgaxshort
|
||||
keyword:sgfindershort
|
||||
keyword:apple
|
||||
keyword:aaplimg
|
||||
keyword:itunes
|
||||
keyword:icloud
|
||||
checkipv6.synology.com
|
||||
|
@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=1.8
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Transparent_Proxy \
|
||||
|
@ -25,6 +25,10 @@ if api.is_ipv6(server_host) then
|
||||
end
|
||||
local server = server_host .. ":" .. server_port
|
||||
|
||||
if (node.hysteria_hop) then
|
||||
server = server .. "," .. node.hysteria_hop
|
||||
end
|
||||
|
||||
local config = {
|
||||
server = server,
|
||||
protocol = node.protocol or "udp",
|
||||
@ -40,6 +44,9 @@ local config = {
|
||||
retry_interval = 5,
|
||||
recv_window_conn = (node.hysteria_recv_window_conn) and tonumber(node.hysteria_recv_window_conn) or nil,
|
||||
recv_window = (node.hysteria_recv_window) and tonumber(node.hysteria_recv_window) or nil,
|
||||
handshake_timeout = (node.hysteria_handshake_timeout) and tonumber(node.hysteria_handshake_timeout) or nil,
|
||||
idle_timeout = (node.hysteria_idle_timeout) and tonumber(node.hysteria_idle_timeout) or nil,
|
||||
hop_interval = (node.hysteria_hop_interval) and tonumber(node.hysteria_hop_interval) or nil,
|
||||
disable_mtu_discovery = (node.hysteria_disable_mtu_discovery) and true or false,
|
||||
fast_open = (node.fast_open == "1") and true or false,
|
||||
socks5 = (local_socks_address and local_socks_port) and {
|
||||
|
@ -296,6 +296,9 @@ port:depends({ type = "Xray", protocol = "shadowsocks" })
|
||||
port:depends({ type = "Xray", protocol = "trojan" })
|
||||
port:depends({ type = "Xray", protocol = "wireguard" })
|
||||
|
||||
hysteria_hop = s:option(Value, "hysteria_hop", translate("Additional ports for hysteria hop"))
|
||||
hysteria_hop:depends("type", "Hysteria")
|
||||
|
||||
username = s:option(Value, "username", translate("Username"))
|
||||
username:depends("type", "Naiveproxy")
|
||||
username:depends({ type = "V2ray", protocol = "http" })
|
||||
@ -772,6 +775,15 @@ hysteria_recv_window_conn:depends("type", "Hysteria")
|
||||
hysteria_recv_window = s:option(Value, "hysteria_recv_window", translate("QUIC connection receive window"))
|
||||
hysteria_recv_window:depends("type", "Hysteria")
|
||||
|
||||
hysteria_handshake_timeout = s:option(Value, "hysteria_handshake_timeout", translate("Handshake Timeout"))
|
||||
hysteria_handshake_timeout:depends("type", "Hysteria")
|
||||
|
||||
hysteria_idle_timeout = s:option(Value, "hysteria_idle_timeout", translate("Idle Timeout"))
|
||||
hysteria_idle_timeout:depends("type", "Hysteria")
|
||||
|
||||
hysteria_hop_interval = s:option(Value, "hysteria_hop_interval", translate("Hop Interval"))
|
||||
hysteria_hop_interval:depends("type", "Hysteria")
|
||||
|
||||
hysteria_disable_mtu_discovery = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection"))
|
||||
hysteria_disable_mtu_discovery:depends("type", "Hysteria")
|
||||
|
||||
|
@ -1200,3 +1200,15 @@ msgstr "自定义geoip文件更新链接"
|
||||
|
||||
msgid "Custom geosite URL"
|
||||
msgstr "自定义geosite文件更新链接"
|
||||
|
||||
msgid "Handshake Timeout"
|
||||
msgstr "握手超时 "
|
||||
|
||||
msgid "Idle Timeout"
|
||||
msgstr "空闲超时 "
|
||||
|
||||
msgid "Hop Interval"
|
||||
msgstr "端口跳跃时间 "
|
||||
|
||||
msgid "Additional ports for hysteria hop"
|
||||
msgstr "端口跳跃额外端口"
|
||||
|
@ -172,6 +172,7 @@ get_wan6_ip() {
|
||||
load_acl() {
|
||||
local items=$(uci show ${CONFIG} | grep "=acl_rule" | cut -d '.' -sf 2 | cut -d '=' -sf 1)
|
||||
[ -n "$items" ] && {
|
||||
local index=0
|
||||
local item
|
||||
local redir_port dns_port dnsmasq_port
|
||||
local ipt_tmp msg msg2
|
||||
@ -180,6 +181,7 @@ load_acl() {
|
||||
dnsmasq_port=11400
|
||||
echolog "访问控制:"
|
||||
for item in $items; do
|
||||
index=$(expr $index + 1)
|
||||
local enabled sid remarks sources tcp_no_redir_ports udp_no_redir_ports tcp_redir_ports udp_redir_ports node direct_dns_protocol direct_dns direct_dns_doh direct_dns_client_ip direct_dns_query_strategy remote_dns_protocol only_proxy_fakedns remote_dns remote_dns_doh remote_dns_client_ip remote_dns_query_strategy
|
||||
local _ip _mac _iprange _ipset _ip_or_mac rule_list node_remark config_file
|
||||
sid=$(uci -q show "${CONFIG}.${item}" | grep "=acl_rule" | awk -F '=' '{print $1}' | awk -F '.' '{print $2}')
|
||||
@ -237,15 +239,28 @@ load_acl() {
|
||||
if [ -n "${type}" ] && ([ "${type}" = "v2ray" ] || [ "${type}" = "xray" ]); then
|
||||
config_file=$TMP_ACL_PATH/${node}_TCP_UDP_DNS_${redir_port}.json
|
||||
dns_port=$(get_new_port $(expr $dns_port + 1))
|
||||
run_v2ray flag=acl_$sid node=$node redir_port=$redir_port dns_listen_port=${dns_port} direct_dns_protocol=${direct_dns_protocol} direct_dns_udp_server=${direct_dns} direct_dns_tcp_server=${direct_dns} direct_dns_doh="${direct_dns}" direct_dns_client_ip=${direct_dns_client_ip} direct_dns_query_strategy=${direct_dns_query_strategy} remote_dns_protocol=${remote_dns_protocol} remote_dns_tcp_server=${remote_dns} remote_dns_udp_server=${remote_dns} remote_dns_doh="${remote_dns}" remote_dns_client_ip=${remote_dns_client_ip} remote_dns_query_strategy=${remote_dns_query_strategy} config_file=${config_file}
|
||||
local acl_socks_port=$(get_new_port $(expr $redir_port + $index))
|
||||
run_v2ray flag=acl_$sid node=$node redir_port=$redir_port socks_address=127.0.0.1 socks_port=$acl_socks_port dns_listen_port=${dns_port} direct_dns_protocol=${direct_dns_protocol} direct_dns_udp_server=${direct_dns} direct_dns_tcp_server=${direct_dns} direct_dns_doh="${direct_dns}" direct_dns_client_ip=${direct_dns_client_ip} direct_dns_query_strategy=${direct_dns_query_strategy} remote_dns_protocol=${remote_dns_protocol} remote_dns_tcp_server=${remote_dns} remote_dns_udp_server=${remote_dns} remote_dns_doh="${remote_dns}" remote_dns_client_ip=${remote_dns_client_ip} remote_dns_query_strategy=${remote_dns_query_strategy} config_file=${config_file}
|
||||
fi
|
||||
dnsmasq_port=$(get_new_port $(expr $dnsmasq_port + 1))
|
||||
redirect_dns_port=$dnsmasq_port
|
||||
mkdir -p $TMP_ACL_PATH/$sid
|
||||
mkdir -p $TMP_ACL_PATH/$sid/dnsmasq.d
|
||||
default_dnsmasq_cfgid=$(uci show dhcp.@dnsmasq[0] | awk -F '.' '{print $2}' | awk -F '=' '{print $1}'| head -1)
|
||||
[ -s "/tmp/etc/dnsmasq.conf.${default_dnsmasq_cfgid}" ] && {
|
||||
cp -r /tmp/etc/dnsmasq.conf.${default_dnsmasq_cfgid} $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
sed -i "/ubus/d" $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
sed -i "/dhcp/d" $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
sed -i "/port=/d" $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
sed -i "/conf-dir/d" $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
sed -i "/no-poll/d" $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
sed -i "/no-resolv/d" $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
}
|
||||
echo "port=${dnsmasq_port}" >> $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
#echo "conf-dir=${TMP_ACL_PATH}/${sid}/dnsmasq.d" >> $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
echo "conf-dir=${TMP_ACL_PATH}/${sid}/dnsmasq.d" >> $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
echo "server=127.0.0.1#${dns_port}" >> $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
#source $APP_PATH/helper_dnsmasq.sh add TMP_DNSMASQ_PATH=$TMP_ACL_PATH/$sid/dnsmasq.d DNSMASQ_CONF_FILE=/dev/null TUN_DNS=127.0.0.1#${dns_port} NO_LOGIC_LOG=1
|
||||
echo "no-poll" >> $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
echo "no-resolv" >> $TMP_ACL_PATH/$sid/dnsmasq.conf
|
||||
#source $APP_PATH/helper_dnsmasq.sh add TMP_DNSMASQ_PATH=$TMP_ACL_PATH/$sid/dnsmasq.d DNSMASQ_CONF_FILE=/dev/null DEFAULT_DNS=$AUTO_DNS TUN_DNS=127.0.0.1#${dns_port} NO_LOGIC_LOG=1
|
||||
ln_run "$(first_type dnsmasq)" "dnsmasq_${sid}" "/dev/null" -C $TMP_ACL_PATH/$sid/dnsmasq.conf -x $TMP_ACL_PATH/$sid/dnsmasq.pid
|
||||
eval node_${node}_$(echo -n "${tcp_proxy_mode}${remote_dns}" | md5sum | cut -d " " -f1)=${dnsmasq_port}
|
||||
filter_node $node TCP > /dev/null 2>&1 &
|
||||
|
@ -8,7 +8,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=lucky
|
||||
PKG_VERSION:=1.7.17
|
||||
PKG_VERSION:=1.7.21
|
||||
PKG_RELEASE:=1
|
||||
PKGARCH:=all
|
||||
|
||||
@ -45,7 +45,7 @@ PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=GDY666 <gdy666@foxmail.com>
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
PKG_HASH:=324caca77cafef4c25788a6f9b9d9e434378fe76c940220fb1a4b59a81967314
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user