mirror of
https://github.com/caiwx86/small-packages.git
synced 2026-07-29 01:41:51 +08:00
update 2026-07-17 10:09:14
This commit is contained in:
parent
992143f0a8
commit
cd5197e3d2
14
homeproxy/.github/rescan-translation.sh
vendored
14
homeproxy/.github/rescan-translation.sh
vendored
@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE_DIR="$(cd "$(dirname $0)"; pwd)"
|
||||
LUCI_DIR="$BASE_DIR/../../luci"
|
||||
|
||||
if [ -d "$LUCI_DIR" ]; then
|
||||
perl "$LUCI_DIR/build/i18n-scan.pl" . > "$BASE_DIR/../po/templates/homeproxy.pot"
|
||||
perl "$LUCI_DIR/build/i18n-update.pl" "$BASE_DIR/../po"
|
||||
else
|
||||
LUCI_URL="https://raw.githubusercontent.com/openwrt/luci/691574263356689912c5bd31984bb1b96417a847"
|
||||
perl <(curl -fs "$LUCI_URL/build/i18n-scan.pl") . > "$BASE_DIR/../po/templates/homeproxy.pot"
|
||||
perl <(curl -fs "$LUCI_URL/build/i18n-update.pl") "$BASE_DIR/../po"
|
||||
fi
|
||||
find "$BASE_DIR/../po" -name '*.po~' -delete;
|
||||
50
homeproxy/.github/update-geodata.sh
vendored
50
homeproxy/.github/update-geodata.sh
vendored
@ -1,50 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE_DIR="$(cd "$(dirname $0)"; pwd)"
|
||||
RESOURCES_DIR="$BASE_DIR/../root/etc/homeproxy/resources"
|
||||
|
||||
TEMP_DIR="$(mktemp -d -p $BASE_DIR)"
|
||||
|
||||
check_list_update() {
|
||||
local listtype="$1"
|
||||
local listrepo="$2"
|
||||
local listref="$3"
|
||||
local listname="$4"
|
||||
|
||||
local list_info="$(gh api "repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")"
|
||||
local list_sha="$(echo -e "$list_info" | jq -r ".[].sha")"
|
||||
local list_ver="$(echo -e "$list_info" | jq -r ".[].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')"
|
||||
if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then
|
||||
echo -e "[${listtype^^}] Failed to get the latest version, please retry later."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT FOUND")"
|
||||
if [ "$local_list_ver" = "$list_ver" ]; then
|
||||
echo -e "[${listtype^^}] Current version: $list_ver."
|
||||
echo -e "[${listtype^^}] You're already at the latest version."
|
||||
return 3
|
||||
else
|
||||
echo -e "[${listtype^^}] Local version: $local_list_ver, latest version: $list_ver."
|
||||
fi
|
||||
|
||||
if ! curl -fsSL "https://raw.githubusercontent.com/$listrepo/$list_sha/$listname" -o "$TEMP_DIR/$listname" || [ ! -s "$TEMP_DIR/$listname" ]; then
|
||||
rm -f "$TEMP_DIR/$listname"
|
||||
echo -e "[${listtype^^}] Update failed."
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -f "$TEMP_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}"
|
||||
echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
|
||||
echo -e "[${listtype^^}] Successfully updated."
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
check_list_update "china_ip4" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt"
|
||||
check_list_update "china_ip6" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt"
|
||||
check_list_update "gfw_list" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
|
||||
check_list_update "china_list" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \
|
||||
sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt"
|
||||
|
||||
rm -rf "$TEMP_DIR"
|
||||
@ -1,55 +0,0 @@
|
||||
name: Rescan Translation
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'htdocs/**'
|
||||
- 'root/**'
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete old Releases and Workflows
|
||||
uses: ophub/delete-releases-workflows@main
|
||||
with:
|
||||
gh_token: ${{secrets.GITHUB_TOKEN}}
|
||||
delete_workflows: true
|
||||
workflows_keep_day: 0
|
||||
|
||||
scan:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: cleanup
|
||||
steps:
|
||||
- name: Checkout source tree
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: Rescan translation
|
||||
env:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
run: |
|
||||
sudo -E apt -yqq update
|
||||
sudo -E apt -yqq install gettext
|
||||
|
||||
curl -LO "https://github.com/openwrt/luci/raw/master/build/i18n-scan.pl"
|
||||
curl -LO "https://github.com/openwrt/luci/raw/master/build/i18n-update.pl"
|
||||
perl "i18n-scan.pl" . > "po/templates/homeproxy.pot"
|
||||
perl "i18n-update.pl" "po"
|
||||
find po/ -name '*.po~' -exec rm -f {} \;
|
||||
rm -f "i18n-scan.pl" "i18n-update.pl"
|
||||
[ -z "$(git status -s)" ] || echo -e "CHANGE_STAT=1" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Commit changes
|
||||
if: ${{ env.CHANGE_STAT }}
|
||||
run: |
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git config --local user.email "<github-actions[bot]@users.noreply.github.com>"
|
||||
git add .
|
||||
git commit -m "chore(po): rescan translation"
|
||||
git push -f origin HEAD:main
|
||||
70
homeproxy/.github/workflows/Update-Geodata.yml
vendored
70
homeproxy/.github/workflows/Update-Geodata.yml
vendored
@ -1,70 +0,0 @@
|
||||
name: Update Geodata
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: 0 0 * * 0
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete old Releases and Workflows
|
||||
uses: ophub/delete-releases-workflows@main
|
||||
with:
|
||||
gh_token: ${{secrets.GITHUB_TOKEN}}
|
||||
delete_workflows: true
|
||||
workflows_keep_day: 0
|
||||
|
||||
update:
|
||||
runs-on: ubuntu-22.04
|
||||
needs: cleanup
|
||||
steps:
|
||||
- name: Checkout Projects
|
||||
uses: actions/checkout@main
|
||||
|
||||
- name: Update Geodata
|
||||
run: |
|
||||
RES_RULE="surge"
|
||||
RES_PATH="root/etc/homeproxy/resources"
|
||||
CFG_PATH="root/etc/config/homeproxy"
|
||||
|
||||
rm -rf ./$RES_PATH/*
|
||||
|
||||
git clone -q --depth=1 --single-branch --branch "release" "https://github.com/Loyalsoldier/surge-rules.git" ./$RES_RULE/
|
||||
cd ./$RES_RULE/ && RES_VER=$(git log -1 --pretty=format:'%s' | grep -o "[0-9]*")
|
||||
|
||||
#更新大陆列表
|
||||
echo "RES_VER=$RES_VER" >> $GITHUB_ENV
|
||||
echo $RES_VER | tee china_ip4.ver china_ip6.ver china_list.ver gfw_list.ver
|
||||
|
||||
awk -F, '/^IP-CIDR,/{print $2 > "china_ip4.txt"} /^IP-CIDR6,/{print $2 > "china_ip6.txt"}' cncidr.txt
|
||||
sed 's/^\.//g' direct.txt > china_list.txt ; sed 's/^\.//g' gfw.txt > gfw_list.txt
|
||||
|
||||
mv -f ./{china_*,gfw_list}.{ver,txt} ../$RES_PATH/
|
||||
|
||||
#更新TG列表
|
||||
TG_IPV4=$(grep "^IP-CIDR," "telegramcidr.txt" | sed "s/IP-CIDR,\(.*\)/list wan_proxy_ipv4_ips '\1'/g")
|
||||
TG_IPV6=$(grep "^IP-CIDR6," "telegramcidr.txt" | sed "s/IP-CIDR6,\(.*\)/list wan_proxy_ipv6_ips '\1'/g")
|
||||
TG_IPV4_ENTRIES=$(echo "$TG_IPV4" | sed ':a;N;$!ba; s/\n/\\\n/g')
|
||||
TG_IPV6_ENTRIES=$(echo "$TG_IPV6" | sed ':a;N;$!ba; s/\n/\\\n/g')
|
||||
|
||||
sed -i "/wan_proxy_/d" ../$CFG_PATH
|
||||
sed -i "/option lan_proxy_mode 'disabled'/a $TG_IPV6_ENTRIES" ../$CFG_PATH
|
||||
sed -i "/option lan_proxy_mode 'disabled'/a $TG_IPV4_ENTRIES" ../$CFG_PATH
|
||||
sed -i "/list wan_proxy_/ s/^/ /" ../$CFG_PATH
|
||||
|
||||
cd .. && rm -rf ./$RES_RULE/ && echo "homeproxy date has been updated!"
|
||||
|
||||
- name: Commit Changes
|
||||
run: |
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git config --local user.email "<github-actions[bot]@users.noreply.github.com>"
|
||||
git add .
|
||||
git commit -m "chore(resources): update geodata to ${{env.RES_VER}}"
|
||||
git push -f origin HEAD:main
|
||||
@ -1,340 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your
|
||||
freedom to share and change it. By contrast, the GNU General Public
|
||||
License is intended to guarantee your freedom to share and change free
|
||||
software--to make sure the software is free for all its users. This
|
||||
General Public License applies to most of the Free Software
|
||||
Foundation's software and to any other program whose authors commit to
|
||||
using it. (Some other Free Software Foundation software is covered by
|
||||
the GNU Library General Public License instead.) You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
this service if you wish), that you receive source code or can get it
|
||||
if you want it, that you can change the software or use pieces of it
|
||||
in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid
|
||||
anyone to deny you these rights or to ask you to surrender the rights.
|
||||
These restrictions translate to certain responsibilities for you if you
|
||||
distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must give the recipients all the rights that
|
||||
you have. You must make sure that they, too, receive or can get the
|
||||
source code. And you must show them these terms so they know their
|
||||
rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and
|
||||
(2) offer you this license which gives you legal permission to copy,
|
||||
distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain
|
||||
that everyone understands that there is no warranty for this free
|
||||
software. If the software is modified by someone else and passed on, we
|
||||
want its recipients to know that what they have is not the original, so
|
||||
that any problems introduced by others will not reflect on the original
|
||||
authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software
|
||||
patents. We wish to avoid the danger that redistributors of a free
|
||||
program will individually obtain patent licenses, in effect making the
|
||||
program proprietary. To prevent this, we have made it clear that any
|
||||
patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains
|
||||
a notice placed by the copyright holder saying it may be distributed
|
||||
under the terms of this General Public License. The "Program", below,
|
||||
refers to any such program or work, and a "work based on the Program"
|
||||
means either the Program or any derivative work under copyright law:
|
||||
that is to say, a work containing the Program or a portion of it,
|
||||
either verbatim or with modifications and/or translated into another
|
||||
language. (Hereinafter, translation is included without limitation in
|
||||
the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not
|
||||
covered by this License; they are outside its scope. The act of
|
||||
running the Program is not restricted, and the output from the Program
|
||||
is covered only if its contents constitute a work based on the
|
||||
Program (independent of having been made by running the Program).
|
||||
Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's
|
||||
source code as you receive it, in any medium, provided that you
|
||||
conspicuously and appropriately publish on each copy an appropriate
|
||||
copyright notice and disclaimer of warranty; keep intact all the
|
||||
notices that refer to this License and to the absence of any warranty;
|
||||
and give any other recipients of the Program a copy of this License
|
||||
along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and
|
||||
you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion
|
||||
of it, thus forming a work based on the Program, and copy and
|
||||
distribute such modifications or work under the terms of Section 1
|
||||
above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices
|
||||
stating that you changed the files and the date of any change.
|
||||
|
||||
b) You must cause any work that you distribute or publish, that in
|
||||
whole or in part contains or is derived from the Program or any
|
||||
part thereof, to be licensed as a whole at no charge to all third
|
||||
parties under the terms of this License.
|
||||
|
||||
c) If the modified program normally reads commands interactively
|
||||
when run, you must cause it, when started running for such
|
||||
interactive use in the most ordinary way, to print or display an
|
||||
announcement including an appropriate copyright notice and a
|
||||
notice that there is no warranty (or else, saying that you provide
|
||||
a warranty) and that users may redistribute the program under
|
||||
these conditions, and telling the user how to view a copy of this
|
||||
License. (Exception: if the Program itself is interactive but
|
||||
does not normally print such an announcement, your work based on
|
||||
the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If
|
||||
identifiable sections of that work are not derived from the Program,
|
||||
and can be reasonably considered independent and separate works in
|
||||
themselves, then this License, and its terms, do not apply to those
|
||||
sections when you distribute them as separate works. But when you
|
||||
distribute the same sections as part of a whole which is a work based
|
||||
on the Program, the distribution of the whole must be on the terms of
|
||||
this License, whose permissions for other licensees extend to the
|
||||
entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest
|
||||
your rights to work written entirely by you; rather, the intent is to
|
||||
exercise the right to control the distribution of derivative or
|
||||
collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program
|
||||
with the Program (or with a work based on the Program) on a volume of
|
||||
a storage or distribution medium does not bring the other work under
|
||||
the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it,
|
||||
under Section 2) in object code or executable form under the terms of
|
||||
Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable
|
||||
source code, which must be distributed under the terms of Sections
|
||||
1 and 2 above on a medium customarily used for software interchange; or,
|
||||
|
||||
b) Accompany it with a written offer, valid for at least three
|
||||
years, to give any third party, for a charge no more than your
|
||||
cost of physically performing source distribution, a complete
|
||||
machine-readable copy of the corresponding source code, to be
|
||||
distributed under the terms of Sections 1 and 2 above on a medium
|
||||
customarily used for software interchange; or,
|
||||
|
||||
c) Accompany it with the information you received as to the offer
|
||||
to distribute corresponding source code. (This alternative is
|
||||
allowed only for noncommercial distribution and only if you
|
||||
received the program in object code or executable form with such
|
||||
an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for
|
||||
making modifications to it. For an executable work, complete source
|
||||
code means all the source code for all modules it contains, plus any
|
||||
associated interface definition files, plus the scripts used to
|
||||
control compilation and installation of the executable. However, as a
|
||||
special exception, the source code distributed need not include
|
||||
anything that is normally distributed (in either source or binary
|
||||
form) with the major components (compiler, kernel, and so on) of the
|
||||
operating system on which the executable runs, unless that component
|
||||
itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering
|
||||
access to copy from a designated place, then offering equivalent
|
||||
access to copy the source code from the same place counts as
|
||||
distribution of the source code, even though third parties are not
|
||||
compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program
|
||||
except as expressly provided under this License. Any attempt
|
||||
otherwise to copy, modify, sublicense or distribute the Program is
|
||||
void, and will automatically terminate your rights under this License.
|
||||
However, parties who have received copies, or rights, from you under
|
||||
this License will not have their licenses terminated so long as such
|
||||
parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not
|
||||
signed it. However, nothing else grants you permission to modify or
|
||||
distribute the Program or its derivative works. These actions are
|
||||
prohibited by law if you do not accept this License. Therefore, by
|
||||
modifying or distributing the Program (or any work based on the
|
||||
Program), you indicate your acceptance of this License to do so, and
|
||||
all its terms and conditions for copying, distributing or modifying
|
||||
the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the
|
||||
Program), the recipient automatically receives a license from the
|
||||
original licensor to copy, distribute or modify the Program subject to
|
||||
these terms and conditions. You may not impose any further
|
||||
restrictions on the recipients' exercise of the rights granted herein.
|
||||
You are not responsible for enforcing compliance by third parties to
|
||||
this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent
|
||||
infringement or for any other reason (not limited to patent issues),
|
||||
conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot
|
||||
distribute so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you
|
||||
may not distribute the Program at all. For example, if a patent
|
||||
license would not permit royalty-free redistribution of the Program by
|
||||
all those who receive copies directly or indirectly through you, then
|
||||
the only way you could satisfy both it and this License would be to
|
||||
refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under
|
||||
any particular circumstance, the balance of the section is intended to
|
||||
apply and the section as a whole is intended to apply in other
|
||||
circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any
|
||||
patents or other property right claims or to contest validity of any
|
||||
such claims; this section has the sole purpose of protecting the
|
||||
integrity of the free software distribution system, which is
|
||||
implemented by public license practices. Many people have made
|
||||
generous contributions to the wide range of software distributed
|
||||
through that system in reliance on consistent application of that
|
||||
system; it is up to the author/donor to decide if he or she is willing
|
||||
to distribute software through any other system and a licensee cannot
|
||||
impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to
|
||||
be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in
|
||||
certain countries either by patents or by copyrighted interfaces, the
|
||||
original copyright holder who places the Program under this License
|
||||
may add an explicit geographical distribution limitation excluding
|
||||
those countries, so that distribution is permitted only in or among
|
||||
countries not thus excluded. In such case, this License incorporates
|
||||
the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions
|
||||
of the General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program
|
||||
specifies a version number of this License which applies to it and "any
|
||||
later version", you have the option of following the terms and conditions
|
||||
either of that version or of any later version published by the Free
|
||||
Software Foundation. If the Program does not specify a version number of
|
||||
this License, you may choose any version ever published by the Free Software
|
||||
Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free
|
||||
programs whose distribution conditions are different, write to the author
|
||||
to ask for permission. For software which is copyrighted by the Free
|
||||
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||
make exceptions for this. Our decision will be guided by the two goals
|
||||
of preserving the free status of all derivatives of our free software and
|
||||
of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||
REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGES.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
convey the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this
|
||||
when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, the commands you use may
|
||||
be called something other than `show w' and `show c'; they could even be
|
||||
mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your
|
||||
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||
necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||
|
||||
<signature of Ty Coon>, 1 April 1989
|
||||
Ty Coon, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into
|
||||
proprietary programs. If your program is a subroutine library, you may
|
||||
consider it more useful to permit linking proprietary applications with the
|
||||
library. If this is what you want to do, use the GNU Library General
|
||||
Public License instead of this License.
|
||||
@ -1,33 +0,0 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:= \
|
||||
+sing-box \
|
||||
+firewall4 \
|
||||
+kmod-nft-tproxy \
|
||||
+curl \
|
||||
+jsonfilter \
|
||||
+ucode-mod-digest
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy
|
||||
PKG_VERSION=$(PKG_SRC_VERSION)
|
||||
PKG_RELEASE:=15
|
||||
PKG_PO_VERSION=$(PKG_SRC_VERSION)-r$(PKG_RELEASE)
|
||||
|
||||
define Package/luci-app-homeproxy/conffiles
|
||||
/etc/config/homeproxy
|
||||
/etc/homeproxy/cache/
|
||||
/etc/homeproxy/certs/
|
||||
/etc/homeproxy/ruleset/
|
||||
/etc/homeproxy/resources/direct_list.txt
|
||||
/etc/homeproxy/resources/proxy_list.txt
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@ -1 +0,0 @@
|
||||
自用版homeproxy。
|
||||
@ -1,333 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2025 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_length: {
|
||||
/* AEAD */
|
||||
'aes-128-gcm': 0,
|
||||
'aes-192-gcm': 0,
|
||||
'aes-256-gcm': 0,
|
||||
'chacha20-ietf-poly1305': 0,
|
||||
'xchacha20-ietf-poly1305': 0,
|
||||
/* AEAD 2022 */
|
||||
'2022-blake3-aes-128-gcm': 16,
|
||||
'2022-blake3-aes-256-gcm': 32,
|
||||
'2022-blake3-chacha20-poly1305': 32
|
||||
},
|
||||
|
||||
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'
|
||||
],
|
||||
|
||||
CBIStaticList: form.DynamicList.extend({
|
||||
__name__: 'CBI.StaticList',
|
||||
|
||||
renderWidget: function(/* ... */) {
|
||||
let dl = form.DynamicList.prototype.renderWidget.apply(this, arguments);
|
||||
dl.querySelector('.add-item ul > li[data-value="-"]')?.remove();
|
||||
return dl;
|
||||
}
|
||||
}),
|
||||
|
||||
calcStringMD5(e) {
|
||||
/* Thanks to https://stackoverflow.com/a/41602636 */
|
||||
let h = (a, b) => {
|
||||
let c, d, e, f, g;
|
||||
c = a & 2147483648;
|
||||
d = b & 2147483648;
|
||||
e = a & 1073741824;
|
||||
f = b & 1073741824;
|
||||
g = (a & 1073741823) + (b & 1073741823);
|
||||
return e & f ? g ^ 2147483648 ^ c ^ d : e | f ? g & 1073741824 ? g ^ 3221225472 ^ c ^ d : g ^ 1073741824 ^ c ^ d : g ^ c ^ d;
|
||||
}, k = (a, b, c, d, e, f, g) => h((a = h(a, h(h(b & c | ~b & d, e), g))) << f | a >>> 32 - f, b),
|
||||
l = (a, b, c, d, e, f, g) => h((a = h(a, h(h(b & d | c & ~d, e), g))) << f | a >>> 32 - f, b),
|
||||
m = (a, b, c, d, e, f, g) => h((a = h(a, h(h(b ^ c ^ d, e), g))) << f | a >>> 32 - f, b),
|
||||
n = (a, b, c, d, e, f, g) => h((a = h(a, h(h(c ^ (b | ~d), e), g))) << f | a >>> 32 - f, b),
|
||||
p = a => { let b = '', d = ''; for (let c = 0; c <= 3; c++) d = a >>> 8 * c & 255, d = '0' + d.toString(16), b += d.substr(d.length - 2, 2); return b; };
|
||||
|
||||
let f = [], q, r, s, t, a, b, c, d;
|
||||
e = (() => {
|
||||
e = e.replace(/\r\n/g, '\n');
|
||||
let b = '';
|
||||
for (let d = 0; d < e.length; d++) {
|
||||
let c = e.charCodeAt(d);
|
||||
b += c < 128 ? String.fromCharCode(c) : c < 2048 ? String.fromCharCode(c >> 6 | 192) + String.fromCharCode(c & 63 | 128) :
|
||||
String.fromCharCode(c >> 12 | 224) + String.fromCharCode(c >> 6 & 63 | 128) + String.fromCharCode(c & 63 | 128);
|
||||
}
|
||||
return b;
|
||||
})();
|
||||
f = (() => {
|
||||
let c = e.length, a = c + 8, d = 16 * ((a - a % 64) / 64 + 1), b = Array(d - 1), f = 0, g = 0;
|
||||
for (; g < c;) a = (g - g % 4) / 4, f = g % 4 * 8, b[a] |= e.charCodeAt(g) << f, g++;
|
||||
a = (g - g % 4) / 4, b[a] |= 128 << g % 4 * 8, b[d - 2] = c << 3, b[d - 1] = c >>> 29;
|
||||
return b;
|
||||
})();
|
||||
|
||||
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(str) {
|
||||
if (!str)
|
||||
return null;
|
||||
|
||||
/* Thanks to luci-app-ssr-plus */
|
||||
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||
let padding = (4 - str.length % 4) % 4;
|
||||
if (padding)
|
||||
str = str + Array(padding + 1).join('=');
|
||||
|
||||
return decodeURIComponent(Array.prototype.map.call(atob(str), (c) =>
|
||||
'%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
||||
).join(''));
|
||||
},
|
||||
|
||||
getBuiltinFeatures() {
|
||||
const callGetSingBoxFeatures = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'singbox_get_features',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return L.resolveDefault(callGetSingBoxFeatures(), {});
|
||||
},
|
||||
|
||||
generateRand(type, length) {
|
||||
let byteArr;
|
||||
if (['base64', 'hex'].includes(type))
|
||||
byteArr = crypto.getRandomValues(new Uint8Array(length));
|
||||
switch (type) {
|
||||
case 'base64':
|
||||
/* Thanks to https://stackoverflow.com/questions/9267899 */
|
||||
return btoa(String.fromCharCode.apply(null, byteArr));
|
||||
case 'hex':
|
||||
return Array.from(byteArr, (byte) =>
|
||||
(byte & 255).toString(16).padStart(2, '0')
|
||||
).join('');
|
||||
case 'uuid':
|
||||
/* Thanks to https://stackoverflow.com/a/2117523 */
|
||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
|
||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
};
|
||||
},
|
||||
|
||||
loadDefaultLabel(uciconfig, ucisection) {
|
||||
let label = uci.get(uciconfig, ucisection, 'label');
|
||||
if (label) {
|
||||
return label;
|
||||
} else {
|
||||
uci.set(uciconfig, ucisection, 'label', ucisection);
|
||||
return ucisection;
|
||||
}
|
||||
},
|
||||
|
||||
loadModalTitle(title, addtitle, uciconfig, ucisection) {
|
||||
let label = uci.get(uciconfig, ucisection, 'label');
|
||||
return label ? title + ' » ' + label : addtitle;
|
||||
},
|
||||
|
||||
renderSectionAdd(section, extra_class) {
|
||||
let el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
|
||||
nameEl = el.querySelector('.cbi-section-create-name');
|
||||
ui.addValidator(nameEl, 'uciname', true, (v) => {
|
||||
let button = el.querySelector('.cbi-section-create > .cbi-button-add');
|
||||
let uciconfig = section.uciconfig || section.map.config;
|
||||
|
||||
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(_option, type, filename, ev) {
|
||||
const callWriteCertificate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'certificate_write',
|
||||
params: ['filename'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return ui.uploadFile('/tmp/homeproxy_certificate.tmp', 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(length, section_id, value) {
|
||||
/* Thanks to luci-proto-wireguard */
|
||||
if (section_id && value)
|
||||
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;
|
||||
},
|
||||
|
||||
validateCertificatePath(section_id, value) {
|
||||
if (section_id && value)
|
||||
if (!value.match(/^(\/etc\/homeproxy\/certs\/|\/etc\/acme\/|\/etc\/ssl\/).+$/))
|
||||
return _('Expecting: %s').format(_('/etc/homeproxy/certs/..., /etc/acme/..., /etc/ssl/...'));
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
|
||||
validateUniqueValue(uciconfig, ucisection, ucioption, section_id, value) {
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
if (ucioption === 'node' && value === 'urltest')
|
||||
return true;
|
||||
|
||||
let 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(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;
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,887 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require form';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
'require homeproxy as hp';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const CBIGenValue = form.Value.extend({
|
||||
__name__: 'CBI.GenValue',
|
||||
|
||||
renderWidget(/* ... */) {
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
if (!this.password)
|
||||
node.classList.add('control-group');
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
title: _('Generate'),
|
||||
click: ui.createHandlerFn(this, handleGenKey, this.hp_options || this.option)
|
||||
}, [ _('Generate') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
});
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('homeproxy'), {}).then((res) => {
|
||||
let isRunning = false;
|
||||
try {
|
||||
isRunning = res['homeproxy']['instances']['sing-box-s']['running'];
|
||||
} catch (e) { }
|
||||
return isRunning;
|
||||
});
|
||||
}
|
||||
|
||||
function renderStatus(isRunning, version) {
|
||||
let spanTemp = '<em><span style="color:%s"><strong>%s (sing-box v%s) %s</strong></span></em>';
|
||||
let renderHTML;
|
||||
if (isRunning)
|
||||
renderHTML = spanTemp.format('green', _('HomeProxy Server'), version, _('RUNNING'));
|
||||
else
|
||||
renderHTML = spanTemp.format('red', _('HomeProxy Server'), version, _('NOT RUNNING'));
|
||||
|
||||
return renderHTML;
|
||||
}
|
||||
|
||||
function handleGenKey(option) {
|
||||
let section_id = this.section.section;
|
||||
let type = this.section.getOption('type')?.formvalue(section_id);
|
||||
let widget = L.bind((option) => {
|
||||
return this.map.findElement('id', 'widget.' + this.cbid(section_id).replace(/\.[^\.]+$/, '.') + option);
|
||||
}, this);
|
||||
|
||||
const callSingBoxGenerator = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'singbox_generator',
|
||||
params: ['type', 'params'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
if (typeof option === 'object') {
|
||||
return callSingBoxGenerator(option.type, option.params).then((res) => {
|
||||
if (res.result)
|
||||
option.callback.call(this, res.result).forEach(([k, v]) => {
|
||||
widget(k).value = v ?? '';
|
||||
});
|
||||
else
|
||||
ui.addNotification(null, E('p', _('Failed to generate %s, error: %s.').format(type, res.error)));
|
||||
});
|
||||
} else {
|
||||
let password, required_method;
|
||||
|
||||
if (option === 'uuid')
|
||||
required_method = 'uuid';
|
||||
else if (type === 'shadowsocks')
|
||||
required_method = this.section.getOption('shadowsocks_encrypt_method')?.formvalue(section_id);
|
||||
|
||||
switch (required_method) {
|
||||
case 'none':
|
||||
password = '';
|
||||
break;
|
||||
case 'uuid':
|
||||
password = hp.generateRand('uuid');
|
||||
break;
|
||||
default:
|
||||
password = hp.generateRand('hex', 16);
|
||||
break;
|
||||
}
|
||||
/* AEAD */
|
||||
((length) => {
|
||||
if (length && length > 0)
|
||||
password = hp.generateRand('base64', length);
|
||||
})(hp.shadowsocks_encrypt_length[required_method]);
|
||||
|
||||
return widget(option).value = password;
|
||||
}
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
uci.load('homeproxy'),
|
||||
hp.getBuiltinFeatures()
|
||||
]);
|
||||
},
|
||||
|
||||
render(data) {
|
||||
let m, s, o;
|
||||
let features = data[1];
|
||||
|
||||
m = new form.Map('homeproxy', _('HomeProxy Server'),
|
||||
_('The modern ImmortalWrt proxy platform for ARM64/AMD64.'));
|
||||
|
||||
s = m.section(form.TypedSection);
|
||||
s.render = function() {
|
||||
poll.add(() => {
|
||||
return L.resolveDefault(getServiceStatus()).then((res) => {
|
||||
let view = document.getElementById('service_status');
|
||||
view.innerHTML = renderStatus(res, features.version);
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data...'))
|
||||
]);
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'server', 'homeproxy', _('Global settings'));
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.rmempty = false;
|
||||
|
||||
s = m.section(form.GridSection, 'server', _('Server Settings'));
|
||||
s.addremove = true;
|
||||
s.rowcolors = 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.Flag, 'firewall', _('Firewall'),
|
||||
_('Allow access from the Internet.'));
|
||||
o.editable = true;
|
||||
|
||||
o = s.option(form.ListValue, 'type', _('Type'));
|
||||
o.value('anytls', _('AnyTLS'));
|
||||
o.value('http', _('HTTP'));
|
||||
if (features.with_quic) {
|
||||
o.value('hysteria', _('Hysteria'));
|
||||
o.value('hysteria2', _('Hysteria2'));
|
||||
o.value('naive', _('NaïveProxy'));
|
||||
}
|
||||
o.value('mixed', _('Mixed'));
|
||||
o.value('shadowsocks', _('Shadowsocks'));
|
||||
o.value('socks', _('Socks'));
|
||||
o.value('trojan', _('Trojan'));
|
||||
if (features.with_quic)
|
||||
o.value('tuic', _('Tuic'));
|
||||
o.value('vless', _('VLESS'));
|
||||
o.value('vmess', _('VMess'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'address', _('Listen address'));
|
||||
o.placeholder = '::';
|
||||
o.datatype = 'ipaddr';
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'port', _('Listen 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', 'mixed');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'socks');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(CBIGenValue, 'password', _('Password'));
|
||||
o.password = true;
|
||||
o.depends('type', 'anytls');
|
||||
o.depends({'type': /^(http|mixed|naive|socks)$/, 'username': /[\s\S]/});
|
||||
o.depends('type', 'hysteria2');
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'tuic');
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
let type = this.section.formvalue(section_id, 'type');
|
||||
let required_type = [ 'anytls', 'http', 'mixed', 'naive', 'socks', 'shadowsocks', 'trojan' ];
|
||||
|
||||
if (required_type.includes(type)) {
|
||||
if (type === 'shadowsocks') {
|
||||
let encmode = this.section.formvalue(section_id, 'shadowsocks_encrypt_method');
|
||||
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;
|
||||
|
||||
/* AnyTLS config */
|
||||
o = s.option(form.DynamicList, 'anytls_padding_scheme', _('Padding scheme'),
|
||||
_('AnyTLS padding scheme in array.'));
|
||||
o.depends('type', 'anytls');
|
||||
o.modalonly = true;
|
||||
|
||||
/* Hysteria (2) 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.depends('type', 'hysteria2');
|
||||
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.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'hysteria_auth_type', _('Authentication type'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('base64', _('Base64'));
|
||||
o.value('string', _('String'));
|
||||
o.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_auth_payload', _('Authentication payload'));
|
||||
o.password = true;
|
||||
o.depends({'type': 'hysteria', 'hysteria_auth_type': /[\s\S]/});
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('salamander', _('Salamander'));
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(CBIGenValue, 'hysteria_obfs_password', _('Obfuscate password'));
|
||||
o.password = true;
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends({'type': 'hysteria2', 'hysteria_obfs_type': /[\s\S]/});
|
||||
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.depends('type', 'hysteria');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'),
|
||||
_('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.'));
|
||||
o.depends({'type': 'hysteria2', 'hysteria_down_mbps': '', 'hysteria_up_mbps': ''});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'hysteria_masquerade', _('Masquerade'),
|
||||
_('HTTP/3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
/* Hysteria (2) config end */
|
||||
|
||||
/* Shadowsocks config */
|
||||
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
||||
for (let i of hp.shadowsocks_encrypt_methods)
|
||||
o.value(i);
|
||||
o.default = 'aes-128-gcm';
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.modalonly = true;
|
||||
|
||||
/* Tuic config start */
|
||||
o = s.option(CBIGenValue, 'uuid', _('UUID'));
|
||||
o.password = true;
|
||||
o.depends('type', 'tuic');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.validate = hp.validateUUID;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tuic_congestion_control', _('Congestion control algorithm'),
|
||||
_('QUIC congestion control algorithm.'));
|
||||
o.value('cubic');
|
||||
o.value('new_reno');
|
||||
o.value('bbr');
|
||||
o.default = 'cubic';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tuic_auth_timeout', _('Auth timeout'),
|
||||
_('How long the server should wait for the client to send the authentication command (in seconds).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '3';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tuic_enable_zero_rtt', _('Enable 0-RTT handshake'),
|
||||
_('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.<br/>' +
|
||||
'Disabling this is highly recommended, as it is vulnerable to replay attacks.'));
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tuic_heartbeat', _('Heartbeat interval'),
|
||||
_('Interval for sending heartbeat packets for keeping the connection alive (in seconds).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '10';
|
||||
o.depends('type', 'tuic');
|
||||
o.modalonly = true;
|
||||
/* Tuic config end */
|
||||
|
||||
/* VLESS / VMess config start */
|
||||
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
|
||||
o.value('', _('None'));
|
||||
o.value('xtls-rprx-vision');
|
||||
o.depends('type', 'vless');
|
||||
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('httpupgrade', _('HTTPUpgrade'));
|
||||
o.value('quic', _('QUIC'));
|
||||
o.value('ws', _('WebSocket'));
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.onchange = function(ev, section_id, value) {
|
||||
let 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.');
|
||||
|
||||
let tls_element = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
|
||||
if ((value === 'http' && tls_element.checked) || (value === 'grpc' && !features.with_grpc))
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.');
|
||||
else if (value === 'grpc' && features.with_grpc)
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
_('If the transport doesn\'t see any activity after a duration of this time (in seconds), it pings the client to check if the connection is still active.');
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
/* gRPC config start */
|
||||
o = s.option(form.Value, 'grpc_servicename', _('gRPC service name'));
|
||||
o.depends('transport', 'grpc');
|
||||
o.modalonly = true;
|
||||
|
||||
/* gRPC config end */
|
||||
|
||||
/* HTTP(Upgrade) config start */
|
||||
o = s.option(form.DynamicList, 'http_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('transport', 'http');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'httpupgrade_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('transport', 'httpupgrade');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_path', _('Path'));
|
||||
o.depends('transport', 'http');
|
||||
o.depends('transport', 'httpupgrade');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_method', _('Method'));
|
||||
o.depends('transport', 'http');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'http_idle_timeout', _('Idle timeout'),
|
||||
_('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('transport', 'grpc');
|
||||
o.depends({'transport': 'http', 'tls': '1'});
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.with_grpc) {
|
||||
o = s.option(form.Value, 'http_ping_timeout', _('Ping timeout'),
|
||||
_('The timeout (in seconds) that after performing a keepalive check, the client will wait for activity. If no activity is detected, the connection will be closed.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('transport', 'grpc');
|
||||
o.modalonly = true;
|
||||
}
|
||||
/* HTTP config end */
|
||||
|
||||
/* WebSocket config start */
|
||||
o = s.option(form.Value, 'ws_host', _('Host'));
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'ws_path', _('Path'));
|
||||
o.depends('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.value('2048');
|
||||
o.depends('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.value('Sec-WebSocket-Protocol');
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
/* WebSocket config end */
|
||||
|
||||
/* Transport config end */
|
||||
|
||||
/* Mux config start */
|
||||
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'multiplex_padding', _('Enable padding'));
|
||||
o.depends('multiplex', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.hp_has_tcp_brutal) {
|
||||
o = s.option(form.Flag, 'multiplex_brutal', _('Enable TCP Brutal'),
|
||||
_('Enable TCP Brutal congestion control algorithm'));
|
||||
o.depends('multiplex', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'multiplex_brutal_down', _('Download bandwidth'),
|
||||
_('Download bandwidth in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('multiplex_brutal', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'multiplex_brutal_up', _('Upload bandwidth'),
|
||||
_('Upload bandwidth in Mbps.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('multiplex_brutal', '1');
|
||||
o.modalonly = true;
|
||||
}
|
||||
/* Mux config end */
|
||||
|
||||
/* TLS config start */
|
||||
o = s.option(form.Flag, 'tls', _('TLS'));
|
||||
o.depends('type', 'anytls');
|
||||
o.depends('type', 'http');
|
||||
o.depends('type', 'hysteria');
|
||||
o.depends('type', 'hysteria2');
|
||||
o.depends('type', 'naive');
|
||||
o.depends('type', 'trojan');
|
||||
o.depends('type', 'tuic');
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.rmempty = false;
|
||||
o.validate = function(section_id, value) {
|
||||
if (section_id) {
|
||||
let type = this.map.lookupOption('type', section_id)[0].formvalue(section_id);
|
||||
let tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
|
||||
|
||||
if (['hysteria', 'hysteria2', 'tuic'].includes(type)) {
|
||||
tls.checked = true;
|
||||
tls.disabled = true;
|
||||
} else {
|
||||
tls.disabled = null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
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 (let 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 (let i of hp.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(hp.CBIStaticList, '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 (let i of hp.tls_cipher_suites)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.optional = true;
|
||||
o.modalonly = true;
|
||||
|
||||
if (features.with_acme) {
|
||||
o = s.option(form.Flag, 'tls_acme', _('Enable ACME'),
|
||||
_('Use ACME TLS certificate issuer.'));
|
||||
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_dns01_challenge', _('DNS-01 challenge'))
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
|
||||
o.value('alidns', _('AliDNS'));
|
||||
o.value('cfdns', _('Cloudflare'));
|
||||
o.depends('tls_dns01_challenge', '1');
|
||||
o.default = 'cfdns';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_akid', _('Access key ID'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_aksec', _('Access key secret'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_ali_rid', _('Region ID'));
|
||||
o.depends('tls_dns01_provider', 'alidns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'cfdns');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_dhc', _('Disable HTTP challenge'));
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tls_acme_dtac', _('Disable TLS ALPN challenge'));
|
||||
o.depends('tls_dns01_challenge', '0');
|
||||
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_dns01_challenge', '0');
|
||||
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_dns01_challenge', '0');
|
||||
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.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_ea_keyid', _('External account key ID'));
|
||||
o.password = true;
|
||||
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.password = true;
|
||||
o.depends('tls_acme_external_account', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
o = s.option(form.Flag, 'tls_reality', _('REALITY'));
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'type': /^(anytls|vless)$/});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'type': /^(anytls|vless)$/});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(CBIGenValue, 'tls_reality_private_key', _('REALITY private key'));
|
||||
o.password = true;
|
||||
o.hp_options = {
|
||||
type: 'reality-keypair',
|
||||
params: '',
|
||||
callback: function(result) {
|
||||
return [
|
||||
[this.option, result.private_key],
|
||||
['tls_reality_public_key', result.public_key]
|
||||
]
|
||||
}
|
||||
}
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_public_key', _('REALITY public key'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_max_time_difference', _('Max time difference'),
|
||||
_('The maximum time difference between the server and the client.'));
|
||||
o.depends('tls_reality', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_server_addr', _('Handshake server address'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('tls_reality', '1');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_reality_server_port', _('Handshake server port'));
|
||||
o.datatype = 'port';
|
||||
o.depends('tls_reality', '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': '0', 'tls_reality': null});
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
|
||||
o.validate = hp.validateCertificatePath;
|
||||
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': '0', 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': '0', 'tls_reality': null});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': '0'});
|
||||
o.depends({'tls': '1', 'tls_acme': null, 'tls_reality': null});
|
||||
o.validate = hp.validateCertificatePath;
|
||||
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;
|
||||
|
||||
o = s.option(form.TextValue, 'tls_ech_key', _('ECH key'));
|
||||
o.placeholder = '-----BEGIN ECH KEYS-----\nACBE2+piYBLrOywCbRYU+ZpEkk8keeBlUXbKqLRmQ/68FwBL/g0ARwAAIAAgn8HI\n93RfdV/LaDk+LC9H4h+4WhVBFmWKdhiT3vvpGi8ACAABAAEAAQADABRvdXRlci1z\nbmkuYW55LmRvbWFpbgAA\n-----END ECH KEYS-----';
|
||||
o.monospace = true;
|
||||
o.cols = 30
|
||||
o.rows = 3;
|
||||
o.hp_options = {
|
||||
type: 'ech-keypair',
|
||||
params: '',
|
||||
callback: function(result) {
|
||||
return [
|
||||
[this.option, result.ech_key],
|
||||
['tls_ech_config', result.ech_cfg]
|
||||
]
|
||||
}
|
||||
}
|
||||
o.renderWidget = function(section_id, option_index, cfgvalue) {
|
||||
let node = form.TextValue.prototype.renderWidget.apply(this, arguments);
|
||||
const cbid = this.cbid(section_id) + '._outer_sni';
|
||||
|
||||
node.appendChild(E('div', { 'class': 'control-group' }, [
|
||||
E('input', {
|
||||
id: cbid,
|
||||
class: 'cbi-input-text',
|
||||
style: 'width: 10em',
|
||||
placeholder: 'outer-sni.any.domain'
|
||||
}),
|
||||
E('button', {
|
||||
class: 'cbi-button cbi-button-add',
|
||||
click: ui.createHandlerFn(this, () => {
|
||||
this.hp_options.params = document.getElementById(cbid).value;
|
||||
|
||||
return handleGenKey.call(this, this.hp_options);
|
||||
})
|
||||
}, [ _('Generate') ])
|
||||
]));
|
||||
|
||||
return node;
|
||||
}
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.TextValue, 'tls_ech_config', _('ECH config'));
|
||||
o.placeholder = '-----BEGIN ECH CONFIGS-----\nAEv+DQBHAAAgACCfwcj3dF91X8toOT4sL0fiH7haFUEWZYp2GJPe++kaLwAIAAEA\nAQABAAMAFG91dGVyLXNuaS5hbnkuZG9tYWluAAA=\n-----END ECH CONFIGS-----';
|
||||
o.monospace = true;
|
||||
o.cols = 30
|
||||
o.rows = 3;
|
||||
o.depends('tls', '1');
|
||||
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.depends({'network': 'udp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'tcp_multi_path', _('MultiPath TCP'));
|
||||
o.depends({'network': 'udp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'udp_fragment', _('UDP Fragment'),
|
||||
_('Enable UDP fragmentation.'));
|
||||
o.depends({'network': 'tcp', '!reverse': true});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'udp_timeout', _('UDP NAT expiration time'),
|
||||
_('In seconds.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '300';
|
||||
o.depends({'network': 'tcp', '!reverse': true});
|
||||
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;
|
||||
|
||||
o = s.option(widgets.DeviceSelect, 'bind_interface', _('Bind interface'),
|
||||
_('The network interface to bind to.'));
|
||||
o.multiple = false;
|
||||
o.noaliases = true;
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'reuse_addr', _('Reuse address'),
|
||||
_('Reuse listener address.'));
|
||||
o.modalonly = true;
|
||||
/* Extra settings end */
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@ -1,435 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2022-2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
/* Thanks to luci-app-aria2 */
|
||||
const css = ' \
|
||||
#log_textarea { \
|
||||
padding: 10px; \
|
||||
text-align: left; \
|
||||
} \
|
||||
#log_textarea pre { \
|
||||
padding: .5rem; \
|
||||
word-break: break-all; \
|
||||
margin: 0; \
|
||||
} \
|
||||
.description { \
|
||||
background-color: #33ccff; \
|
||||
}';
|
||||
|
||||
const hp_dir = '/var/run/homeproxy';
|
||||
|
||||
const connectionSites = [
|
||||
{ type: 'baidu', name: _('Baidu'), url: 'https://www.baidu.com/' },
|
||||
{ type: 'bilibili', name: _('Bilibili'), url: 'https://www.bilibili.com/' },
|
||||
{ type: 'jd', name: _('JD'), url: 'https://www.jd.com/' },
|
||||
{ type: 'google', name: _('Google'), url: 'https://www.google.com/' },
|
||||
{ type: 'github', name: _('GitHub'), url: 'https://github.com/' },
|
||||
{ type: 'youtube', name: _('YouTube'), url: 'https://www.youtube.com/' }
|
||||
];
|
||||
|
||||
const connectionTestTimeout = 10000;
|
||||
|
||||
function getConnectionStatus() {
|
||||
const callConnStat = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'connection_check',
|
||||
params: ['site'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Website')),
|
||||
E('th', { 'class': 'th' }, _('URL')),
|
||||
E('th', { 'class': 'th' }, _('Connectivity')),
|
||||
E('th', { 'class': 'th' }, _('Latency'))
|
||||
])
|
||||
]);
|
||||
const statusElements = {};
|
||||
const rows = connectionSites.map((site) => {
|
||||
const state = E('strong', { 'style': 'color:gray' }, '-');
|
||||
const latency = E('span', {}, '-');
|
||||
statusElements[site.type] = { state, latency };
|
||||
|
||||
return [
|
||||
site.name,
|
||||
E('a', {
|
||||
'href': site.url,
|
||||
'target': '_blank',
|
||||
'rel': 'noreferrer noopener',
|
||||
'style': 'word-break:break-all'
|
||||
}, site.url),
|
||||
state,
|
||||
latency
|
||||
];
|
||||
});
|
||||
cbi_update_table(table, rows);
|
||||
|
||||
let running = false;
|
||||
let generation = 0;
|
||||
let testButton;
|
||||
|
||||
const updateResult = (site, result) => {
|
||||
const elements = statusElements[site];
|
||||
if (!elements)
|
||||
return;
|
||||
|
||||
if (result?.result) {
|
||||
elements.state.style.setProperty('color', 'green');
|
||||
dom.content(elements.state, _('Success'));
|
||||
dom.content(elements.latency, _('%s ms').format(result.latency_ms));
|
||||
} else {
|
||||
elements.state.style.setProperty('color', 'red');
|
||||
dom.content(elements.state, result?.timed_out ? _('Timed out') : _('Failed'));
|
||||
dom.content(elements.latency, '-');
|
||||
}
|
||||
};
|
||||
|
||||
const runAllTests = () => {
|
||||
if (running)
|
||||
return Promise.resolve();
|
||||
|
||||
running = true;
|
||||
testButton.disabled = true;
|
||||
const currentGeneration = ++generation;
|
||||
connectionSites.forEach((site) => {
|
||||
const elements = statusElements[site.type];
|
||||
elements.state.style.setProperty('color', 'gray');
|
||||
dom.content(elements.state, _('Testing...'));
|
||||
dom.content(elements.latency, '-');
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = (result) => {
|
||||
if (settled)
|
||||
return;
|
||||
|
||||
settled = true;
|
||||
resolve(result);
|
||||
};
|
||||
const timer = window.setTimeout(() => finish({ timed_out: true }), connectionTestTimeout);
|
||||
|
||||
L.resolveDefault(callConnStat('all'), { results: [] }).then((result) => {
|
||||
window.clearTimeout(timer);
|
||||
finish(result);
|
||||
});
|
||||
}).then((result) => {
|
||||
if (currentGeneration !== generation)
|
||||
return;
|
||||
|
||||
const results = {};
|
||||
(result.results || []).forEach((siteResult) => {
|
||||
results[siteResult.site] = siteResult;
|
||||
});
|
||||
connectionSites.forEach((site) => {
|
||||
updateResult(site.type, results[site.type] || {
|
||||
result: false,
|
||||
timed_out: !!result.timed_out
|
||||
});
|
||||
});
|
||||
}).finally(() => {
|
||||
if (currentGeneration === generation) {
|
||||
running = false;
|
||||
testButton.disabled = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
testButton = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'style': 'margin-left:4px',
|
||||
'click': ui.createHandlerFn(this, runAllTests)
|
||||
}, [ _('Test all') ]);
|
||||
|
||||
const view = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h3', { 'name': 'content', 'style': 'align-items:center;display:flex' }, [
|
||||
_('Connection status'),
|
||||
testButton
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section' }, [ table ])
|
||||
]);
|
||||
|
||||
window.setTimeout(runAllTests, 0);
|
||||
return view;
|
||||
}
|
||||
|
||||
const resources = [
|
||||
{
|
||||
type: 'china_ip4',
|
||||
name: _('China IPv4 list')
|
||||
},
|
||||
{
|
||||
type: 'china_ip6',
|
||||
name: _('China IPv6 list')
|
||||
},
|
||||
{
|
||||
type: 'china_list',
|
||||
name: _('China domain list')
|
||||
},
|
||||
{
|
||||
type: 'gfw_list',
|
||||
name: _('GFW domain list')
|
||||
}
|
||||
];
|
||||
|
||||
function getResources(o) {
|
||||
const callResStatus = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'resources_get',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const callResUpdate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'resources_update',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
return L.resolveDefault(callResStatus(), { resources: [] }).then((result) => {
|
||||
const status = {};
|
||||
(result.resources || []).forEach((resource) => {
|
||||
status[resource.type] = resource;
|
||||
});
|
||||
const table = E('table', { 'class': 'table' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th' }, _('Name')),
|
||||
E('th', { 'class': 'th' }, _('Version')),
|
||||
E('th', { 'class': 'th' }, _('Source'))
|
||||
])
|
||||
]);
|
||||
const rows = resources.map((resource) => {
|
||||
const resourceStatus = status[resource.type] || {};
|
||||
const available = resourceStatus.version;
|
||||
const source = resourceStatus.source;
|
||||
|
||||
return [
|
||||
resource.name,
|
||||
E('span', { 'style': available ? 'color:green' : 'color:red' },
|
||||
available || '-'),
|
||||
source ? E('a', {
|
||||
'href': source,
|
||||
'target': '_blank',
|
||||
'rel': 'noreferrer noopener',
|
||||
'style': 'word-break:break-all'
|
||||
}, source) : '-'
|
||||
];
|
||||
});
|
||||
cbi_update_table(table, rows);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h3', { 'name': 'content', 'style': 'align-items:center;display:flex' }, [
|
||||
_('Resources management'),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'style': 'margin-left:4px',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return L.resolveDefault(callResUpdate(), {}).then((res) => {
|
||||
let message, severity = 'info';
|
||||
|
||||
switch (res.status) {
|
||||
case 0:
|
||||
message = _('Successfully updated.');
|
||||
break;
|
||||
case 1:
|
||||
message = _('Update failed.');
|
||||
severity = 'error';
|
||||
break;
|
||||
case 2:
|
||||
message = _('Already in updating.');
|
||||
break;
|
||||
case 3:
|
||||
message = _('Already at the latest version.');
|
||||
break;
|
||||
default:
|
||||
message = _('Unknown error.');
|
||||
severity = 'error';
|
||||
break;
|
||||
}
|
||||
|
||||
ui.addNotification(null, E('p', message), severity);
|
||||
return o.map.reset();
|
||||
});
|
||||
})
|
||||
}, [ _('Update all') ])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section' }, [ table ])
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function getRuntimeLog(o, name, _option_index, section_id, _in_table) {
|
||||
const filename = o.option.split('_')[1];
|
||||
|
||||
let section, log_level_el;
|
||||
switch (filename) {
|
||||
case 'homeproxy':
|
||||
section = null;
|
||||
break;
|
||||
case 'sing-box-c':
|
||||
section = 'config';
|
||||
break;
|
||||
case 'sing-box-s':
|
||||
section = 'server';
|
||||
break;
|
||||
}
|
||||
|
||||
if (section) {
|
||||
const selected = uci.get('homeproxy', section, 'log_level') || 'warn';
|
||||
const choices = {
|
||||
trace: _('Trace'),
|
||||
debug: _('Debug'),
|
||||
info: _('Info'),
|
||||
warn: _('Warn'),
|
||||
error: _('Error'),
|
||||
fatal: _('Fatal'),
|
||||
panic: _('Panic')
|
||||
};
|
||||
|
||||
log_level_el = E('select', {
|
||||
'id': o.cbid(section_id),
|
||||
'class': 'cbi-input-select',
|
||||
'style': 'margin-left: 4px; width: 6em;',
|
||||
'change': ui.createHandlerFn(this, (ev) => {
|
||||
uci.set('homeproxy', section, 'log_level', ev.target.value);
|
||||
return o.map.save(null, true).then(() => {
|
||||
ui.changes.apply(true);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
Object.keys(choices).forEach((v) => {
|
||||
log_level_el.appendChild(E('option', {
|
||||
'value': v,
|
||||
'selected': (v === selected) ? '' : null
|
||||
}, [ choices[v] ]));
|
||||
});
|
||||
}
|
||||
|
||||
const callLogClean = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'log_clean',
|
||||
params: ['type'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const log_textarea = E('div', { 'id': 'log_textarea' },
|
||||
E('img', {
|
||||
'src': L.resource('icons/loading.svg'),
|
||||
'alt': _('Loading'),
|
||||
'style': 'vertical-align:middle'
|
||||
}, _('Collecting data...'))
|
||||
);
|
||||
|
||||
let log;
|
||||
poll.add(L.bind(() => {
|
||||
return fs.read_direct(String.format('%s/%s.log', hp_dir, filename), 'text')
|
||||
.then((res) => {
|
||||
log = E('pre', { 'wrap': 'pre' }, [
|
||||
res.trim() || _('Log is empty.')
|
||||
]);
|
||||
|
||||
dom.content(log_textarea, log);
|
||||
}).catch((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', 'style': 'align-items: center; display: flex;'}, [
|
||||
_('%s log').format(name),
|
||||
log_level_el || '',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'style': 'margin-left: 4px;',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return L.resolveDefault(callLogClean(filename), {});
|
||||
})
|
||||
}, [ _('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() {
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('homeproxy');
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy');
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_connection_status');
|
||||
o.render = L.bind(getConnectionStatus, this);
|
||||
|
||||
o = s.option(form.DummyValue, '_resources');
|
||||
o.render = L.bind(getResources, this, o);
|
||||
|
||||
o = s.option(form.Value, 'github_token', _('GitHub token'));
|
||||
o.password = true;
|
||||
o.renderWidget = function() {
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments);
|
||||
|
||||
(node.querySelector('.control-group') || node).appendChild(E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'title': _('Save'),
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return this.map.save(null, true).then(() => {
|
||||
ui.changes.apply(true);
|
||||
});
|
||||
}, this.option)
|
||||
}, [ _('Save') ]));
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy');
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_homeproxy_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, o, _('HomeProxy'));
|
||||
|
||||
o = s.option(form.DummyValue, '_sing-box-c_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, o, _('sing-box client'));
|
||||
|
||||
o = s.option(form.DummyValue, '_sing-box-s_logview');
|
||||
o.render = L.bind(getRuntimeLog, this, o, _('sing-box server'));
|
||||
|
||||
return m.render();
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,32 +0,0 @@
|
||||
{
|
||||
"bounding": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"effective": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"ambient": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"permitted": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
],
|
||||
"inheritable": [
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SYS_PTRACE"
|
||||
]
|
||||
}
|
||||
@ -1,72 +0,0 @@
|
||||
|
||||
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,873,993,995,5222,8080,8443,9418'
|
||||
option mixed_port '5330'
|
||||
option redirect_port '5331'
|
||||
option tproxy_port '5332'
|
||||
option dns_port '5333'
|
||||
option dns_redirect '1'
|
||||
option ntp_server 'nil'
|
||||
option udp_timeout ''
|
||||
option tun_name 'singtun0'
|
||||
option tun_addr4 '172.19.0.1/30'
|
||||
option tun_addr6 'fdfe:dcba:9876::1/126'
|
||||
option tun_mtu '9000'
|
||||
option table_mark '100'
|
||||
option self_mark '100'
|
||||
option tproxy_mark '101'
|
||||
option tun_mark '102'
|
||||
|
||||
config homeproxy 'migration'
|
||||
option crontab '1'
|
||||
|
||||
config homeproxy 'config'
|
||||
option main_node 'nil'
|
||||
option main_udp_node 'same'
|
||||
option dns_server 'https://dns.quad9.net/dns-query'
|
||||
option china_dns_server 'https://dns.alidns.com/dns-query'
|
||||
option routing_mode 'bypass_mainland_china'
|
||||
option routing_port 'common'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option github_token ''
|
||||
option log_level 'warn'
|
||||
|
||||
config homeproxy 'control'
|
||||
option lan_proxy_mode 'disabled'
|
||||
list wan_proxy_ipv4_ips '91.105.192.0/23'
|
||||
list wan_proxy_ipv4_ips '91.108.4.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.8.0/21'
|
||||
list wan_proxy_ipv4_ips '91.108.16.0/21'
|
||||
list wan_proxy_ipv4_ips '91.108.56.0/22'
|
||||
list wan_proxy_ipv4_ips '95.161.64.0/20'
|
||||
list wan_proxy_ipv4_ips '149.154.160.0/20'
|
||||
list wan_proxy_ipv4_ips '185.76.151.0/24'
|
||||
list wan_proxy_ipv6_ips '2001:67c:4e8::/48'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23c::/47'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23f::/48'
|
||||
list wan_proxy_ipv6_ips '2a0a:f280::/32'
|
||||
|
||||
config homeproxy 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy 'dns'
|
||||
option default_strategy 'prefer_ipv4'
|
||||
option default_server 'default-dns'
|
||||
option disable_cache '0'
|
||||
option disable_cache_expire '0'
|
||||
|
||||
config homeproxy 'subscription'
|
||||
option auto_update '0'
|
||||
option allow_insecure '0'
|
||||
option latency_test_mode 'icmp'
|
||||
option packet_encoding 'xudp'
|
||||
option update_via_proxy '0'
|
||||
option auto_update_time '0 */6 * * *'
|
||||
option filter_nodes 'disabled'
|
||||
|
||||
config homeproxy 'server'
|
||||
option enabled '0'
|
||||
option log_level 'warn'
|
||||
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
202607042328
|
||||
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
202607042328
|
||||
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
202607042328
|
||||
File diff suppressed because it is too large
Load Diff
@ -1 +0,0 @@
|
||||
202607042328
|
||||
@ -1,19 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
|
||||
NAME="homeproxy"
|
||||
|
||||
log_max_size="50" #KB
|
||||
main_log_file="/var/run/$NAME/$NAME.log"
|
||||
singc_log_file="/var/run/$NAME/sing-box-c.log"
|
||||
sings_log_file="/var/run/$NAME/sing-box-s.log"
|
||||
|
||||
while true; do
|
||||
sleep 180
|
||||
for i in "$main_log_file" "$singc_log_file" "$sings_log_file"; do
|
||||
[ -s "$i" ] || continue
|
||||
[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
|
||||
done
|
||||
done
|
||||
@ -1,665 +0,0 @@
|
||||
#!/usr/bin/utpl -S
|
||||
|
||||
{%-
|
||||
import { readfile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc';
|
||||
|
||||
const fw4 = require('fw4');
|
||||
|
||||
function array_to_nftarr(array) {
|
||||
if (type(array) !== 'array')
|
||||
return null;
|
||||
|
||||
return `{ ${join(', ', uniq(array))} }`;
|
||||
}
|
||||
|
||||
function resolve_ipv6(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
let ipv6 = fw4.parse_subnet(str)?.[0];
|
||||
if (!ipv6 || ipv6.family !== 6)
|
||||
return null;
|
||||
|
||||
if (ipv6.bits > -1)
|
||||
return `${ipv6.addr}/${ipv6.bits}`;
|
||||
else
|
||||
return `& ${ipv6.mask} == ${ipv6.addr}`;
|
||||
}
|
||||
|
||||
function resolve_mark(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
let mark = fw4.parse_mark(str);
|
||||
if (isEmpty(mark))
|
||||
return null;
|
||||
|
||||
if (mark.mask === 0xffffffff)
|
||||
return fw4.hex(mark.mark);
|
||||
else if (mark.mark === 0)
|
||||
return `mark and ${fw4.hex(~mark.mask & 0xffffffff)}`;
|
||||
else if (mark.mark === mark.mask)
|
||||
return `mark or ${fw4.hex(mark.mark)}`;
|
||||
else if (mark.mask === 0)
|
||||
return `mark xor ${fw4.hex(mark.mark)}`;
|
||||
else
|
||||
return `mark and ${fw4.hex(~mark.mask & 0xffffffff)} xor ${fw4.hex(mark.mark)}`;
|
||||
}
|
||||
|
||||
/* 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, china_dns_server, bypass_cn_traffic;
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
|
||||
china_dns_server = uci.get(cfgname, 'config', 'china_dns_server');
|
||||
} else {
|
||||
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
||||
bypass_cn_traffic = uci.get(cfgname, 'routing', 'bypass_cn_traffic') || '0';
|
||||
}
|
||||
|
||||
let routing_port = uci.get(cfgname, 'config', 'routing_port');
|
||||
if (routing_port === 'common')
|
||||
routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,873,993,995,8080,8443,9418';
|
||||
|
||||
const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy',
|
||||
ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0';
|
||||
|
||||
let self_mark, redirect_port,
|
||||
tproxy_port, tproxy_mark,
|
||||
tun_name, tun_mark;
|
||||
|
||||
if (match(proxy_mode, /redirect/)) {
|
||||
self_mark = uci.get(cfgname, 'infra', 'self_mark') || '100';
|
||||
redirect_port = uci.get(cfgname, 'infra', 'redirect_port') || '5331';
|
||||
}
|
||||
if (match(proxy_mode, /tproxy/))
|
||||
if (outbound_udp_node !== 'nil' || routing_mode === 'custom') {
|
||||
tproxy_port = uci.get(cfgname, 'infra', 'tproxy_port') || '5332';
|
||||
tproxy_mark = resolve_mark(uci.get(cfgname, 'infra', 'tproxy_mark') || '101');
|
||||
}
|
||||
if (match(proxy_mode, /tun/)) {
|
||||
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
|
||||
tun_mark = resolve_mark(uci.get(cfgname, 'infra', 'tun_mark') || '102');
|
||||
}
|
||||
|
||||
const control_options = [
|
||||
"listen_interfaces", "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 = {};
|
||||
|
||||
for (let i in control_options)
|
||||
control_info[i] = uci.get(cfgname, 'control', i);
|
||||
|
||||
const dns_redirect = uci.get(cfgname, 'infra', 'dns_redirect') || '1';
|
||||
const dns_hijacked = uci.get('dhcp', '@dnsmasq[0]', 'dns_redirect') || '0',
|
||||
dns_port = uci.get('dhcp', '@dnsmasq[0]', 'port') || '53';
|
||||
/* 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 (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 /* ipv6_support */ %}
|
||||
{% elif (match(routing_mode, /mainland_china/) || bypass_cn_traffic === '1'): %}
|
||||
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')), /[\r\n]/)): %}
|
||||
{{ cnip4 }},
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
{% if ((ipv6_support === '1') || china_dns_server): %}
|
||||
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')), /[\r\n]/)): %}
|
||||
{{ cnip6 }},
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{# WAN ACL addresses #}
|
||||
set homeproxy_wan_proxy_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_proxy_ipv4_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} }
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_wan_proxy_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_proxy_ipv6_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} }
|
||||
{% endif /* wan_proxy_ipv6_ips*/ %}
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
set homeproxy_wan_direct_addr_v4 {
|
||||
type ipv4_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_direct_ipv4_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} }
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
{% if (ipv6_support === '1'): %}
|
||||
set homeproxy_wan_direct_addr_v6 {
|
||||
type ipv6_addr
|
||||
flags interval
|
||||
auto-merge
|
||||
{% if (control_info.wan_direct_ipv6_ips): %}
|
||||
elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} }
|
||||
{% endif /* wan_direct_ipv6_ips */ %}
|
||||
}
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% if (routing_port): %}
|
||||
set homeproxy_routing_port {
|
||||
type inet_service
|
||||
flags interval
|
||||
auto-merge
|
||||
elements = { {{ join(', ', split(routing_port, ',')) }} }
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# DNS hijack & TCP redirect #}
|
||||
chain dstnat {
|
||||
{% if ((dns_redirect === '1') && (dns_hijacked !== '1')): %}
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname {{ array_to_nftarr(control_info.listen_interfaces) }}
|
||||
{%- endif /* listen_interfaces */ %}
|
||||
meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :{{ dns_port }} comment "!{{ cfgname }}: DNS hijack"
|
||||
{% endif /* dns_hijacked */ %}
|
||||
{% if (match(proxy_mode, /redirect/)): %}
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac
|
||||
{% endif /* proxy_mode */ %}
|
||||
}
|
||||
|
||||
{# TCP redirect #}
|
||||
{% if (match(proxy_mode, /redirect/)): %}
|
||||
chain homeproxy_redirect_proxy {
|
||||
meta l4proto tcp counter redirect to :{{ redirect_port }}
|
||||
}
|
||||
|
||||
chain homeproxy_redirect_proxy_port {
|
||||
{% if (routing_port): %}
|
||||
tcp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
goto homeproxy_redirect_proxy
|
||||
}
|
||||
|
||||
chain homeproxy_redirect_lanac {
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
|
||||
{% endif %}
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_redirect
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_redirect
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_redirect
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_redirect {
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% 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 /* ipv6_support */ %}
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% 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 /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_redirect_proxy
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_redirect_proxy
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_redirect_proxy
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_redirect_proxy_port
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# UDP tproxy #}
|
||||
{% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom')): %}
|
||||
chain homeproxy_mangle_tproxy {
|
||||
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept
|
||||
{% if (ipv6_support === '1'): %}
|
||||
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tproxy_port {
|
||||
{% if (routing_port): %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
goto homeproxy_mangle_tproxy
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_mark {
|
||||
{% if (routing_port): %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif %}
|
||||
meta l4proto udp meta mark set {{ tproxy_mark }} counter accept
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_lanac {
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(uniq([...control_info.listen_interfaces, ...['lo']])) }} counter return
|
||||
{% endif %}
|
||||
meta iifname != lo udp dport 53 counter return
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_mangle_prerouting
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_prerouting {
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% 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 /* ipv6_support */ %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% endif /* routing_mode */ %}
|
||||
{% 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 /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_mangle_tproxy_port
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_output {
|
||||
meta mark {{ self_mark }} counter return
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_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 %}
|
||||
|
||||
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
|
||||
{% 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 /* ipv6_support */ %}
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% 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 /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
counter goto homeproxy_mangle_mark
|
||||
}
|
||||
|
||||
chain mangle_prerouting {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac
|
||||
}
|
||||
|
||||
chain mangle_output {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
|
||||
}
|
||||
{% endif %}
|
||||
|
||||
{# TUN #}
|
||||
{% if (match(proxy_mode, /tun/)): %}
|
||||
chain homeproxy_mangle_lanac {
|
||||
iifname {{ tun_name }} counter return
|
||||
udp dport 53 counter return
|
||||
|
||||
{% if (control_info.listen_interfaces): %}
|
||||
meta iifname != {{ array_to_nftarr(control_info.listen_interfaces) }} counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun
|
||||
{% endif /* lan_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun
|
||||
{% endfor /* lan_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun
|
||||
{% endif /* lan_proxy_mac_addrs */ %}
|
||||
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
||||
{% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* lan_direct_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_direct_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter return
|
||||
{% endfor /* lan_direct_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_direct_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return
|
||||
{% endif /* lan_direct_mac_addrs */ %}
|
||||
{% endif /* lan_proxy_mode */ %}
|
||||
|
||||
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
||||
counter goto homeproxy_mangle_tun
|
||||
{% endif %}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tun_mark {
|
||||
{% if (routing_port): %}
|
||||
{% if (proxy_mode === 'tun'): %}
|
||||
tcp dport != @homeproxy_routing_port counter return
|
||||
{% endif /* proxy_mode */ %}
|
||||
udp dport != @homeproxy_routing_port counter return
|
||||
{% endif /* routing_port */ %}
|
||||
|
||||
counter meta mark set {{ tun_mark }}
|
||||
}
|
||||
|
||||
chain homeproxy_mangle_tun {
|
||||
iifname {{ tun_name }} counter return
|
||||
|
||||
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark
|
||||
{% endif %}
|
||||
|
||||
ip daddr @homeproxy_local_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_local_addr_v6 counter return
|
||||
{% endif %}
|
||||
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_global_proxy_ipv4_ips) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_global_proxy_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endfor /* lan_global_proxy_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_global_proxy_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_global_proxy_mac_addrs) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endif /* lan_global_proxy_mac_addrs */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (control_info.wan_direct_ipv4_ips): %}
|
||||
ip daddr {{ array_to_nftarr(control_info.wan_direct_ipv4_ips) }} counter return
|
||||
{% endif /* wan_direct_ipv4_ips */ %}
|
||||
{% if (control_info.wan_direct_ipv6_ips): %}
|
||||
ip6 daddr {{ array_to_nftarr(control_info.wan_direct_ipv6_ips) }} counter return
|
||||
{% endif /* wan_direct_ipv6_ips */ %}
|
||||
|
||||
{% 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 /* ipv6_support */ %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
{% endif /* routing_mode */ %}
|
||||
{% 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 /* ipv6_support */ %}
|
||||
{% endif /* routing_mode */ %}
|
||||
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_ipv4_ips)): %}
|
||||
ip saddr {{ array_to_nftarr(control_info.lan_gaming_mode_ipv4_ips) }} counter meta mark set {{ tun_mark }}
|
||||
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
||||
{% for (let ipv6 in control_info.lan_gaming_mode_ipv6_ips): %}
|
||||
ip6 saddr {{ resolve_ipv6(ipv6) }} counter meta mark set {{ tun_mark }}
|
||||
{% endfor /* lan_gaming_mode_ipv6_ips */ %}
|
||||
{% if (!isEmpty(control_info.lan_gaming_mode_mac_addrs)): %}
|
||||
ether saddr {{ array_to_nftarr(control_info.lan_gaming_mode_mac_addrs) }} counter meta mark set {{ tun_mark }}
|
||||
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
||||
|
||||
counter goto homeproxy_mangle_tun_mark
|
||||
}
|
||||
|
||||
chain mangle_prerouting {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac
|
||||
}
|
||||
|
||||
chain mangle_output {
|
||||
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun
|
||||
}
|
||||
{% endif %}
|
||||
@ -1,51 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
'use strict';
|
||||
|
||||
import { writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty, RUN_DIR } from 'homeproxy';
|
||||
|
||||
const cfgname = 'homeproxy';
|
||||
const uci = cursor();
|
||||
uci.load(cfgname);
|
||||
|
||||
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china',
|
||||
proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy';
|
||||
|
||||
let outbound_node, tun_name;
|
||||
if (match(proxy_mode, /tun/)) {
|
||||
if (routing_mode === 'custom')
|
||||
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
||||
else
|
||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||
|
||||
if (outbound_node !== 'nil')
|
||||
tun_name = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
|
||||
}
|
||||
|
||||
const server_enabled = uci.get(cfgname, 'server', 'enabled');
|
||||
|
||||
let forward = [],
|
||||
input = [];
|
||||
|
||||
if (tun_name) {
|
||||
push(forward, `oifname ${tun_name} counter accept comment "!${cfgname}: accept tun forward"`);
|
||||
push(input ,`iifname ${tun_name} counter accept comment "!${cfgname}: accept tun input"`);
|
||||
}
|
||||
|
||||
if (server_enabled === '1') {
|
||||
uci.foreach(cfgname, 'server', (s) => {
|
||||
if (s.enabled !== '1' || s.firewall !== '1')
|
||||
return;
|
||||
|
||||
let proto = s.network || '{ tcp, udp }';
|
||||
push(input, `meta l4proto ${proto} th dport ${s.port} counter accept comment "!${cfgname}: accept server ${s['.name']}"`);
|
||||
});
|
||||
}
|
||||
|
||||
if (!isEmpty(forward))
|
||||
writefile(RUN_DIR + '/fw4_forward.nft', join('\n', forward) + '\n');
|
||||
|
||||
if (!isEmpty(input))
|
||||
writefile(RUN_DIR + '/fw4_input.nft', join('\n', input) + '\n');
|
||||
@ -1,872 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023-2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { readfile, writefile } from 'fs';
|
||||
import { isnan } from 'math';
|
||||
import { connect } from 'ubus';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import {
|
||||
isEmpty, parseURL, strToBool, strToInt, strToTime,
|
||||
removeBlankAttrs, renderEndpoint, renderOutbound, validation, HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
const ubus = connect();
|
||||
|
||||
/* const features = ubus.call('luci.homeproxy', 'singbox_get_features') || {}; */
|
||||
|
||||
/* 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';
|
||||
const uciruleset = 'ruleset';
|
||||
|
||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
|
||||
|
||||
function normalize_list(value) {
|
||||
if (isEmpty(value))
|
||||
return [];
|
||||
if (type(value) === 'array')
|
||||
return value;
|
||||
return [value];
|
||||
}
|
||||
|
||||
let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']?.[0];
|
||||
if (!wan_dns)
|
||||
wan_dns = (routing_mode in ['proxy_mainland_china', 'global']) ? '9.9.9.9' : '223.5.5.5';
|
||||
|
||||
const dns_port = uci.get(uciconfig, uciinfra, 'dns_port') || '5333';
|
||||
|
||||
const ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server') || 'time.apple.com';
|
||||
|
||||
const ipv6_support = uci.get(uciconfig, ucimain, 'ipv6_support') || '0';
|
||||
|
||||
let main_node, main_udp_node, dedicated_udp_node, default_outbound, default_outbound_dns,
|
||||
domain_strategy, dns_server, china_dns_server, dns_default_strategy,
|
||||
dns_default_server, dns_disable_cache, dns_disable_cache_expire, dns_independent_cache,
|
||||
dns_client_subnet, cache_file_store_rdrc, cache_file_rdrc_timeout, direct_domain_list,
|
||||
proxy_domain_list;
|
||||
|
||||
if (routing_mode !== 'custom') {
|
||||
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
||||
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
||||
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;
|
||||
|
||||
if (routing_mode === 'bypass_mainland_china') {
|
||||
china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
|
||||
if (isEmpty(china_dns_server) || type(china_dns_server) !== 'string' || china_dns_server === 'wan')
|
||||
china_dns_server = wan_dns;
|
||||
}
|
||||
dns_default_strategy = (ipv6_support !== '1') ? 'ipv4_only' : null;
|
||||
|
||||
direct_domain_list = trim(readfile(HP_DIR + '/resources/direct_list.txt'));
|
||||
if (direct_domain_list)
|
||||
direct_domain_list = split(direct_domain_list, /[\r\n]/);
|
||||
|
||||
proxy_domain_list = trim(readfile(HP_DIR + '/resources/proxy_list.txt'));
|
||||
if (proxy_domain_list)
|
||||
proxy_domain_list = split(proxy_domain_list, /[\r\n]/);
|
||||
|
||||
} else {
|
||||
/* DNS settings */
|
||||
dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_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');
|
||||
dns_independent_cache = uci.get(uciconfig, ucidnssetting, 'independent_cache');
|
||||
dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
|
||||
cache_file_store_rdrc = uci.get(uciconfig, ucidnssetting, 'cache_file_store_rdrc'),
|
||||
cache_file_rdrc_timeout = uci.get(uciconfig, ucidnssetting, 'cache_file_rdrc_timeout');
|
||||
|
||||
/* Routing settings */
|
||||
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
|
||||
default_outbound_dns = uci.get(uciconfig, uciroutingsetting, 'default_outbound_dns') || 'default-dns';
|
||||
domain_strategy = uci.get(uciconfig, uciroutingsetting, 'domain_strategy');
|
||||
}
|
||||
|
||||
const proxy_mode = uci.get(uciconfig, ucimain, 'proxy_mode') || 'redirect_tproxy',
|
||||
default_interface = uci.get(uciconfig, ucicontrol, 'bind_interface');
|
||||
|
||||
const mixed_port = uci.get(uciconfig, uciinfra, 'mixed_port') || '5330';
|
||||
|
||||
let self_mark, redirect_port, tproxy_port, tun_name,
|
||||
tun_addr4, tun_addr6, tun_mtu, tcpip_stack, udp_timeout;
|
||||
|
||||
if (routing_mode === 'custom')
|
||||
udp_timeout = uci.get(uciconfig, uciroutingsetting, 'udp_timeout');
|
||||
else
|
||||
udp_timeout = uci.get(uciconfig, 'infra', 'udp_timeout');
|
||||
|
||||
if (match(proxy_mode, /redirect/)) {
|
||||
self_mark = uci.get(uciconfig, 'infra', 'self_mark') || '100';
|
||||
redirect_port = uci.get(uciconfig, 'infra', 'redirect_port') || '5331';
|
||||
}
|
||||
if (match(proxy_mode, /tproxy/))
|
||||
if (main_udp_node !== 'nil' || routing_mode === 'custom')
|
||||
tproxy_port = uci.get(uciconfig, 'infra', 'tproxy_port') || '5332';
|
||||
if (match(proxy_mode, /tun/)) {
|
||||
tun_name = uci.get(uciconfig, uciinfra, 'tun_name') || 'singtun0';
|
||||
tun_addr4 = uci.get(uciconfig, uciinfra, 'tun_addr4') || '172.19.0.1/30';
|
||||
tun_addr6 = uci.get(uciconfig, uciinfra, 'tun_addr6') || 'fdfe:dcba:9876::1/126';
|
||||
tun_mtu = uci.get(uciconfig, uciinfra, 'tun_mtu') || '9000';
|
||||
tcpip_stack = 'system';
|
||||
if (routing_mode === 'custom') {
|
||||
tcpip_stack = uci.get(uciconfig, uciroutingsetting, 'tcpip_stack') || 'system';
|
||||
}
|
||||
}
|
||||
|
||||
const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn';
|
||||
/* UCI config end */
|
||||
|
||||
/* Config helper start */
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
function parse_dnsserver(server_addr, default_protocol) {
|
||||
if (isEmpty(server_addr))
|
||||
return null;
|
||||
|
||||
if (!match(server_addr, /:\/\//))
|
||||
server_addr = (default_protocol || 'udp') + '://' + (validation('ip6addr', server_addr) ? `[${server_addr}]` : server_addr);
|
||||
server_addr = parseURL(server_addr);
|
||||
|
||||
return {
|
||||
type: server_addr.protocol,
|
||||
server: server_addr.hostname,
|
||||
server_port: strToInt(server_addr.port),
|
||||
path: (server_addr.pathname !== '/') ? server_addr.pathname : null,
|
||||
}
|
||||
}
|
||||
|
||||
function parse_dnsquery(strquery) {
|
||||
if (type(strquery) !== 'array' || isEmpty(strquery))
|
||||
return null;
|
||||
|
||||
let querys = [];
|
||||
for (let i in strquery)
|
||||
isnan(int(i)) ? push(querys, i) : push(querys, int(i));
|
||||
|
||||
return querys;
|
||||
|
||||
}
|
||||
|
||||
function filter_existing_nodes(nodes) {
|
||||
if (type(nodes) !== 'array' || isEmpty(nodes))
|
||||
return [];
|
||||
|
||||
return filter(nodes, (k) => {
|
||||
const node = uci.get_all(uciconfig, k) || {};
|
||||
return !isEmpty(node);
|
||||
});
|
||||
}
|
||||
|
||||
const generate_endpoint = renderEndpoint;
|
||||
|
||||
function generate_outbound(node) {
|
||||
return renderOutbound(node, self_mark);
|
||||
}
|
||||
|
||||
function get_outbound(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
if (type(cfg) === 'array') {
|
||||
if ('any-out' in cfg)
|
||||
return 'any';
|
||||
|
||||
let outbounds = [];
|
||||
for (let i in cfg)
|
||||
push(outbounds, get_outbound(i));
|
||||
return outbounds;
|
||||
} else {
|
||||
switch (cfg) {
|
||||
case 'direct-out':
|
||||
return cfg;
|
||||
default:
|
||||
const node = uci.get(uciconfig, cfg, 'node');
|
||||
if (isEmpty(node))
|
||||
die(sprintf("%s's node is missing, please check your configuration.", cfg));
|
||||
else if (node === 'urltest')
|
||||
return 'cfg-' + cfg + '-out';
|
||||
else
|
||||
return 'cfg-' + node + '-out';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function get_resolver(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
switch (cfg) {
|
||||
case 'default-dns':
|
||||
case 'system-dns':
|
||||
return cfg;
|
||||
default:
|
||||
return 'cfg-' + cfg + '-dns';
|
||||
}
|
||||
}
|
||||
|
||||
function get_ruleset(cfg) {
|
||||
if (isEmpty(cfg))
|
||||
return null;
|
||||
|
||||
let rules = [];
|
||||
for (let i in cfg)
|
||||
push(rules, isEmpty(i) ? null : 'cfg-' + i + '-rule');
|
||||
return rules;
|
||||
}
|
||||
/* Config helper end */
|
||||
|
||||
const config = {};
|
||||
|
||||
/* Log */
|
||||
config.log = {
|
||||
disabled: false,
|
||||
level: log_level,
|
||||
output: RUN_DIR + '/sing-box-c.log',
|
||||
timestamp: true
|
||||
};
|
||||
|
||||
/* NTP */
|
||||
if (!isEmpty(ntp_server))
|
||||
config.ntp = {
|
||||
enabled: true,
|
||||
server: ntp_server,
|
||||
detour: 'direct-out',
|
||||
domain_resolver: 'default-dns',
|
||||
};
|
||||
|
||||
/* DNS start */
|
||||
/* Default settings */
|
||||
config.dns = {
|
||||
servers: [
|
||||
{
|
||||
tag: 'default-dns',
|
||||
type: 'udp',
|
||||
server: wan_dns,
|
||||
detour: self_mark ? 'direct-out' : null
|
||||
},
|
||||
{
|
||||
tag: 'system-dns',
|
||||
type: 'local',
|
||||
detour: self_mark ? 'direct-out' : null
|
||||
}
|
||||
],
|
||||
rules: [],
|
||||
strategy: dns_default_strategy,
|
||||
disable_cache: strToBool(dns_disable_cache),
|
||||
disable_expire: strToBool(dns_disable_cache_expire),
|
||||
independent_cache: strToBool(dns_independent_cache),
|
||||
client_subnet: dns_client_subnet
|
||||
};
|
||||
|
||||
if (!isEmpty(main_node)) {
|
||||
/* Main DNS */
|
||||
push(config.dns.servers, {
|
||||
tag: 'main-dns',
|
||||
domain_resolver: {
|
||||
server: 'default-dns',
|
||||
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null
|
||||
},
|
||||
detour: 'main-out',
|
||||
...parse_dnsserver(dns_server, 'tcp')
|
||||
});
|
||||
config.dns.final = 'main-dns';
|
||||
|
||||
if (length(direct_domain_list))
|
||||
push(config.dns.rules, {
|
||||
rule_set: 'direct-domain',
|
||||
action: 'route',
|
||||
server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns'
|
||||
});
|
||||
|
||||
/* Filter out SVCB/HTTPS queries for "exquisite" Apple devices */
|
||||
if (routing_mode === 'gfwlist' || length(proxy_domain_list))
|
||||
push(config.dns.rules, {
|
||||
rule_set: (routing_mode !== 'gfwlist') ? 'proxy-domain' : null,
|
||||
query_type: [64, 65],
|
||||
action: 'reject'
|
||||
});
|
||||
|
||||
if (routing_mode === 'bypass_mainland_china') {
|
||||
push(config.dns.servers, {
|
||||
tag: 'china-dns',
|
||||
domain_resolver: {
|
||||
server: 'default-dns',
|
||||
strategy: 'prefer_ipv6'
|
||||
},
|
||||
detour: self_mark ? 'direct-out' : null,
|
||||
...parse_dnsserver(china_dns_server)
|
||||
});
|
||||
|
||||
if (length(proxy_domain_list))
|
||||
push(config.dns.rules, {
|
||||
rule_set: 'proxy-domain',
|
||||
action: 'route',
|
||||
server: 'main-dns'
|
||||
});
|
||||
|
||||
push(config.dns.rules, {
|
||||
rule_set: 'geosite-cn',
|
||||
action: 'route',
|
||||
server: 'china-dns',
|
||||
strategy: 'prefer_ipv6'
|
||||
});
|
||||
push(config.dns.rules, {
|
||||
type: 'logical',
|
||||
mode: 'and',
|
||||
rules: [
|
||||
{
|
||||
rule_set: 'geosite-noncn',
|
||||
invert: true
|
||||
},
|
||||
{
|
||||
rule_set: 'geoip-cn'
|
||||
}
|
||||
],
|
||||
action: 'route',
|
||||
server: 'china-dns',
|
||||
strategy: 'prefer_ipv6'
|
||||
});
|
||||
}
|
||||
} else if (!isEmpty(default_outbound)) {
|
||||
/* DNS servers */
|
||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
let outbound = get_outbound(cfg.outbound);
|
||||
if (outbound === 'direct-out' && isEmpty(self_mark))
|
||||
outbound = null;
|
||||
|
||||
push(config.dns.servers, {
|
||||
tag: 'cfg-' + cfg['.name'] + '-dns',
|
||||
type: cfg.type,
|
||||
server: cfg.server,
|
||||
server_port: strToInt(cfg.server_port),
|
||||
path: cfg.path,
|
||||
headers: cfg.headers,
|
||||
tls: cfg.tls_sni ? {
|
||||
enabled: true,
|
||||
server_name: cfg.tls_sni
|
||||
} : null,
|
||||
domain_resolver: (cfg.domain_resolver || cfg.domain_strategy) ? {
|
||||
server: get_resolver(cfg.domain_resolver || dns_default_server),
|
||||
strategy: cfg.domain_strategy
|
||||
} : null,
|
||||
detour: outbound
|
||||
});
|
||||
});
|
||||
|
||||
/* DNS rules */
|
||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
push(config.dns.rules, {
|
||||
ip_version: strToInt(cfg.ip_version),
|
||||
query_type: parse_dnsquery(cfg.query_type),
|
||||
network: cfg.network,
|
||||
protocol: cfg.protocol,
|
||||
domain: cfg.domain,
|
||||
domain_suffix: cfg.domain_suffix,
|
||||
domain_keyword: cfg.domain_keyword,
|
||||
domain_regex: cfg.domain_regex,
|
||||
port: parse_port(cfg.port),
|
||||
port_range: cfg.port_range,
|
||||
source_ip_cidr: cfg.source_ip_cidr,
|
||||
source_ip_is_private: strToBool(cfg.source_ip_is_private),
|
||||
ip_cidr: cfg.ip_cidr,
|
||||
ip_is_private: strToBool(cfg.ip_is_private),
|
||||
source_port: parse_port(cfg.source_port),
|
||||
source_port_range: cfg.source_port_range,
|
||||
process_name: cfg.process_name,
|
||||
process_path: cfg.process_path,
|
||||
process_path_regex: cfg.process_path_regex,
|
||||
user: cfg.user,
|
||||
rule_set: get_ruleset(cfg.rule_set),
|
||||
rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source),
|
||||
rule_set_ip_cidr_accept_empty: strToBool(cfg.rule_set_ip_cidr_accept_empty),
|
||||
invert: strToBool(cfg.invert),
|
||||
action: cfg.action,
|
||||
server: get_resolver(cfg.server),
|
||||
strategy: cfg.domain_strategy,
|
||||
disable_cache: strToBool(cfg.dns_disable_cache),
|
||||
rewrite_ttl: strToInt(cfg.rewrite_ttl),
|
||||
client_subnet: cfg.client_subnet,
|
||||
method: cfg.reject_method,
|
||||
no_drop: strToBool(cfg.reject_no_drop),
|
||||
rcode: cfg.predefined_rcode,
|
||||
answer: cfg.predefined_answer,
|
||||
ns: cfg.predefined_ns,
|
||||
extra: cfg.predefined_extra
|
||||
});
|
||||
});
|
||||
|
||||
if (isEmpty(config.dns.rules))
|
||||
config.dns.rules = null;
|
||||
|
||||
config.dns.final = get_resolver(dns_default_server);
|
||||
}
|
||||
/* DNS end */
|
||||
|
||||
/* Inbound start */
|
||||
config.inbounds = [];
|
||||
|
||||
push(config.inbounds, {
|
||||
type: 'direct',
|
||||
tag: 'dns-in',
|
||||
listen: '::',
|
||||
listen_port: int(dns_port)
|
||||
});
|
||||
|
||||
push(config.inbounds, {
|
||||
type: 'mixed',
|
||||
tag: 'mixed-in',
|
||||
listen: '::',
|
||||
listen_port: int(mixed_port),
|
||||
udp_timeout: strToTime(udp_timeout),
|
||||
set_system_proxy: false
|
||||
});
|
||||
|
||||
if (match(proxy_mode, /redirect/))
|
||||
push(config.inbounds, {
|
||||
type: 'redirect',
|
||||
tag: 'redirect-in',
|
||||
|
||||
listen: '::',
|
||||
listen_port: int(redirect_port)
|
||||
});
|
||||
if (match(proxy_mode, /tproxy/))
|
||||
push(config.inbounds, {
|
||||
type: 'tproxy',
|
||||
tag: 'tproxy-in',
|
||||
|
||||
listen: '::',
|
||||
listen_port: int(tproxy_port),
|
||||
network: 'udp',
|
||||
udp_timeout: strToTime(udp_timeout)
|
||||
});
|
||||
if (match(proxy_mode, /tun/))
|
||||
push(config.inbounds, {
|
||||
type: 'tun',
|
||||
tag: 'tun-in',
|
||||
|
||||
interface_name: tun_name,
|
||||
address: (ipv6_support === '1') ? [tun_addr4, tun_addr6] : [tun_addr4],
|
||||
mtu: strToInt(tun_mtu),
|
||||
auto_route: false,
|
||||
udp_timeout: strToTime(udp_timeout),
|
||||
stack: tcpip_stack
|
||||
});
|
||||
/* Inbound end */
|
||||
|
||||
/* Outbound start */
|
||||
config.endpoints = [];
|
||||
|
||||
/* Default outbounds */
|
||||
config.outbounds = [
|
||||
{
|
||||
type: 'direct',
|
||||
tag: 'direct-out',
|
||||
routing_mark: strToInt(self_mark)
|
||||
}
|
||||
];
|
||||
|
||||
/* Main outbounds */
|
||||
if (!isEmpty(main_node)) {
|
||||
let urltest_nodes = [];
|
||||
|
||||
if (main_node === 'urltest') {
|
||||
const main_urltest_nodes = filter_existing_nodes(
|
||||
normalize_list(uci.get(uciconfig, ucimain, 'main_urltest_nodes'))
|
||||
);
|
||||
const main_urltest_interval = uci.get(uciconfig, ucimain, 'main_urltest_interval');
|
||||
const main_urltest_tolerance = uci.get(uciconfig, ucimain, 'main_urltest_tolerance');
|
||||
|
||||
push(config.outbounds, {
|
||||
type: 'urltest',
|
||||
tag: 'main-out',
|
||||
outbounds: map(main_urltest_nodes, (k) => `cfg-${k}-out`),
|
||||
interval: strToTime(main_urltest_interval),
|
||||
tolerance: strToInt(main_urltest_tolerance),
|
||||
idle_timeout: (strToInt(main_urltest_interval) > 1800) ? `${main_urltest_interval * 2}s` : null,
|
||||
});
|
||||
urltest_nodes = main_urltest_nodes;
|
||||
} else {
|
||||
const main_node_cfg = uci.get_all(uciconfig, main_node) || {};
|
||||
if (main_node_cfg.type === 'wireguard') {
|
||||
const main_endpoint = generate_endpoint(main_node_cfg);
|
||||
if (main_endpoint) {
|
||||
main_endpoint.tag = 'main-out';
|
||||
push(config.endpoints, main_endpoint);
|
||||
}
|
||||
} else {
|
||||
const main_outbound = generate_outbound(main_node_cfg);
|
||||
if (main_outbound) {
|
||||
main_outbound.tag = 'main-out';
|
||||
push(config.outbounds, main_outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (main_udp_node === 'urltest') {
|
||||
const main_udp_urltest_nodes = filter_existing_nodes(
|
||||
normalize_list(uci.get(uciconfig, ucimain, 'main_udp_urltest_nodes'))
|
||||
);
|
||||
const main_udp_urltest_interval = uci.get(uciconfig, ucimain, 'main_udp_urltest_interval');
|
||||
const main_udp_urltest_tolerance = uci.get(uciconfig, ucimain, 'main_udp_urltest_tolerance');
|
||||
|
||||
push(config.outbounds, {
|
||||
type: 'urltest',
|
||||
tag: 'main-udp-out',
|
||||
outbounds: map(main_udp_urltest_nodes, (k) => `cfg-${k}-out`),
|
||||
interval: strToTime(main_udp_urltest_interval),
|
||||
tolerance: strToInt(main_udp_urltest_tolerance),
|
||||
idle_timeout: (strToInt(main_udp_urltest_interval) > 1800) ? `${main_udp_urltest_interval * 2}s` : null,
|
||||
});
|
||||
urltest_nodes = [...urltest_nodes, ...filter(main_udp_urltest_nodes, (l) => !~index(urltest_nodes, l))];
|
||||
} else if (dedicated_udp_node) {
|
||||
const main_udp_node_cfg = uci.get_all(uciconfig, main_udp_node) || {};
|
||||
if (main_udp_node_cfg.type === 'wireguard') {
|
||||
const main_udp_endpoint = generate_endpoint(main_udp_node_cfg);
|
||||
if (main_udp_endpoint) {
|
||||
main_udp_endpoint.tag = 'main-udp-out';
|
||||
push(config.endpoints, main_udp_endpoint);
|
||||
}
|
||||
} else {
|
||||
const main_udp_outbound = generate_outbound(main_udp_node_cfg);
|
||||
if (main_udp_outbound) {
|
||||
main_udp_outbound.tag = 'main-udp-out';
|
||||
push(config.outbounds, main_udp_outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i in urltest_nodes) {
|
||||
const urltest_node = uci.get_all(uciconfig, i) || {};
|
||||
if (isEmpty(urltest_node))
|
||||
continue;
|
||||
|
||||
if (urltest_node.type === 'wireguard') {
|
||||
const endpoint = generate_endpoint(urltest_node);
|
||||
if (endpoint) {
|
||||
endpoint.tag = 'cfg-' + i + '-out';
|
||||
push(config.endpoints, endpoint);
|
||||
}
|
||||
} else {
|
||||
const outbound = generate_outbound(urltest_node);
|
||||
if (outbound) {
|
||||
outbound.tag = 'cfg-' + i + '-out';
|
||||
push(config.outbounds, outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!isEmpty(default_outbound)) {
|
||||
let urltest_nodes = [],
|
||||
routing_nodes = [];
|
||||
|
||||
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
if (cfg.node === 'urltest') {
|
||||
const urltest_list = filter_existing_nodes(normalize_list(cfg.urltest_nodes));
|
||||
push(config.outbounds, {
|
||||
type: 'urltest',
|
||||
tag: 'cfg-' + cfg['.name'] + '-out',
|
||||
outbounds: map(urltest_list, (k) => `cfg-${k}-out`),
|
||||
url: cfg.urltest_url,
|
||||
interval: strToTime(cfg.urltest_interval),
|
||||
tolerance: strToInt(cfg.urltest_tolerance),
|
||||
idle_timeout: strToTime(cfg.urltest_idle_timeout),
|
||||
interrupt_exist_connections: strToBool(cfg.urltest_interrupt_exist_connections)
|
||||
});
|
||||
urltest_nodes = [...urltest_nodes, ...filter(urltest_list, (l) => !~index(urltest_nodes, l))];
|
||||
} else {
|
||||
const outbound = uci.get_all(uciconfig, cfg.node) || {};
|
||||
if (isEmpty(outbound))
|
||||
return;
|
||||
|
||||
if (outbound.type === 'wireguard') {
|
||||
const endpoint = generate_endpoint(outbound);
|
||||
if (!endpoint)
|
||||
return;
|
||||
|
||||
endpoint.bind_interface = cfg.bind_interface;
|
||||
endpoint.detour = get_outbound(cfg.outbound);
|
||||
if (cfg.domain_resolver)
|
||||
endpoint.domain_resolver = {
|
||||
server: get_resolver(cfg.domain_resolver),
|
||||
strategy: cfg.domain_strategy
|
||||
};
|
||||
push(config.endpoints, endpoint);
|
||||
} else {
|
||||
const routed_outbound = generate_outbound(outbound);
|
||||
if (!routed_outbound)
|
||||
return;
|
||||
|
||||
routed_outbound.bind_interface = cfg.bind_interface;
|
||||
routed_outbound.detour = get_outbound(cfg.outbound);
|
||||
if (cfg.domain_resolver)
|
||||
routed_outbound.domain_resolver = {
|
||||
server: get_resolver(cfg.domain_resolver),
|
||||
strategy: cfg.domain_strategy
|
||||
};
|
||||
push(config.outbounds, routed_outbound);
|
||||
}
|
||||
push(routing_nodes, cfg.node);
|
||||
}
|
||||
});
|
||||
|
||||
for (let i in filter(urltest_nodes, (l) => !~index(routing_nodes, l))) {
|
||||
const urltest_node = uci.get_all(uciconfig, i) || {};
|
||||
if (isEmpty(urltest_node))
|
||||
continue;
|
||||
|
||||
if (urltest_node.type === 'wireguard') {
|
||||
const endpoint = generate_endpoint(urltest_node);
|
||||
if (endpoint)
|
||||
push(config.endpoints, endpoint);
|
||||
} else {
|
||||
const outbound = generate_outbound(urltest_node);
|
||||
if (outbound)
|
||||
push(config.outbounds, outbound);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEmpty(config.endpoints))
|
||||
config.endpoints = null;
|
||||
/* Outbound end */
|
||||
|
||||
/* Routing rules start */
|
||||
/* Default settings */
|
||||
config.route = {
|
||||
rules: [
|
||||
{
|
||||
inbound: 'dns-in',
|
||||
action: 'hijack-dns'
|
||||
},
|
||||
{
|
||||
action: 'sniff'
|
||||
}
|
||||
],
|
||||
rule_set: [],
|
||||
auto_detect_interface: isEmpty(default_interface) ? true : null,
|
||||
default_interface: default_interface
|
||||
};
|
||||
|
||||
/* Routing rules */
|
||||
if (!isEmpty(main_node)) {
|
||||
/* Avoid DNS loop */
|
||||
config.route.default_domain_resolver = {
|
||||
server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns',
|
||||
strategy: (ipv6_support !== '1') ? 'prefer_ipv4' : null
|
||||
};
|
||||
|
||||
/* Direct list */
|
||||
if (length(direct_domain_list))
|
||||
push(config.route.rules, {
|
||||
rule_set: 'direct-domain',
|
||||
action: 'route',
|
||||
outbound: 'direct-out'
|
||||
});
|
||||
|
||||
/* Main UDP out */
|
||||
if (dedicated_udp_node)
|
||||
push(config.route.rules, {
|
||||
network: 'udp',
|
||||
action: 'route',
|
||||
outbound: 'main-udp-out'
|
||||
});
|
||||
|
||||
/* Proxy list */
|
||||
if (length(proxy_domain_list))
|
||||
push(config.route.rules, {
|
||||
rule_set: 'proxy-domain',
|
||||
action: 'route',
|
||||
outbound: 'main-out'
|
||||
});
|
||||
|
||||
config.route.final = 'main-out';
|
||||
|
||||
/* Rule set */
|
||||
/* Direct list */
|
||||
if (length(direct_domain_list))
|
||||
push(config.route.rule_set, {
|
||||
type: 'inline',
|
||||
tag: 'direct-domain',
|
||||
rules: [
|
||||
{
|
||||
domain_keyword: direct_domain_list,
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/* Proxy list */
|
||||
if (length(proxy_domain_list))
|
||||
push(config.route.rule_set, {
|
||||
type: 'inline',
|
||||
tag: 'proxy-domain',
|
||||
rules: [
|
||||
{
|
||||
domain_keyword: proxy_domain_list,
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (routing_mode === 'bypass_mainland_china') {
|
||||
push(config.route.rule_set, {
|
||||
type: 'remote',
|
||||
tag: 'geoip-cn',
|
||||
format: 'binary',
|
||||
url: 'https://fastly.jsdelivr.net/gh/1715173329/IPCIDR-CHINA@rule-set/cn.srs',
|
||||
download_detour: 'direct-out'
|
||||
});
|
||||
push(config.route.rule_set, {
|
||||
type: 'remote',
|
||||
tag: 'geosite-cn',
|
||||
format: 'binary',
|
||||
url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-cn.srs',
|
||||
download_detour: 'direct-out'
|
||||
});
|
||||
push(config.route.rule_set, {
|
||||
type: 'remote',
|
||||
tag: 'geosite-noncn',
|
||||
format: 'binary',
|
||||
url: 'https://fastly.jsdelivr.net/gh/1715173329/sing-geosite@rule-set-unstable/geosite-geolocation-!cn.srs',
|
||||
download_detour: 'direct-out'
|
||||
});
|
||||
}
|
||||
|
||||
if (isEmpty(config.route.rule_set))
|
||||
config.route.rule_set = null;
|
||||
} else if (!isEmpty(default_outbound)) {
|
||||
config.route.default_domain_resolver = {
|
||||
server: get_resolver(default_outbound_dns)
|
||||
};
|
||||
|
||||
if (domain_strategy)
|
||||
push(config.route.rules, {
|
||||
action: 'resolve',
|
||||
strategy: domain_strategy
|
||||
});
|
||||
|
||||
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return null;
|
||||
|
||||
push(config.route.rules, {
|
||||
ip_version: strToInt(cfg.ip_version),
|
||||
protocol: cfg.protocol,
|
||||
network: cfg.network,
|
||||
domain: cfg.domain,
|
||||
domain_suffix: cfg.domain_suffix,
|
||||
domain_keyword: cfg.domain_keyword,
|
||||
domain_regex: cfg.domain_regex,
|
||||
source_ip_cidr: cfg.source_ip_cidr,
|
||||
source_ip_is_private: strToBool(cfg.source_ip_is_private),
|
||||
ip_cidr: cfg.ip_cidr,
|
||||
ip_is_private: strToBool(cfg.ip_is_private),
|
||||
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,
|
||||
process_path_regex: cfg.process_path_regex,
|
||||
user: cfg.user,
|
||||
rule_set: get_ruleset(cfg.rule_set),
|
||||
rule_set_ip_cidr_match_source: strToBool(cfg.rule_set_ip_cidr_match_source),
|
||||
invert: strToBool(cfg.invert),
|
||||
action: cfg.action,
|
||||
outbound: get_outbound(cfg.outbound),
|
||||
override_address: cfg.override_address,
|
||||
override_port: strToInt(cfg.override_port),
|
||||
udp_disable_domain_unmapping: strToBool(cfg.udp_disable_domain_unmapping),
|
||||
udp_connect: strToBool(cfg.udp_connect),
|
||||
udp_timeout: strToTime(cfg.udp_timeout),
|
||||
tls_fragment: strToBool(cfg.tls_fragment),
|
||||
tls_fragment_fallback_delay: strToTime(cfg.tls_fragment_fallback_delay),
|
||||
tls_record_fragment: strToBool(cfg.tls_record_fragment)
|
||||
});
|
||||
});
|
||||
|
||||
if (default_outbound === 'reject')
|
||||
push(config.route.rules, { action: 'reject' });
|
||||
else
|
||||
config.route.final = get_outbound(default_outbound);
|
||||
|
||||
/* Rule set */
|
||||
uci.foreach(uciconfig, uciruleset, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return null;
|
||||
|
||||
push(config.route.rule_set, {
|
||||
type: cfg.type,
|
||||
tag: 'cfg-' + cfg['.name'] + '-rule',
|
||||
format: cfg.format,
|
||||
path: cfg.path,
|
||||
url: cfg.url,
|
||||
download_detour: get_outbound(cfg.outbound) || ((cfg.type === 'remote') ? 'direct-out' : null),
|
||||
update_interval: cfg.update_interval
|
||||
});
|
||||
});
|
||||
}
|
||||
/* Routing rules end */
|
||||
|
||||
/* Experimental start */
|
||||
if (routing_mode in ['bypass_mainland_china', 'custom']) {
|
||||
config.experimental = {
|
||||
clash_api: {
|
||||
external_controller: '127.0.0.1:9090'
|
||||
},
|
||||
cache_file: {
|
||||
enabled: true,
|
||||
path: HP_DIR + '/cache/cache.db',
|
||||
store_rdrc: strToBool(cache_file_store_rdrc),
|
||||
rdrc_timeout: strToTime(cache_file_rdrc_timeout),
|
||||
}
|
||||
};
|
||||
}
|
||||
/* Experimental end */
|
||||
|
||||
system('mkdir -p ' + RUN_DIR);
|
||||
if (!writefile(RUN_DIR + '/sing-box-c.json.new', sprintf('%.J\n', removeBlankAttrs(config))))
|
||||
exit(1);
|
||||
@ -1,186 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import {
|
||||
isEmpty, strToBool, strToInt, strToTime,
|
||||
removeBlankAttrs, HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
/* UCI config start */
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
const uciserver = 'server';
|
||||
|
||||
const log_level = uci.get(uciconfig, uciserver, 'log_level') || 'warn';
|
||||
/* UCI config end */
|
||||
|
||||
const config = {};
|
||||
|
||||
/* Log */
|
||||
config.log = {
|
||||
disabled: false,
|
||||
level: log_level,
|
||||
output: RUN_DIR + '/sing-box-s.log',
|
||||
timestamp: true
|
||||
};
|
||||
|
||||
config.inbounds = [];
|
||||
|
||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
push(config.inbounds, {
|
||||
type: cfg.type,
|
||||
tag: 'cfg-' + cfg['.name'] + '-in',
|
||||
|
||||
listen: cfg.address || '::',
|
||||
listen_port: strToInt(cfg.port),
|
||||
bind_interface: cfg.bind_interface,
|
||||
reuse_addr: strToBool(cfg.reuse_addr),
|
||||
tcp_fast_open: strToBool(cfg.tcp_fast_open),
|
||||
tcp_multi_path: strToBool(cfg.tcp_multi_path),
|
||||
udp_fragment: strToBool(cfg.udp_fragment),
|
||||
udp_timeout: strToTime(cfg.udp_timeout),
|
||||
network: cfg.network,
|
||||
|
||||
/* AnyTLS */
|
||||
padding_scheme: cfg.anytls_padding_scheme,
|
||||
|
||||
/* Hysteria */
|
||||
up_mbps: strToInt(cfg.hysteria_up_mbps),
|
||||
down_mbps: strToInt(cfg.hysteria_down_mbps),
|
||||
obfs: cfg.hysteria_obfs_type ? {
|
||||
type: cfg.hysteria_obfs_type,
|
||||
password: cfg.hysteria_obfs_password
|
||||
} : cfg.hysteria_obfs_password,
|
||||
recv_window_conn: strToInt(cfg.hysteria_recv_window_conn),
|
||||
recv_window_client: strToInt(cfg.hysteria_revc_window_client),
|
||||
max_conn_client: strToInt(cfg.hysteria_max_conn_client),
|
||||
disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery),
|
||||
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
|
||||
masquerade: cfg.hysteria_masquerade,
|
||||
|
||||
/* Shadowsocks */
|
||||
method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
|
||||
password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
|
||||
|
||||
/* Tuic */
|
||||
congestion_control: cfg.tuic_congestion_control,
|
||||
auth_timeout: strToTime(cfg.tuic_auth_timeout),
|
||||
zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt),
|
||||
heartbeat: strToTime(cfg.tuic_heartbeat),
|
||||
|
||||
/* AnyTLS / HTTP / Hysteria (2) / Mixed / Socks / Trojan / Tuic / VLESS / VMess */
|
||||
users: (cfg.type !== 'shadowsocks') ? [
|
||||
{
|
||||
name: !(cfg.type in ['http', 'mixed', 'naive', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null,
|
||||
username: cfg.username,
|
||||
password: cfg.password,
|
||||
|
||||
/* Hysteria */
|
||||
auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
|
||||
auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
|
||||
|
||||
/* Tuic */
|
||||
uuid: cfg.uuid,
|
||||
|
||||
/* VLESS / VMess */
|
||||
flow: cfg.vless_flow,
|
||||
alterId: strToInt(cfg.vmess_alterid)
|
||||
}
|
||||
] : null,
|
||||
|
||||
multiplex: (cfg.multiplex === '1') ? {
|
||||
enabled: true,
|
||||
padding: strToBool(cfg.multiplex_padding),
|
||||
brutal: (cfg.multiplex_brutal === '1') ? {
|
||||
enabled: true,
|
||||
up_mbps: strToInt(cfg.multiplex_brutal_up),
|
||||
down_mbps: strToInt(cfg.multiplex_brutal_down)
|
||||
} : null
|
||||
} : 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_domain,
|
||||
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: strToBool(cfg.tls_acme_dhc),
|
||||
disable_tls_alpn_challenge: strToBool(cfg.tls_acme_dtac),
|
||||
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,
|
||||
dns01_challenge: (cfg.tls_dns01_challenge === '1') ? {
|
||||
provider: cfg.tls_dns01_provider,
|
||||
access_key_id: cfg.tls_dns01_ali_akid,
|
||||
access_key_secret: cfg.tls_dns01_ali_aksec,
|
||||
region_id: cfg.tls_dns01_ali_rid,
|
||||
api_token: cfg.tls_dns01_cf_api_token
|
||||
} : null
|
||||
} : null,
|
||||
ech: (cfg.tls_ech_key) ? {
|
||||
enabled: true,
|
||||
key: split(cfg.tls_ech_key, '\n'),
|
||||
// config: split(cfg.tls_ech_config, '\n')
|
||||
} : null,
|
||||
reality: (cfg.tls_reality === '1') ? {
|
||||
enabled: true,
|
||||
private_key: cfg.tls_reality_private_key,
|
||||
short_id: cfg.tls_reality_short_id,
|
||||
max_time_difference: strToTime(cfg.tls_reality_max_time_difference),
|
||||
handshake: {
|
||||
server: cfg.tls_reality_server_addr,
|
||||
server_port: strToInt(cfg.tls_reality_server_port)
|
||||
}
|
||||
} : null
|
||||
} : null,
|
||||
|
||||
transport: !isEmpty(cfg.transport) ? {
|
||||
type: cfg.transport,
|
||||
host: cfg.http_host || cfg.httpupgrade_host,
|
||||
path: cfg.http_path || cfg.ws_path,
|
||||
headers: cfg.ws_host ? {
|
||||
Host: cfg.ws_host
|
||||
} : null,
|
||||
method: cfg.http_method,
|
||||
max_early_data: strToInt(cfg.websocket_early_data),
|
||||
early_data_header_name: cfg.websocket_early_data_header,
|
||||
service_name: cfg.grpc_servicename,
|
||||
idle_timeout: strToTime(cfg.http_idle_timeout),
|
||||
ping_timeout: strToTime(cfg.http_ping_timeout)
|
||||
} : null
|
||||
});
|
||||
});
|
||||
|
||||
if (length(config.inbounds) === 0)
|
||||
exit(1);
|
||||
|
||||
system('mkdir -p ' + RUN_DIR);
|
||||
if (!writefile(RUN_DIR + '/sing-box-s.json.new', sprintf('%.J\n', removeBlankAttrs(config))))
|
||||
exit(1);
|
||||
@ -1,385 +0,0 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
import { mkstemp } from 'fs';
|
||||
import { 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 getTime(epoch) {
|
||||
const local_time = localtime(epoch);
|
||||
return replace(replace(sprintf(
|
||||
'%d-%2d-%2d@%2d:%2d:%2d',
|
||||
local_time.year,
|
||||
local_time.mon,
|
||||
local_time.mday,
|
||||
local_time.hour,
|
||||
local_time.min,
|
||||
local_time.sec
|
||||
), ' ', '0'), '@', ' ');
|
||||
|
||||
};
|
||||
|
||||
export function wGET(url, ua) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
if (!ua)
|
||||
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
|
||||
|
||||
const output = executeCommand(`/usr/bin/wget -qO- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
|
||||
return trim(output.stdout);
|
||||
};
|
||||
/* Utilities end */
|
||||
|
||||
/* String helper start */
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
|
||||
export function strToBool(str) {
|
||||
return (str === '1') || null;
|
||||
};
|
||||
|
||||
export function strToInt(str) {
|
||||
return !isEmpty(str) ? (int(str) || null) : null;
|
||||
};
|
||||
|
||||
export function strToTime(str) {
|
||||
return !isEmpty(str) ? (str + 's') : null;
|
||||
};
|
||||
|
||||
function strListToInts(value) {
|
||||
if (type(value) !== 'array' || isEmpty(value))
|
||||
return null;
|
||||
|
||||
return map(value, (item) => int(item));
|
||||
};
|
||||
|
||||
export function renderEndpoint(node) {
|
||||
if (type(node) !== 'object' || isEmpty(node))
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: node.type,
|
||||
tag: 'cfg-' + node['.name'] + '-out',
|
||||
address: node.wireguard_local_address,
|
||||
mtu: strToInt(node.wireguard_mtu),
|
||||
private_key: node.wireguard_private_key,
|
||||
peers: (node.type === 'wireguard') ? [
|
||||
{
|
||||
address: node.address,
|
||||
port: strToInt(node.port),
|
||||
allowed_ips: [
|
||||
'0.0.0.0/0',
|
||||
'::/0'
|
||||
],
|
||||
persistent_keepalive_interval: strToInt(node.wireguard_persistent_keepalive_interval),
|
||||
public_key: node.wireguard_peer_public_key,
|
||||
pre_shared_key: node.wireguard_pre_shared_key,
|
||||
reserved: strListToInts(node.wireguard_reserved)
|
||||
}
|
||||
] : null,
|
||||
system: (node.type === 'wireguard') ? false : null,
|
||||
tcp_fast_open: strToBool(node.tcp_fast_open),
|
||||
tcp_multi_path: strToBool(node.tcp_multi_path),
|
||||
udp_fragment: strToBool(node.udp_fragment)
|
||||
};
|
||||
};
|
||||
|
||||
export function renderOutbound(node, routingMark) {
|
||||
if (type(node) !== 'object' || isEmpty(node))
|
||||
return null;
|
||||
|
||||
const tls_utls_value = (node.type === 'anytls' && isEmpty(node.tls_utls)) ? 'chrome' : node.tls_utls;
|
||||
return {
|
||||
type: node.type,
|
||||
tag: 'cfg-' + node['.name'] + '-out',
|
||||
routing_mark: strToInt(routingMark),
|
||||
|
||||
server: node.address,
|
||||
server_port: strToInt(node.port),
|
||||
server_ports: node.hysteria_hopping_port,
|
||||
|
||||
username: (node.type !== 'ssh') ? node.username : null,
|
||||
user: (node.type === 'ssh') ? node.username : null,
|
||||
password: node.password,
|
||||
|
||||
idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval),
|
||||
idle_session_timeout: strToTime(node.anytls_idle_session_timeout),
|
||||
min_idle_session: strToInt(node.anytls_min_idle_session),
|
||||
hop_interval: strToTime(node.hysteria_hop_interval),
|
||||
up_mbps: strToInt(node.hysteria_up_mbps),
|
||||
down_mbps: strToInt(node.hysteria_down_mbps),
|
||||
obfs: node.hysteria_obfs_type ? {
|
||||
type: node.hysteria_obfs_type,
|
||||
password: node.hysteria_obfs_password
|
||||
} : node.hysteria_obfs_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: strToBool(node.hysteria_disable_mtu_discovery),
|
||||
method: node.shadowsocks_encrypt_method,
|
||||
plugin: node.shadowsocks_plugin,
|
||||
plugin_opts: node.shadowsocks_plugin_opts,
|
||||
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : null),
|
||||
client_version: node.ssh_client_version,
|
||||
host_key: node.ssh_host_key,
|
||||
host_key_algorithms: node.ssh_host_key_algo,
|
||||
private_key: node.ssh_priv_key,
|
||||
private_key_passphrase: node.ssh_priv_key_pp,
|
||||
uuid: node.uuid,
|
||||
congestion_control: node.tuic_congestion_control,
|
||||
udp_relay_mode: node.tuic_udp_relay_mode,
|
||||
udp_over_stream: strToBool(node.tuic_udp_over_stream),
|
||||
zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt),
|
||||
heartbeat: strToTime(node.tuic_heartbeat),
|
||||
flow: node.vless_flow,
|
||||
alter_id: strToInt(node.vmess_alterid),
|
||||
security: node.vmess_encrypt,
|
||||
global_padding: strToBool(node.vmess_global_padding),
|
||||
authenticated_length: strToBool(node.vmess_authenticated_length),
|
||||
packet_encoding: node.packet_encoding,
|
||||
|
||||
multiplex: (node.multiplex === '1') ? {
|
||||
enabled: true,
|
||||
protocol: node.multiplex_protocol,
|
||||
max_connections: strToInt(node.multiplex_max_connections),
|
||||
min_streams: strToInt(node.multiplex_min_streams),
|
||||
max_streams: strToInt(node.multiplex_max_streams),
|
||||
padding: strToBool(node.multiplex_padding),
|
||||
brutal: (node.multiplex_brutal === '1') ? {
|
||||
enabled: true,
|
||||
up_mbps: strToInt(node.multiplex_brutal_up),
|
||||
down_mbps: strToInt(node.multiplex_brutal_down)
|
||||
} : null
|
||||
} : null,
|
||||
tls: (node.tls === '1') ? {
|
||||
enabled: true,
|
||||
server_name: node.tls_sni,
|
||||
insecure: strToBool(node.tls_insecure),
|
||||
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.tls_ech === '1') ? {
|
||||
enabled: true,
|
||||
config: node.tls_ech_config,
|
||||
config_path: node.tls_ech_config_path
|
||||
} : null,
|
||||
utls: !isEmpty(tls_utls_value) ? {
|
||||
enabled: true,
|
||||
fingerprint: tls_utls_value
|
||||
} : null,
|
||||
reality: (node.tls_reality === '1') ? {
|
||||
enabled: true,
|
||||
public_key: node.tls_reality_public_key,
|
||||
short_id: node.tls_reality_short_id
|
||||
} : null
|
||||
} : null,
|
||||
transport: !isEmpty(node.transport) ? {
|
||||
type: node.transport,
|
||||
host: node.http_host || node.httpupgrade_host,
|
||||
path: node.http_path || node.ws_path,
|
||||
headers: node.ws_host ? {
|
||||
Host: node.ws_host
|
||||
} : null,
|
||||
method: node.http_method,
|
||||
max_early_data: strToInt(node.websocket_early_data),
|
||||
early_data_header_name: node.websocket_early_data_header,
|
||||
service_name: node.grpc_servicename,
|
||||
idle_timeout: strToTime(node.http_idle_timeout),
|
||||
ping_timeout: strToTime(node.http_ping_timeout),
|
||||
permit_without_stream: strToBool(node.grpc_permit_without_stream)
|
||||
} : null,
|
||||
udp_over_tcp: (node.udp_over_tcp === '1') ? {
|
||||
enabled: true,
|
||||
version: strToInt(node.udp_over_tcp_version)
|
||||
} : null,
|
||||
tcp_fast_open: strToBool(node.tcp_fast_open),
|
||||
tcp_multi_path: strToBool(node.tcp_multi_path),
|
||||
udp_fragment: strToBool(node.udp_fragment)
|
||||
};
|
||||
};
|
||||
|
||||
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 === 0);
|
||||
};
|
||||
/* 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) ||
|
||||
validation('ip6addr', replace(val, /\[|\]/g, '')) ||
|
||||
validation('hostname', val))
|
||||
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 end */
|
||||
@ -1,306 +0,0 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty, parseURL, validation } from 'homeproxy';
|
||||
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
const uciinfra = 'infra',
|
||||
ucimigration = 'migration',
|
||||
ucimain = 'config',
|
||||
ucinode = 'node',
|
||||
ucidns = 'dns',
|
||||
ucidnsserver = 'dns_server',
|
||||
ucidnsrule = 'dns_rule',
|
||||
ucirouting = 'routing',
|
||||
uciroutingnode = 'routing_node',
|
||||
uciroutingrule = 'routing_rule',
|
||||
uciserver = 'server';
|
||||
|
||||
/* chinadns-ng has been removed */
|
||||
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
|
||||
uci.delete(uciconfig, uciinfra, 'china_dns_port');
|
||||
|
||||
/* chinadns server now only accepts single server */
|
||||
const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
|
||||
if (type(china_dns_server) === 'array') {
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]);
|
||||
} else {
|
||||
if (match(china_dns_server, /,/))
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
|
||||
}
|
||||
|
||||
/* github_token option has been moved to config section */
|
||||
const github_token = uci.get(uciconfig, uciinfra, 'github_token');
|
||||
if (github_token) {
|
||||
uci.set(uciconfig, ucimain, 'github_token', github_token);
|
||||
uci.delete(uciconfig, uciinfra, 'github_token')
|
||||
}
|
||||
|
||||
/* ntp_server was introduced */
|
||||
if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
|
||||
uci.set(uciconfig, uciinfra, 'ntp_server', 'nil');
|
||||
|
||||
/* tun_gso was deprecated in sb 1.11 */
|
||||
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
|
||||
uci.delete(uciconfig, uciinfra, 'tun_gso');
|
||||
|
||||
/* endpoint_independent_nat has been removed */
|
||||
if (!isEmpty(uci.get(uciconfig, ucirouting, 'endpoint_independent_nat')))
|
||||
uci.delete(uciconfig, ucirouting, 'endpoint_independent_nat');
|
||||
|
||||
/* legacy inbound sniff fields were replaced by route actions */
|
||||
if (!isEmpty(uci.get(uciconfig, uciinfra, 'sniff_override')))
|
||||
uci.delete(uciconfig, uciinfra, 'sniff_override');
|
||||
if (!isEmpty(uci.get(uciconfig, ucirouting, 'sniff_override')))
|
||||
uci.delete(uciconfig, ucirouting, 'sniff_override');
|
||||
|
||||
/* block-out was replaced by the reject route action */
|
||||
if (uci.get(uciconfig, ucirouting, 'default_outbound') === 'block-out')
|
||||
uci.set(uciconfig, ucirouting, 'default_outbound', 'reject');
|
||||
|
||||
/* create migration section */
|
||||
if (!uci.get(uciconfig, ucimigration))
|
||||
uci.set(uciconfig, ucimigration, uciconfig);
|
||||
|
||||
/* delete old crontab command */
|
||||
const migration_crontab = uci.get(uciconfig, ucimigration, 'crontab');
|
||||
if (!migration_crontab) {
|
||||
system('sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>"/dev/null"');
|
||||
uci.set(uciconfig, ucimigration, 'crontab', '1');
|
||||
}
|
||||
|
||||
/* log_level was introduced */
|
||||
if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
|
||||
uci.set(uciconfig, ucimain, 'log_level', 'warn');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
|
||||
uci.set(uciconfig, uciserver, 'log_level', 'warn');
|
||||
|
||||
/* empty value defaults to all ports now */
|
||||
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
|
||||
uci.delete(uciconfig, ucimain, 'routing_port');
|
||||
|
||||
/* experimental section was removed */
|
||||
if (uci.get(uciconfig, 'experimental'))
|
||||
uci.delete(uciconfig, 'experimental');
|
||||
|
||||
/* block-dns was removed from built-in dns servers */
|
||||
const default_dns_server = uci.get(uciconfig, ucidns, 'default_server');
|
||||
|
||||
/* dns_strategy was renamed to default_strategy */
|
||||
const legacy_dns_strategy = uci.get(uciconfig, ucidns, 'dns_strategy');
|
||||
if (!isEmpty(legacy_dns_strategy)) {
|
||||
if (isEmpty(uci.get(uciconfig, ucidns, 'default_strategy')))
|
||||
uci.set(uciconfig, ucidns, 'default_strategy', legacy_dns_strategy);
|
||||
uci.delete(uciconfig, ucidns, 'dns_strategy');
|
||||
}
|
||||
|
||||
if (default_dns_server === 'block-dns') {
|
||||
/* append a rule at last to block all DNS queries */
|
||||
uci.set(uciconfig, '_migration_dns_final_block', ucidnsrule);
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'label', 'migration_final_block_dns');
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'enabled', '1');
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'mode', 'default');
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'action', 'reject');
|
||||
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
|
||||
} else if (default_dns_server === 'local-dns')
|
||||
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
|
||||
|
||||
const dns_server_migration = {};
|
||||
/* DNS servers options */
|
||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||
/* legacy DNS server resolver fields were moved into dial fields */
|
||||
if (!isEmpty(cfg.address_resolver)) {
|
||||
if (isEmpty(cfg.domain_resolver))
|
||||
uci.set(uciconfig, cfg['.name'], 'domain_resolver', cfg.address_resolver);
|
||||
uci.delete(uciconfig, cfg['.name'], 'address_resolver');
|
||||
}
|
||||
if (!isEmpty(cfg.address_strategy)) {
|
||||
if (isEmpty(cfg.domain_strategy))
|
||||
uci.set(uciconfig, cfg['.name'], 'domain_strategy', cfg.address_strategy);
|
||||
uci.delete(uciconfig, cfg['.name'], 'address_strategy');
|
||||
}
|
||||
|
||||
/* legacy format was deprecated in sb 1.12 */
|
||||
if (cfg.address) {
|
||||
const addr = parseURL((!match(cfg.address, /:\/\//) ? 'udp://' : '') + (validation('ip6addr', cfg.address) ? `[${cfg.address}]` : cfg.address));
|
||||
if (!addr)
|
||||
return;
|
||||
|
||||
/* RCode was moved into DNS rules */
|
||||
if (addr.protocol === 'rcode') {
|
||||
dns_server_migration[cfg['.name']] = { action: 'predefined' };
|
||||
switch (addr.hostname) {
|
||||
case 'success':
|
||||
dns_server_migration[cfg['.name']].rcode = 'NOERROR';
|
||||
break;
|
||||
case 'format_error':
|
||||
dns_server_migration[cfg['.name']].rcode = 'FORMERR';
|
||||
break;
|
||||
case 'server_failure':
|
||||
dns_server_migration[cfg['.name']].rcode = 'SERVFAIL';
|
||||
break;
|
||||
case 'name_error':
|
||||
dns_server_migration[cfg['.name']].rcode = 'NXDOMAIN';
|
||||
break;
|
||||
case 'not_implemented':
|
||||
dns_server_migration[cfg['.name']].rcode = 'NOTIMP';
|
||||
break;
|
||||
case 'refused':
|
||||
default:
|
||||
dns_server_migration[cfg['.name']].rcode = 'REFUSED';
|
||||
break;
|
||||
}
|
||||
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
return;
|
||||
}
|
||||
uci.set(uciconfig, cfg['.name'], 'type', addr.protocol);
|
||||
uci.set(uciconfig, cfg['.name'], 'server', addr.hostname);
|
||||
uci.set(uciconfig, cfg['.name'], 'server_port', addr.port);
|
||||
uci.set(uciconfig, cfg['.name'], 'path', (addr.pathname !== '/') ? addr.pathname : null);
|
||||
uci.delete(uciconfig, cfg['.name'], 'address');
|
||||
}
|
||||
|
||||
if (cfg.strategy) {
|
||||
if (cfg['.name'] === default_dns_server)
|
||||
uci.set(uciconfig, ucidns, 'default_strategy', cfg.strategy);
|
||||
dns_server_migration[cfg['.name']] = { strategy: cfg.strategy };
|
||||
uci.delete(uciconfig, cfg['.name'], 'strategy');
|
||||
}
|
||||
|
||||
if (cfg.client_subnet) {
|
||||
if (cfg['.name'] === default_dns_server)
|
||||
uci.set(uciconfig, ucidns, 'client_subnet', cfg.client_subnet);
|
||||
|
||||
if (isEmpty(dns_server_migration[cfg['.name']]))
|
||||
dns_server_migration[cfg['.name']] = {};
|
||||
dns_server_migration[cfg['.name']].client_subnet = cfg.client_subnet;
|
||||
uci.delete(uciconfig, cfg['.name'], 'client_subnet');
|
||||
}
|
||||
});
|
||||
|
||||
/* DNS rules options */
|
||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
/* outbound was removed in sb 1.12 */
|
||||
if (cfg.outbound) {
|
||||
uci.delete(uciconfig, cfg['.name']);
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
const outbounds = (type(cfg.outbound) === 'array') ? cfg.outbound : [ cfg.outbound ];
|
||||
for (let outbound in outbounds) {
|
||||
switch (outbound) {
|
||||
case 'direct-out':
|
||||
case 'block-out':
|
||||
break;
|
||||
case 'any-out':
|
||||
uci.set(uciconfig, ucirouting, 'default_outbound_dns', cfg.server);
|
||||
break;
|
||||
default:
|
||||
uci.set(uciconfig, outbound, 'domain_resolver', cfg.server);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
|
||||
if (!isEmpty(cfg.rule_set_ipcidr_match_source))
|
||||
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
||||
|
||||
/* block-dns was moved into action in sb 1.11 */
|
||||
if (cfg.server === 'block-dns') {
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
||||
uci.delete(uciconfig, cfg['.name'], 'server');
|
||||
} else if (!cfg.action) {
|
||||
/* add missing 'action' field */
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
||||
}
|
||||
|
||||
/* strategy and client_subnet were moved into dns rules */
|
||||
if (dns_server_migration[cfg.server]) {
|
||||
if (dns_server_migration[cfg.server].strategy)
|
||||
uci.set(uciconfig, cfg['.name'], 'strategy', dns_server_migration[cfg.server].strategy);
|
||||
|
||||
if (dns_server_migration[cfg.server].client_subnet)
|
||||
uci.set(uciconfig, cfg['.name'], 'client_subnet', dns_server_migration[cfg.server].client_subnet);
|
||||
|
||||
if (dns_server_migration[cfg.server].rcode) {
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'predefined');
|
||||
uci.set(uciconfig, cfg['.name'], 'rcode', dns_server_migration[cfg.server].rcode);
|
||||
uci.delete(uciconfig, cfg['.name'], 'server');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/* nodes options */
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* destination override and Proxy Protocol were removed from direct outbound */
|
||||
for (let option in ['override_address', 'override_port', 'proxy_protocol'])
|
||||
if (!isEmpty(cfg[option]))
|
||||
uci.delete(uciconfig, cfg['.name'], option);
|
||||
|
||||
/* tls_ech_tls_disable_drs is useless and deprecated in sb 1.12 */
|
||||
if (!isEmpty(cfg.tls_ech_tls_disable_drs))
|
||||
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
|
||||
|
||||
/* tls_ech_enable_pqss is useless and deprecated in sb 1.12 */
|
||||
if (!isEmpty(cfg.tls_ech_enable_pqss))
|
||||
uci.delete(uciconfig, cfg['.name'], 'tls_ech_enable_pqss');
|
||||
|
||||
/* wireguard_gso was deprecated in sb 1.11 */
|
||||
if (!isEmpty(cfg.wireguard_gso))
|
||||
uci.delete(uciconfig, cfg['.name'], 'wireguard_gso');
|
||||
});
|
||||
|
||||
/* routing rules options */
|
||||
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
|
||||
if (!isEmpty(cfg.rule_set_ipcidr_match_source))
|
||||
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
||||
|
||||
/* block-out was moved into action in sb 1.11 */
|
||||
if (cfg.outbound === 'block-out') {
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
||||
uci.delete(uciconfig, cfg['.name'], 'outbound');
|
||||
} else if (!cfg.action) {
|
||||
/* add missing 'action' field */
|
||||
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
||||
}
|
||||
});
|
||||
|
||||
/* server options */
|
||||
/* auto_firewall was moved into server options */
|
||||
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
|
||||
if (!isEmpty(auto_firewall))
|
||||
uci.delete(uciconfig, uciserver, 'auto_firewall');
|
||||
|
||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
/* auto_firewall was moved into server options */
|
||||
if (auto_firewall === '1')
|
||||
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
|
||||
|
||||
/* sniff_override was deprecated in sb 1.11 */
|
||||
if (!isEmpty(cfg.sniff_override))
|
||||
uci.delete(uciconfig, cfg['.name'], 'sniff_override');
|
||||
|
||||
/* domain_strategy is now pointless without sniff override */
|
||||
if (!isEmpty(cfg.domain_strategy))
|
||||
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
|
||||
});
|
||||
|
||||
if (!isEmpty(uci.changes(uciconfig)))
|
||||
uci.commit(uciconfig);
|
||||
@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2023 ImmortalWrt.org
|
||||
|
||||
SCRIPTS_DIR="/etc/homeproxy/scripts"
|
||||
|
||||
"$SCRIPTS_DIR"/update_resources.sh
|
||||
|
||||
"$SCRIPTS_DIR"/update_subscriptions.uc
|
||||
@ -1,102 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2025 ImmortalWrt.org
|
||||
|
||||
NAME="homeproxy"
|
||||
|
||||
RESOURCES_DIR="${RESOURCES_DIR:-/etc/$NAME/resources}"
|
||||
RUN_DIR="${RUN_DIR:-/var/run/$NAME}"
|
||||
LOG_PATH="$RUN_DIR/$NAME.log"
|
||||
REPO_NAME="Loyalsoldier/surge-rules"
|
||||
REPO_BRANCH="release"
|
||||
SOURCE_BASE="${SOURCE_BASE:-https://cdn.jsdelivr.net/gh/$REPO_NAME@$REPO_BRANCH}"
|
||||
RELEASE_API="${RELEASE_API:-https://api.github.com/repos/$REPO_NAME/releases/latest}"
|
||||
|
||||
mkdir -p "$RESOURCES_DIR" "$RUN_DIR"
|
||||
|
||||
log() {
|
||||
printf '%s %s\n' "$(date "+%Y-%m-%d %H:%M:%S")" "$*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
download() {
|
||||
local source_file="$1"
|
||||
local target_file="$2"
|
||||
|
||||
curl -fsSL --retry 2 --connect-timeout 10 --max-time 60 \
|
||||
-o "$target_file" "$SOURCE_BASE/$source_file" && [ -s "$target_file" ]
|
||||
}
|
||||
|
||||
exec 9>"$RUN_DIR/update_resources.lock"
|
||||
if ! flock -n 9 > "/dev/null" 2>&1; then
|
||||
log "[RESOURCES] A task is already running."
|
||||
exit 2
|
||||
fi
|
||||
|
||||
GITHUB_TOKEN="${GITHUB_TOKEN:-$(uci -q get homeproxy.config.github_token)}"
|
||||
if [ -n "$GITHUB_TOKEN" ]; then
|
||||
NEW_VER="$(curl -fsSL --retry 2 --connect-timeout 10 --max-time 30 \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"$RELEASE_API" | jsonfilter -e "@.tag_name")"
|
||||
else
|
||||
NEW_VER="$(curl -fsSL --retry 2 --connect-timeout 10 --max-time 30 \
|
||||
"$RELEASE_API" | jsonfilter -e "@.tag_name")"
|
||||
fi
|
||||
if [ -z "$NEW_VER" ]; then
|
||||
log "[RESOURCES] Failed to get the latest version, please retry later."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT=1
|
||||
for RESOURCE in china_ip4 china_ip6 china_list gfw_list; do
|
||||
OLD_VER="$(cat "$RESOURCES_DIR/$RESOURCE.ver" 2>/dev/null || echo "NOT FOUND")"
|
||||
if [ "$OLD_VER" = "$NEW_VER" ]; then
|
||||
log "[$RESOURCE] Current version: $NEW_VER."
|
||||
else
|
||||
CURRENT=0
|
||||
log "[$RESOURCE] Local version: $OLD_VER, latest version: $NEW_VER."
|
||||
fi
|
||||
done
|
||||
[ "$CURRENT" -eq 0 ] || {
|
||||
log "[RESOURCES] You're already at the latest version."
|
||||
exit 3
|
||||
}
|
||||
|
||||
TMP_DIR="$(mktemp -d "$RUN_DIR/resources-update.XXXXXX")" || exit 1
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT INT TERM
|
||||
|
||||
if ! download "cncidr.txt" "$TMP_DIR/cncidr.txt" || \
|
||||
! download "direct.txt" "$TMP_DIR/china_list.txt" || \
|
||||
! download "gfw.txt" "$TMP_DIR/gfw_list.txt"; then
|
||||
log "[RESOURCES] Update failed while downloading source lists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp "$TMP_DIR/cncidr.txt" "$TMP_DIR/china_ip4.txt" && \
|
||||
cp "$TMP_DIR/cncidr.txt" "$TMP_DIR/china_ip6.txt" && \
|
||||
sed -i "/IP-CIDR6,/d; s/IP-CIDR,//g" "$TMP_DIR/china_ip4.txt" && \
|
||||
sed -i "/IP-CIDR,/d; s/IP-CIDR6,//g" "$TMP_DIR/china_ip6.txt" && \
|
||||
sed -i "s/^\\.//g" "$TMP_DIR/china_list.txt" "$TMP_DIR/gfw_list.txt"
|
||||
if [ "$?" -ne 0 ]; then
|
||||
log "[RESOURCES] Update failed while processing source lists."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for RESOURCE in china_ip4 china_ip6 china_list gfw_list; do
|
||||
if [ ! -s "$TMP_DIR/$RESOURCE.txt" ] || grep -qi '<html' "$TMP_DIR/$RESOURCE.txt"; then
|
||||
log "[$RESOURCE] Update failed: invalid processed list."
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$NEW_VER" > "$TMP_DIR/$RESOURCE.ver" || exit 1
|
||||
done
|
||||
|
||||
for RESOURCE in china_ip4 china_ip6 china_list gfw_list; do
|
||||
if ! mv -f "$TMP_DIR/$RESOURCE.txt" "$RESOURCES_DIR/$RESOURCE.txt" || \
|
||||
! mv -f "$TMP_DIR/$RESOURCE.ver" "$RESOURCES_DIR/$RESOURCE.ver"; then
|
||||
log "[$RESOURCE] Update failed: unable to replace list."
|
||||
exit 1
|
||||
fi
|
||||
log "[$RESOURCE] Successfully updated."
|
||||
done
|
||||
|
||||
exit 0
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,409 +0,0 @@
|
||||
#!/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"
|
||||
CACHE_DIR="$HP_DIR/cache"
|
||||
CACHE_PATH="$CACHE_DIR/cache.db"
|
||||
|
||||
# we don't know which is the default server, just take the first one
|
||||
DNSMASQ_UCI_CONFIG="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')"
|
||||
if [ -f "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG" ]; then
|
||||
DNSMASQ_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG")/dnsmasq-homeproxy.d"
|
||||
else
|
||||
DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d"
|
||||
fi
|
||||
|
||||
log() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
validate_cron() {
|
||||
local expression="$1" field
|
||||
|
||||
case "$expression" in
|
||||
""|*[!0-9*/?,\ -]*) return 1 ;;
|
||||
esac
|
||||
|
||||
set -f
|
||||
set -- $expression
|
||||
set +f
|
||||
[ "$#" -eq 5 ] || return 1
|
||||
|
||||
for field in "$@"; do
|
||||
[ -n "$field" ] || return 1
|
||||
done
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local routing_mode proxy_mode
|
||||
config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
|
||||
config_get proxy_mode "config" "proxy_mode" "redirect_tproxy"
|
||||
|
||||
local 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"
|
||||
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
|
||||
|
||||
# Generate and validate every enabled instance before changing system state.
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
if ! ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH" || \
|
||||
[ ! -s "$RUN_DIR/sing-box-c.json.new" ]; then
|
||||
log "Error: failed to generate client configuration."
|
||||
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
|
||||
return 1
|
||||
elif ! "$PROG" check --config "$RUN_DIR/sing-box-c.json.new" 2>>"$LOG_PATH"; then
|
||||
log "Error: wrong client configuration detected."
|
||||
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
if ! ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH" || \
|
||||
[ ! -s "$RUN_DIR/sing-box-s.json.new" ]; then
|
||||
log "Error: failed to generate server configuration."
|
||||
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
|
||||
return 1
|
||||
elif ! "$PROG" check --config "$RUN_DIR/sing-box-s.json.new" 2>>"$LOG_PATH"; then
|
||||
log "Error: wrong server configuration detected."
|
||||
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if { [ "$outbound_node" = "nil" ] || mv -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-c.json"; } && \
|
||||
{ [ "$server_enabled" = "0" ] || mv -f "$RUN_DIR/sing-box-s.json.new" "$RUN_DIR/sing-box-s.json"; }; then
|
||||
[ "$outbound_node" != "nil" ] || rm -f "$RUN_DIR/sing-box-c.json"
|
||||
[ "$server_enabled" = "1" ] || rm -f "$RUN_DIR/sing-box-s.json"
|
||||
else
|
||||
log "Error: failed to install generated configuration."
|
||||
rm -f "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-s.json.new"
|
||||
return 1
|
||||
fi
|
||||
|
||||
case "$routing_mode" in
|
||||
"bypass_mainland_china"|"custom")
|
||||
mkdir -p "$CACHE_DIR"
|
||||
touch "$CACHE_PATH"
|
||||
chown -R sing-box:sing-box "$CACHE_DIR"
|
||||
;;
|
||||
esac
|
||||
|
||||
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" "0 */6 * * *"
|
||||
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null"
|
||||
if validate_cron "$auto_update_time"; then
|
||||
printf '%s %s #%s_autosetup\n' "$auto_update_time" "$HP_DIR/scripts/update_crond.sh" "$CONF" >> "/etc/crontabs/root"
|
||||
else
|
||||
log "Error: invalid subscription update schedule."
|
||||
fi
|
||||
/etc/init.d/cron restart
|
||||
fi
|
||||
|
||||
# DNSMasq rules
|
||||
local ipv6_support dns_port
|
||||
config_get_bool ipv6_support "config" "ipv6_support" "0"
|
||||
config_get dns_port "infra" "dns_port" "5333"
|
||||
mkdir -p "$DNSMASQ_DIR"
|
||||
echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf"
|
||||
case "$routing_mode" in
|
||||
"bypass_mainland_china")
|
||||
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
|
||||
no-poll
|
||||
no-resolv
|
||||
server=127.0.0.1#$dns_port
|
||||
EOF
|
||||
|
||||
local wan_interface wan_dns china_dns candidate
|
||||
. /lib/functions/network.sh
|
||||
if network_find_wan wan_interface && network_get_dnsserver wan_dns "$wan_interface"; then
|
||||
for candidate in $wan_dns; do
|
||||
case "$candidate" in
|
||||
*:*) ;;
|
||||
*) china_dns="$candidate"; break ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$china_dns" ] || china_dns="${wan_dns%% *}"
|
||||
fi
|
||||
china_dns="${china_dns:-223.5.5.5}"
|
||||
sed -r -e '/^\s*$/d' -e "s|(.*)|server=/\1/$china_dns|g" \
|
||||
"$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf"
|
||||
;;
|
||||
"custom"|"global")
|
||||
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
|
||||
no-poll
|
||||
no-resolv
|
||||
server=127.0.0.1#$dns_port
|
||||
EOF
|
||||
;;
|
||||
"gfwlist")
|
||||
local gfw_nftset_v6
|
||||
[ "$ipv6_support" -eq "0" ] || gfw_nftset_v6=",6#inet#fw4#homeproxy_gfw_list_v6"
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_gfw_list_v4$gfw_nftset_v6/g" \
|
||||
"$HP_DIR/resources/gfw_list.txt" > "$DNSMASQ_DIR/gfw_list.conf"
|
||||
;;
|
||||
"proxy_mainland_china")
|
||||
sed -r -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port/g" \
|
||||
"$HP_DIR/resources/china_list.txt" > "$DNSMASQ_DIR/china_list.conf"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ "$routing_mode" != "custom" ] && [ -s "$HP_DIR/resources/proxy_list.txt" ]; then
|
||||
local proxy_nftset_v6
|
||||
[ "$ipv6_support" -eq "0" ] || proxy_nftset_v6=",6#inet#fw4#homeproxy_wan_proxy_addr_v6"
|
||||
sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_proxy_addr_v4$proxy_nftset_v6/g" \
|
||||
"$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf"
|
||||
fi
|
||||
if [ "$routing_mode" != "custom" ] && [ -s "$HP_DIR/resources/direct_list.txt" ]; then
|
||||
local direct_nftset_v6
|
||||
[ "$ipv6_support" -eq "0" ] || direct_nftset_v6=",6#inet#fw4#homeproxy_wan_direct_addr_v6"
|
||||
sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_direct_addr_v4$direct_nftset_v6/g" \
|
||||
"$HP_DIR/resources/direct_list.txt" > "$DNSMASQ_DIR/direct_list.conf"
|
||||
fi
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
|
||||
# Setup routing table
|
||||
local table_mark
|
||||
config_get table_mark "infra" "table_mark" "100"
|
||||
case "$proxy_mode" in
|
||||
"redirect_tproxy")
|
||||
local outbound_udp_node
|
||||
config_get outbound_udp_node "config" "main_udp_node" "nil"
|
||||
if [ "$outbound_udp_node" != "nil" ] || [ "$routing_mode" = "custom" ]; 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
|
||||
;;
|
||||
"redirect_tun"|"tun")
|
||||
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"
|
||||
sleep 1s
|
||||
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"
|
||||
;;
|
||||
esac
|
||||
|
||||
# sing-box (client)
|
||||
procd_open_instance "sing-box-c"
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command run --config "$RUN_DIR/sing-box-c.json"
|
||||
|
||||
# QUIC-GO GSO is broken on kernel 6.6 currently
|
||||
uname -r | grep -Eq "^6\.6" && procd_set_param env "QUIC_GO_DISABLE_GSO"="true"
|
||||
|
||||
if [ -x "/sbin/ujail" ] && [ "$routing_mode" != "custom" ] && ! grep -Eq '"type": "(wireguard|tun)"' "$RUN_DIR/sing-box-c.json"; then
|
||||
procd_add_jail "sing-box-c" log procfs
|
||||
procd_add_jail_mount "$RUN_DIR/sing-box-c.json"
|
||||
procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log"
|
||||
[ "$routing_mode" != "bypass_mainland_china" ] || procd_add_jail_mount_rw "$CACHE_PATH"
|
||||
procd_add_jail_mount "$HP_DIR/certs/"
|
||||
procd_add_jail_mount "/etc/ssl/"
|
||||
procd_add_jail_mount "/etc/localtime"
|
||||
procd_add_jail_mount "/etc/TZ"
|
||||
procd_set_param capabilities "/etc/capabilities/homeproxy.json"
|
||||
procd_set_param no_new_privs 1
|
||||
procd_set_param user sing-box
|
||||
procd_set_param group sing-box
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
# sing-box (server)
|
||||
procd_open_instance "sing-box-s"
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command run --config "$RUN_DIR/sing-box-s.json"
|
||||
|
||||
# QUIC-GO GSO is broken on kernel 6.6 currently
|
||||
uname -r | grep -Eq "^6\.6" && procd_set_param env "QUIC_GO_DISABLE_GSO"="true"
|
||||
|
||||
if [ -x "/sbin/ujail" ]; then
|
||||
procd_add_jail "sing-box-s" log procfs
|
||||
procd_add_jail_mount "$RUN_DIR/sing-box-s.json"
|
||||
procd_add_jail_mount_rw "$RUN_DIR/sing-box-s.log"
|
||||
procd_add_jail_mount_rw "$HP_DIR/certs/"
|
||||
procd_add_jail_mount "/etc/acme/"
|
||||
procd_add_jail_mount "/etc/ssl/"
|
||||
procd_add_jail_mount "/etc/localtime"
|
||||
procd_add_jail_mount "/etc/TZ"
|
||||
procd_set_param capabilities "/etc/capabilities/homeproxy.json"
|
||||
procd_set_param no_new_privs 1
|
||||
procd_set_param user sing-box
|
||||
procd_set_param group sing-box
|
||||
fi
|
||||
|
||||
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
|
||||
fi
|
||||
|
||||
# log-cleaner
|
||||
procd_open_instance "log-cleaner"
|
||||
procd_set_param command "$HP_DIR/scripts/clean_log.sh"
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
|
||||
case "$routing_mode" in
|
||||
"custom")
|
||||
# Prepare ruleset directory
|
||||
[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ "$outbound_node" = "nil" ] || echo > "$RUN_DIR/sing-box-c.log"
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
echo > "$RUN_DIR/sing-box-s.log"
|
||||
mkdir -p "$HP_DIR/certs"
|
||||
fi
|
||||
|
||||
# Update permissions for ujail
|
||||
chown -R sing-box:sing-box "$RUN_DIR"
|
||||
|
||||
# Setup firewall
|
||||
ucode "$HP_DIR/scripts/firewall_pre.uc"
|
||||
[ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft"
|
||||
fw4 reload >"/dev/null" 2>&1
|
||||
|
||||
log "sing-box $(sing-box version -n) started."
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
sed -i "/#${CONF}_autosetup/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 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_redirect_proxy_port" "homeproxy_redirect_lanac" \
|
||||
"homeproxy_mangle_prerouting" "homeproxy_mangle_output" \
|
||||
"homeproxy_mangle_tproxy" "homeproxy_mangle_tproxy_port" \
|
||||
"homeproxy_mangle_tproxy_lanac" "homeproxy_mangle_mark" \
|
||||
"homeproxy_mangle_tun" "homeproxy_mangle_tun_mark"; do
|
||||
nft flush chain inet fw4 "$i"
|
||||
nft delete chain inet fw4 "$i"
|
||||
done 2>"/dev/null"
|
||||
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_wan_proxy_addr_v4" "homeproxy_wan_proxy_addr_v6" \
|
||||
"homeproxy_wan_direct_addr_v4" "homeproxy_wan_direct_addr_v6" \
|
||||
"homeproxy_routing_port"; do
|
||||
nft flush set inet fw4 "$i"
|
||||
nft delete set inet fw4 "$i"
|
||||
done 2>"/dev/null"
|
||||
echo 2>"/dev/null" > "$RUN_DIR/fw4_forward.nft"
|
||||
echo 2>"/dev/null" > "$RUN_DIR/fw4_input.nft"
|
||||
echo 2>"/dev/null" > "$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-c.json" "$RUN_DIR/sing-box-c.json.new" "$RUN_DIR/sing-box-c.log" \
|
||||
"$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.json.new" "$RUN_DIR/sing-box-s.log"
|
||||
|
||||
log "Service stopped."
|
||||
}
|
||||
|
||||
service_stopped() {
|
||||
# Load config
|
||||
config_load "$CONF"
|
||||
local tun_name
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
|
||||
# TUN
|
||||
ip link set "$tun_name" down 2>"/dev/null"
|
||||
ip tuntap del mode tun name "$tun_name" 2>"/dev/null"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
log "Reloading service..."
|
||||
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
procd_add_interface_trigger "interface.*.up" wan /etc/init.d/$CONF reload
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ -f "/www/luci-static/resources/icons/loading.gif" ] && \
|
||||
sed -i "s,/loading.svg,/loading.gif,g" "/www/luci-static/resources/view/homeproxy/status.js"
|
||||
|
||||
uci -q batch <<-EOF >"/dev/null"
|
||||
delete firewall.homeproxy_pre
|
||||
|
||||
delete firewall.homeproxy_forward
|
||||
set firewall.homeproxy_forward=include
|
||||
set firewall.homeproxy_forward.type=nftables
|
||||
set firewall.homeproxy_forward.path="/var/run/homeproxy/fw4_forward.nft"
|
||||
set firewall.homeproxy_forward.position="chain-pre"
|
||||
set firewall.homeproxy_forward.chain="forward"
|
||||
|
||||
delete firewall.homeproxy_input
|
||||
set firewall.homeproxy_input=include
|
||||
set firewall.homeproxy_input.type=nftables
|
||||
set firewall.homeproxy_input.path="/var/run/homeproxy/fw4_input.nft"
|
||||
set firewall.homeproxy_input.position="chain-pre"
|
||||
set firewall.homeproxy_input.chain="input"
|
||||
|
||||
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
|
||||
|
||||
exit 0
|
||||
@ -1,3 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec ucode "/etc/homeproxy/scripts/migrate_config.uc"
|
||||
@ -1,45 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
{
|
||||
"luci-app-homeproxy": {
|
||||
"description": "Grant access to homeproxy configuration",
|
||||
"read": {
|
||||
"file": {
|
||||
"/var/run/homeproxy/homeproxy.log": [ "read" ],
|
||||
"/var/run/homeproxy/sing-box-c.log": [ "read" ],
|
||||
"/var/run/homeproxy/sing-box-s.log": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ],
|
||||
"luci.homeproxy": [
|
||||
"acllist_read", "connection_check", "current_node_get",
|
||||
"node_latency_test", "resources_get",
|
||||
"singbox_generator", "singbox_get_features"
|
||||
]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ],
|
||||
"/tmp/homeproxy_certificate.tmp": [ "write" ]
|
||||
},
|
||||
"ubus": {
|
||||
"luci.homeproxy": [
|
||||
"acllist_write", "certificate_write", "log_clean", "resources_update"
|
||||
]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user