🎁 Sync 2026-07-31 00:21:49

This commit is contained in:
github-actions[bot] committed 2026-07-31 00:21:49 +08:00
1 parent f6c3fbbbbe
commit c71b32c9fb
204 files changed
+187145 -10038

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# Router files MUST use LF line endings. CRLF breaks nft (firewall ruleset is
# rejected wholesale), POSIX shell (init.d), and can confuse ucode parsing.
# Force LF on checkout regardless of the developer's core.autocrlf setting.
*.ut text eol=lf
*.uc text eol=lf
*.nft text eol=lf
*.sh text eol=lf
*.js text eol=lf
*.json text eol=lf
# Extensionless router scripts
root/etc/init.d/* text eol=lf
root/usr/share/rpcd/ucode/* text eol=lf
root/etc/uci-defaults/* text eol=lf
+340
View File
@@ -0,0 +1,340 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Library General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Library General
Public License instead of this License.
+58
View File
@@ -0,0 +1,58 @@
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2022-2023 ImmortalWrt.org
# Copyright (C) 2024-2026 1andrevich <1andrevich.recede274@passmail.net>
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Re:HomeProxy - multi-core proxy platform (fork of ImmortalWrt HomeProxy)
LUCI_PKGARCH:=all
LUCI_DEPENDS:= \
+firewall4 \
+ucode-mod-digest
PKG_NAME:=luci-app-re-homeproxy
PKG_VERSION:=1
PKG_RELEASE:=1
PKG_MAINTAINER:=1andrevich <1andrevich.recede274@passmail.net>
PKG_LICENSE:=GPL-2.0-only
PKG_LICENSE_FILES:=LICENSE
define Package/luci-app-re-homeproxy/conffiles
/etc/config/homeproxy
/etc/homeproxy/certs/
/etc/homeproxy/ruleset/
/etc/homeproxy/resources/direct_list.txt
/etc/homeproxy/resources/proxy_list.txt
endef
define Package/luci-app-re-homeproxy/postinst
#!/bin/sh
# Full restart, NOT kill -HUP: SIGHUP only reloads ACLs/session, so rpcd keeps the
# OLD ucode method set from /usr/share/rpcd/ucode/ until restarted. On every update
# that shipped a new/changed method (e.g. zapret_resolve_hosts) the method would be
# missing/stale until a manual `rpcd restart` — surfacing as "DNS not ready",
# yellow "?" in diagnostics, "Unknown method", etc. A restart reloads methods + ACLs.
# Detached + redirected and after a short delay so it (a) lets the install finish
# first, and (b) doesn't block when the upgrade is driven THROUGH rpcd (LuCI Software
# page): rpcd's popen() would otherwise wait on a child holding stdout open.
#
# MUST use setsid, not a bare `( … ) &`: a plain background subshell stays in the
# postinst's process group, so when opkg/apk tears the install down it reaps the
# still-sleeping child and the restart NEVER fires (methods stay stale → "DNS not
# ready" / "Method not found" until a manual `rpcd restart`). setsid puts the
# restarter in its own session, reparented to init, so it survives both the install
# teardown AND rpcd's own stop. Fall back to the plain form only where setsid is
# absent (rare; util-linux setsid ships on stock OpenWrt).
[ -n "$$IPKG_INSTROOT" ] && exit 0
if command -v setsid >/dev/null 2>&1; then
setsid sh -c 'sleep 3; /etc/init.d/rpcd restart' >/dev/null 2>&1 &
else
( sleep 3; /etc/init.d/rpcd restart ) >/dev/null 2>&1 &
fi
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+196
View File
@@ -0,0 +1,196 @@
[**Русский 🇷🇺**](README_ru.md) / [**English**](README.md)
<p align="center">
<a href="https://t.me/one_andrevich"><img src="https://img.shields.io/badge/Telegram-Join-2CA5E0?style=flat-square&logo=telegram&logoColor=white" alt="Telegram"></a>
<a href="https://ko-fi.com/D1D11SQNQD"><img src="https://img.shields.io/badge/Ko--fi-Support-FF5E5B?style=flat-square&logo=ko-fi&logoColor=white" alt="Ko-fi"></a>
<a href="https://nowpayments.io/donation?api_key=decbeb76-30f8-4c6d-ba40-2d2dec7fd888"><img src="https://img.shields.io/badge/Crypto-Donate-2EBE74?style=flat-square&logo=bitcoin&logoColor=white" alt="Crypto donate"></a>
</p>
# Re:HomeProxy
A modern multi-core proxy platform powered by [hiddify-core](https://github.com/hiddify/hiddify-core) and [sing-box-extended](https://github.com/shtorm-7/sing-box-extended).
A fork of [ImmortalWrt HomeProxy](https://github.com/immortalwrt/homeproxy).
## Overview
Re:HomeProxy is a feature-rich proxy management system, a fresh take on ImmortalWrt's HomeProxy. It runs on a choice of cores ([hiddify-core](https://github.com/hiddify/hiddify-core) or [sing-box-extended](https://github.com/shtorm-7/sing-box-extended)), adds a built-in DPI-bypass based on [Zapret2](https://github.com/bol-van/zapret2) and [ByeDPI](https://github.com/hufrea/byedpi) for un-throttling sites without a VPN, ready-made Russia routing rules, and a one-click core installer — all from the LuCI web interface.
## Key Features
- **Multi-core engine** — run on **hiddify-core** or **sing-box-extended**, your choice per device. The built-in **Core Management** page installs and updates the core for you and automatically picks the right build for your available storage (with a compact build for tight-storage devices).
- **Wide protocol support** — Naive, Mieru, Hysteria, SOCKS, Shadowsocks, ShadowTLS, Trojan, VLESS (XHTTP), VMess, WireGuard, **AmneziaWG / WARP** (sing-box-extended), SSH and more.
- **Two built-in DPI-bypass engines** — un-throttle and unblock sites (e.g. YouTube, Discord) **without any VPN subscription**:
- **ByeDPI** ([hufrea/byedpi](https://github.com/hufrea/byedpi)) — a SOCKS-level desync proxy, with 47 ready-made strategy presets and a multi-site **strategy tester** that shows which setting actually works on your ISP.
- **Zapret 2** ([bol-van/zapret2](https://github.com/bol-van/zapret2), nfqws2) — a packet-level NFQUEUE desync that mangles the handshake in-place. Selected per routing rule (e.g. send only YouTube/Discord through it), with curated presets, optional Discord-voice desync, and its own scoped tester.
- **URLTest auto-selection** — automatically routes through the fastest reachable node and fails over when one goes down.
- **Russia routing rules** — one-click RU Proxy Rules (Russia Inside, Re:Filter) with curated domain/IP lists, so only blocked destinations go through the proxy.
- **Subscription support** — import nodes from subscription links (sing-box JSON / Hiddify, base64 / plain share-links, and Xray/V2Ray JSON configs) and update them on demand.
- **Diagnostics** — a built-in page to check core/system health, inspect ports, and generate a shareable report.
- **Modern web interface** — clean, responsive LuCI UI with node management, ACL traffic routing, and NFT rule control.
## Prerequisites
- OpenWRT / ImmortalWrt 24.10 or higher (opkg)
- OpenWRT / ImmortalWrt 25.12 or higher (apk)
Optionally legacy build for 23.05 is available in Releases
## Installation
*~40 MB of free space recommended. Tight on storage? Install the LuCI app first, then use its **Core & Tools** tab (Services → Re:HomeProxy → Core & Tools) to install a core — it auto-picks a build that fits, including a compact build for small memory devices.*
### Quick install (one-liner)
Installs the LuCI app, then interactively walks you through the proxy core and optional ByeDPI / Zapret. Works on APK (25.12+), opkg (24.10) and 23.05 legacy. Run over SSH on the router:
```sh
wget -qO- https://raw.githubusercontent.com/1andrevich/homeproxy-hiddify/master/install.sh | sh
```
Behind a blocked/throttled GitHub, pass a mirror (note: the env var goes on `sh`, not `wget`):
```sh
wget -qO- https://raw.githubusercontent.com/1andrevich/homeproxy-hiddify/master/install.sh | GH_MIRROR=https://your.mirror sh
```
Prefer to do it by hand? Follow the per-version steps below.
### OpenWRT 25.12+ (APK)
#### 1. Install *luci-app-re-homeproxy* package
```sh
wget -O /tmp/homeproxy-hiddify.pub https://github.com/1andrevich/homeproxy-hiddify/releases/latest/download/homeproxy-hiddify.pub
cp /tmp/homeproxy-hiddify.pub /etc/apk/keys/
wget -O /tmp/luci-app-re-homeproxy.apk "$(wget -qO- 'https://api.github.com/repos/1andrevich/homeproxy-hiddify/releases' | grep -o 'https://github\.com/[^"]*luci-app-re-homeproxy[^"]*\.apk' | head -1)"
apk add /tmp/luci-app-re-homeproxy.apk
```
Once the key is in `/etc/apk/keys/` it is trusted permanently — no flag needed for future updates.
#### 2. Install components from the **Core & Tools** tab
Open **Services → Re:HomeProxy → Core & Tools** and install what you need — the installer auto-picks a build that fits your storage:
- **Proxy core** *(required, pick one)* — [hiddify-core](https://github.com/hiddify/hiddify-core) (default) or [sing-box-extended](https://github.com/shtorm-7/sing-box-extended) (adds AmneziaWG / WARP and the widest protocol set). See **[Core Management](../../wiki/Core-Management-en)**.
- **ByeDPI** *(optional)* — SOCKS-level DPI bypass that un-throttles sites without a VPN, with 40 presets and a built-in strategy tester. See **[ByeDPI](../../wiki/ByeDPI-en)**.
- **Zapret 2** *(optional)* — packet-level (nfqws2) DPI bypass selected per routing rule, with curated presets and optional Discord-voice desync. See **[Zapret](../../wiki/Zapret-en)**.
### OpenWRT 24.10 (opkg)
#### 1. Install *luci-app-re-homeproxy* package
```sh
wget -O /tmp/luci-app-re-homeproxy.ipk "$(wget -qO- 'https://api.github.com/repos/1andrevich/homeproxy-hiddify/releases' | grep -o 'https://github\.com/[^"]*luci-app-re-homeproxy[^"]*\.ipk' | head -1)"
opkg install /tmp/luci-app-re-homeproxy.ipk
```
#### 2. Install components from the **Core & Tools** tab
Open **Services → Re:HomeProxy → Core & Tools** and install what you need — the installer auto-picks a build that fits your storage:
- **Proxy core** *(required, pick one)* — [hiddify-core](https://github.com/hiddify/hiddify-core) (default) or [sing-box-extended](https://github.com/shtorm-7/sing-box-extended) (adds AmneziaWG / WARP and the widest protocol set). See **[Core Management](../../wiki/Core-Management-en)**.
- **ByeDPI** *(optional)* — SOCKS-level DPI bypass that un-throttles sites without a VPN, with 40 presets and a built-in strategy tester. See **[ByeDPI](../../wiki/ByeDPI-en)**.
- **Zapret 2** *(optional)* — packet-level (nfqws2) DPI bypass selected per routing rule, with curated presets and optional Discord-voice desync. See **[Zapret](../../wiki/Zapret-en)**.
### Manual install over SSH (alternative to the Core & Tools tab)
The **Core & Tools** tab is recommended — it auto-detects your hardware and picks a build that fits. To install the same components by hand over SSH, first detect your architecture and package format:
```sh
. /etc/openwrt_release; ARCH="$DISTRIB_ARCH"
command -v apk >/dev/null && EXT=apk || EXT=ipk
echo "$ARCH / $EXT"
```
The hiddify-core and ByeDPI packages are signed with the `homeproxy-hiddify.pub` key trusted during the app install above; if you skipped that, add `--allow-untrusted` to `apk add`.
**Proxy core — pick one** (plus its required kernel modules):
```sh
if [ "$EXT" = apk ]; then apk add kmod-nft-tproxy kmod-tun; else opkg install kmod-nft-tproxy kmod-tun; fi
# hiddify-core (default; for a compact build replace 'latest/download' with 'download/upx')
wget -O /tmp/hiddify-core.$EXT "https://github.com/1andrevich/hiddify-core/releases/latest/download/hiddify-core_${ARCH}.${EXT}"
if [ "$EXT" = apk ]; then apk add /tmp/hiddify-core.apk; else opkg install --force-reinstall /tmp/hiddify-core.ipk; fi
# ...OR sing-box-extended (AmneziaWG / WARP, widest protocol set; unsigned)
URL=$(wget -qO- 'https://api.github.com/repos/shtorm-7/sing-box-extended/releases/latest' | grep -o "https://github\.com/[^\"]*${ARCH}[^\"]*\.${EXT}" | head -1)
wget -O /tmp/sing-box-extended.$EXT "$URL"
if [ "$EXT" = apk ]; then apk add --allow-untrusted /tmp/sing-box-extended.apk; else opkg install /tmp/sing-box-extended.ipk; fi
```
**ByeDPI** *(optional)* — needs `curl` (its built-in strategy tester uses it):
```sh
if [ "$EXT" = apk ]; then apk add curl; else opkg install curl; fi
URL=$(wget -qO- 'https://api.github.com/repos/1andrevich/ByeDPI-OpenWrt/releases/latest' | grep -o "https://github\.com/[^\"]*byedpi_[^\"]*${ARCH}\.${EXT}" | head -1)
wget -O /tmp/byedpi.$EXT "$URL"
if [ "$EXT" = apk ]; then apk add /tmp/byedpi.apk; else opkg install /tmp/byedpi.ipk; fi
```
**Zapret 2** *(optional)* — needs the NFQUEUE kernel module:
```sh
if [ "$EXT" = apk ]; then apk add kmod-nft-queue; else opkg install kmod-nft-queue; fi
[ "$EXT" = apk ] && wget -O /etc/apk/keys/zapret2-1andrevich.pub "https://github.com/1andrevich/zapret2-openwrt/releases/latest/download/zapret2-1andrevich.pub"
wget -O /tmp/zapret2.$EXT "https://github.com/1andrevich/zapret2-openwrt/releases/latest/download/zapret2_${ARCH}.${EXT}"
if [ "$EXT" = apk ]; then apk add /tmp/zapret2.apk; else opkg install /tmp/zapret2.ipk; fi
```
### Optional
Installation of [Russian Language Pack](https://github.com/1andrevich/homeproxy-hiddify/blob/master/README_ru.md#3-%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D1%8F%D0%B7%D1%8B%D0%BA%D0%BE%D0%B2%D0%BE%D0%B3%D0%BE-%D0%BF%D0%B0%D0%BA%D0%B5%D1%82%D0%B0-ru-1)
If using "Custom JSON" — see the **[Custom JSON Config](../../wiki/Custom-JSON-Config-en)** wiki page for full details.
### 4. Start the service
```sh
/etc/init.d/homeproxy start
```
The service will auto-start on boot. Monitor logs at **Services → Re:HomeProxy → Core & Tools**.
## Documentation
Full guides live in the **[Wiki](../../wiki/Home)**:
- **[Getting Started](../../wiki/Getting-Started-en)** — from a fresh install to a working connection, step by step
- **[Core Management](../../wiki/Core-Management-en)** — hiddify-core vs sing-box-extended, the smart installer, storage and the compact build
- **[Supported Protocols](../../wiki/Supported-Protocols-en)** — every protocol, transport, and the build tags each needs
- **[Subscriptions & Node Import](../../wiki/Subscriptions-en)** — share links, .conf, Amnezia `vpn://` (AmneziaWG/Xray), subscriptions, base64
- **[Routing & Access Control](../../wiki/Routing-and-Access-Control-en)** — routing modes, RU Proxy Rules, per-device access control
- **[Server Settings](../../wiki/Server-Settings-en)** — run the router as a proxy server (inbounds, types, TLS/ACME)
- **[DNS & Diagnostics](../../wiki/DNS-and-Diagnostics-en)** — clean vs secure DNS, IPv6 leaks, and the Diagnostics page
- **[ByeDPI](../../wiki/ByeDPI-en)** — SOCKS-level DPI bypass, strategy presets and the tester
- **[Zapret](../../wiki/Zapret-en)** — packet-level (nfqws2) DPI bypass, presets, Discord-voice and the tester
- **[Custom Routing](../../wiki/Custom-Routing-en)** — UI routing nodes + rules (match by domain/IP/port/protocol/process)
- **[Custom JSON Config](../../wiki/Custom-JSON-Config-en)** — raw hiddify-core config routing mode
- **[Troubleshooting](../../wiki/Troubleshooting-en)** — common errors and fixes
## Credits & Acknowledgements
Re:HomeProxy stands on the work of many upstream projects. The LuCI app is GPL-licensed; the cores and bypass engines are fetched at install time from their own releases and remain under their own licenses.
**Base & cores**
- [ImmortalWrt HomeProxy](https://github.com/immortalwrt/homeproxy) — the original LuCI app this is a fork of
- [hiddify-core](https://github.com/hiddify/hiddify-core) — default proxy core (a sing-box fork by the Hiddify team)
- [sing-box-extended](https://github.com/shtorm-7/sing-box-extended) — alternative core with extra build tags (AmneziaWG/WARP, widest protocol set)
- [sing-box](https://sing-box.sagernet.org) — the upstream engine both cores derive from
**DPI-bypass engines**
- [hufrea/byedpi](https://github.com/hufrea/byedpi) — the ByeDPI (`ciadpi`) desync engine; OpenWrt packages by [1andrevich/ByeDPI-OpenWrt](https://github.com/1andrevich/ByeDPI-OpenWrt)
- [bol-van/zapret2](https://github.com/bol-van/zapret2) — the Zapret / nfqws2 / blockcheck2 packet desync engine; OpenWrt packages by [1andrevich/zapret2-openwrt](https://github.com/1andrevich/zapret2-openwrt); some strategy presets adapted from [flowseal/zapret-discord-youtube](https://github.com/flowseal/zapret-discord-youtube) (MIT)
**Protocols** — implemented by the cores above (see [Supported Protocols](../../wiki/Supported-Protocols-en)):
Naive, Mieru, Hysteria/Hysteria2, TUIC, SOCKS, Shadowsocks/Shadowsocks 2022, ShadowTLS, AnyTLS, Trojan, VLESS (Reality, XHTTP), VMess, WireGuard, AmneziaWG/WARP, SSH.
**Routing lists**
- [Re:Filter](https://github.com/1andrevich/re-filter) — RKN-registry domain + IP blocklist
- [itdoginfo/allow-domains](https://github.com/itdoginfo/allow-domains) — "Russia Inside" and the per-service routing lists (YouTube, Telegram, Discord, Meta, etc.)
- [itdoginfo](https://github.com/itdoginfo) — HODCA and other curated lists by itdoginfo
All trademarks and service names are the property of their respective owners and are referenced nominatively to identify the traffic each rule or list affects.
+210
View File
@@ -0,0 +1,210 @@
[**Русский 🇷🇺**](README_ru.md) / [**English**](README.md)
<p align="center">
<a href="https://t.me/one_andrevich"><img src="https://img.shields.io/badge/Telegram-Join-2CA5E0?style=flat-square&logo=telegram&logoColor=white" alt="Telegram"></a>
<a href="https://ko-fi.com/D1D11SQNQD"><img src="https://img.shields.io/badge/Ko--fi-Support-FF5E5B?style=flat-square&logo=ko-fi&logoColor=white" alt="Ko-fi"></a>
<a href="https://nowpayments.io/donation?api_key=decbeb76-30f8-4c6d-ba40-2d2dec7fd888"><img src="https://img.shields.io/badge/Crypto-Donate-2EBE74?style=flat-square&logo=bitcoin&logoColor=white" alt="Crypto donate"></a>
</p>
# Re:HomeProxy
Современная многоядерная прокси-платформа на основе [hiddify-core](https://github.com/hiddify/hiddify-core) и [sing-box-extended](https://github.com/shtorm-7/sing-box-extended).
Форк [ImmortalWrt HomeProxy](https://github.com/immortalwrt/homeproxy).
## Обзор
Re:HomeProxy — многофункциональная система управления прокси, новый взгляд на HomeProxy от ImmortalWrt. Работает на выбор ядра ([hiddify-core](https://github.com/hiddify/hiddify-core) или [sing-box-extended](https://github.com/shtorm-7/sing-box-extended)), включает встроенный обход DPI на основе [Zapret2](https://github.com/bol-van/zapret2) и [ByeDPI](https://github.com/hufrea/byedpi) для разблокировки сайтов без VPN, готовые правила маршрутизации для России и установщик ядра в один клик — всё из веб-интерфейса LuCI.
## Ключевые возможности
- **Многоядерный движок** — работа на **hiddify-core** или **sing-box-extended** на ваш выбор. Встроенная страница **Управление ядром** сама установит и обновит ядро и автоматически подберёт подходящую сборку под свободное место (включая компактную сборку для устройств с малым объёмом памяти).
- **Широкая поддержка протоколов** — Naive, Mieru, Hysteria, SOCKS, Shadowsocks, ShadowTLS, Trojan, VLESS (XHTTP), VMess, WireGuard, **AmneziaWG / WARP** (sing-box-extended), SSH и другие.
- **Два встроенных движка обхода DPI** — разблокировка сайтов и снятие троттлинга (например, YouTube, Discord) **без какой-либо VPN-подписки**:
- **ByeDPI** ([hufrea/byedpi](https://github.com/hufrea/byedpi)) — рассинхронизирующий прокси на уровне SOCKS, 47 готовых пресетов стратегий и **тестер стратегий** по нескольким сайтам, который показывает, какая настройка реально работает у вашего провайдера.
- **Zapret 2** ([bol-van/zapret2](https://github.com/bol-van/zapret2), nfqws2) — рассинхронизация на уровне пакетов через NFQUEUE, искажает рукопожатие «на лету». Назначается в правилах маршрутизации (например, отправить через него только YouTube/Discord), с подобранными пресетами, опциональным обходом для голоса Discord и собственным тестером.
- **Автовыбор через URLTest** — автоматически направляет трафик через самый быстрый доступный узел и переключается при сбое.
- **Правила маршрутизации для России** — RU Proxy Rules в один клик (Russia Inside, Re:Filter) с готовыми списками доменов/IP, чтобы через прокси шли только заблокированные адреса.
- **Поддержка подписок** — импорт узлов по ссылкам подписок (sing-box JSON / Hiddify, base64 / обычные share-ссылки, а также JSON-конфиги Xray/V2Ray) и обновление по требованию.
- **Диагностика** — встроенная страница для проверки состояния ядра/системы, просмотра портов и формирования отчёта.
- **Современный веб-интерфейс** — чистый адаптивный интерфейс LuCI с управлением узлами, маршрутизацией ACL и NFT-правилами.
## Требования
- OpenWRT / ImmortalWrt 24.10 или выше (opkg)
- OpenWRT / ImmortalWrt 25.12 или выше (apk)
Опционально в разделе Releases доступна legacy-сборка для 23.05
## Установка
*Рекомендуется ~40 Мб свободного места. Мало места? Сначала установите пакет LuCI, затем на вкладке **Ядро и службы** (Службы → Re:HomeProxy → Ядро и службы) установите ядро — оно само подберёт подходящую сборку для устройств с малым объёмом памяти.*
### Быстрая установка (одной строкой)
Ставит LuCI-приложение, затем интерактивно проводит по выбору ядра прокси и опциональных ByeDPI / Zapret. Работает на APK (25.12+), opkg (24.10) и 23.05 legacy. Выполните по SSH на роутере:
```sh
wget -qO- https://raw.githubusercontent.com/1andrevich/homeproxy-hiddify/master/install.sh | sh
```
При заблокированном/замедленном GitHub можно указать зеркало (важно: переменная ставится перед `sh`, а не перед `wget`):
```sh
wget -qO- https://raw.githubusercontent.com/1andrevich/homeproxy-hiddify/master/install.sh | GH_MIRROR=https://your.mirror sh
```
Хотите вручную? Шаги по версиям — ниже.
### OpenWRT 25.12+ (APK)
#### 1. Установка пакета Re:HomeProxy
```sh
wget -O /tmp/homeproxy-hiddify.pub https://github.com/1andrevich/homeproxy-hiddify/releases/latest/download/homeproxy-hiddify.pub
cp /tmp/homeproxy-hiddify.pub /etc/apk/keys/
wget -O /tmp/luci-app-re-homeproxy.apk "$(wget -qO- 'https://api.github.com/repos/1andrevich/homeproxy-hiddify/releases' | grep -o 'https://github\.com/[^"]*luci-app-re-homeproxy[^"]*\.apk' | head -1)"
apk add /tmp/luci-app-re-homeproxy.apk
```
После того как ключ окажется в `/etc/apk/keys/`, он будет доверенным постоянно — флаг указывать при последующих обновлениях не нужно.
#### 2. Установка компонентов на вкладке **Ядро и службы**
Откройте **Службы → Re:HomeProxy → Ядро и службы** и установите нужное — установщик сам подберёт сборку под свободное место:
- **Ядро прокси** *(обязательно, выберите одно)* — [hiddify-core](https://github.com/hiddify/hiddify-core) (по умолчанию) или [sing-box-extended](https://github.com/shtorm-7/sing-box-extended) (добавляет AmneziaWG / WARP и самый широкий набор протоколов). См. **[Управление ядром](../../wiki/Core-Management-ru)**.
- **ByeDPI** *(опционально)* — обход DPI на уровне SOCKS, снимает троттлинг без VPN, 47 пресетов и встроенный тестер стратегий. См. **[ByeDPI](../../wiki/ByeDPI-ru)**.
- **Zapret 2** *(опционально)* — обход DPI на уровне пакетов (nfqws2), назначается в правилах маршрутизации, с подобранными пресетами и опциональным обходом для голоса Discord. См. **[Zapret](../../wiki/Zapret-ru)**.
#### 3. Установка языкового пакета RU
```sh
wget -O /tmp/luci-i18n-homeproxy-ru.apk "$(wget -qO- 'https://api.github.com/repos/1andrevich/homeproxy-hiddify/releases' | grep -o 'https://github\.com/[^"]*luci-i18n-homeproxy-ru[^"]*\.apk' | head -1)"
apk add /tmp/luci-i18n-homeproxy-ru.apk
```
---
### OpenWRT 24.10 (opkg)
#### 1. Установка пакета Re:HomeProxy
```sh
wget -O /tmp/luci-app-re-homeproxy.ipk "$(wget -qO- 'https://api.github.com/repos/1andrevich/homeproxy-hiddify/releases' | grep -o 'https://github\.com/[^"]*luci-app-re-homeproxy[^"]*\.ipk' | head -1)"
opkg install /tmp/luci-app-re-homeproxy.ipk
```
#### 2. Установка компонентов на вкладке **Ядро и службы**
Откройте **Службы → Re:HomeProxy → Ядро и службы** и установите нужное — установщик сам подберёт сборку под свободное место:
- **Ядро прокси** *(обязательно, выберите одно)* — [hiddify-core](https://github.com/hiddify/hiddify-core) (по умолчанию) или [sing-box-extended](https://github.com/shtorm-7/sing-box-extended) (добавляет AmneziaWG / WARP и самый широкий набор протоколов). См. **[Управление ядром](../../wiki/Core-Management-ru)**.
- **ByeDPI** *(опционально)* — обход DPI на уровне SOCKS, снимает троттлинг без VPN, 47 пресетов и встроенный тестер стратегий. См. **[ByeDPI](../../wiki/ByeDPI-ru)**.
- **Zapret 2** *(опционально)* — обход DPI на уровне пакетов (nfqws2), назначается в правилах маршрутизации, с подобранными пресетами и опциональным обходом для голоса Discord. См. **[Zapret](../../wiki/Zapret-ru)**.
#### 3. Установка языкового пакета RU
```sh
wget -O /tmp/luci-i18n-homeproxy-ru.ipk "$(wget -qO- 'https://api.github.com/repos/1andrevich/homeproxy-hiddify/releases' | grep -o 'https://github\.com/[^"]*luci-i18n-homeproxy-ru[^"]*\.ipk' | head -1)"
opkg install /tmp/luci-i18n-homeproxy-ru.ipk
```
### Ручная установка по SSH (вместо вкладки «Ядро и службы»)
Вкладка **«Ядро и службы»** предпочтительна — она сама определяет железо и подбирает подходящую сборку. Чтобы поставить то же самое вручную по SSH, сначала определите архитектуру и формат пакетов:
```sh
. /etc/openwrt_release; ARCH="$DISTRIB_ARCH"
command -v apk >/dev/null && EXT=apk || EXT=ipk
echo "$ARCH / $EXT"
```
Пакеты hiddify-core и ByeDPI подписаны ключом `homeproxy-hiddify.pub`, который добавляется в доверенные при установке приложения выше; если вы пропустили тот шаг — добавьте `--allow-untrusted` к `apk add`.
**Ядро прокси — выберите одно** (плюс необходимые модули ядра):
```sh
if [ "$EXT" = apk ]; then apk add kmod-nft-tproxy kmod-tun; else opkg install kmod-nft-tproxy kmod-tun; fi
# hiddify-core (по умолчанию; для компактной сборки замените 'latest/download' на 'download/upx')
wget -O /tmp/hiddify-core.$EXT "https://github.com/1andrevich/hiddify-core/releases/latest/download/hiddify-core_${ARCH}.${EXT}"
if [ "$EXT" = apk ]; then apk add /tmp/hiddify-core.apk; else opkg install --force-reinstall /tmp/hiddify-core.ipk; fi
# ...или sing-box-extended (AmneziaWG / WARP, самый широкий набор протоколов; без подписи)
URL=$(wget -qO- 'https://api.github.com/repos/shtorm-7/sing-box-extended/releases/latest' | grep -o "https://github\.com/[^\"]*${ARCH}[^\"]*\.${EXT}" | head -1)
wget -O /tmp/sing-box-extended.$EXT "$URL"
if [ "$EXT" = apk ]; then apk add --allow-untrusted /tmp/sing-box-extended.apk; else opkg install /tmp/sing-box-extended.ipk; fi
```
**ByeDPI** *(опционально)* — нужен `curl` (его использует встроенный тестер стратегий):
```sh
if [ "$EXT" = apk ]; then apk add curl; else opkg install curl; fi
URL=$(wget -qO- 'https://api.github.com/repos/1andrevich/ByeDPI-OpenWrt/releases/latest' | grep -o "https://github\.com/[^\"]*byedpi_[^\"]*${ARCH}\.${EXT}" | head -1)
wget -O /tmp/byedpi.$EXT "$URL"
if [ "$EXT" = apk ]; then apk add /tmp/byedpi.apk; else opkg install /tmp/byedpi.ipk; fi
```
**Zapret 2** *(опционально)* — нужен модуль ядра NFQUEUE:
```sh
if [ "$EXT" = apk ]; then apk add kmod-nft-queue; else opkg install kmod-nft-queue; fi
[ "$EXT" = apk ] && wget -O /etc/apk/keys/zapret2-1andrevich.pub "https://github.com/1andrevich/zapret2-openwrt/releases/latest/download/zapret2-1andrevich.pub"
wget -O /tmp/zapret2.$EXT "https://github.com/1andrevich/zapret2-openwrt/releases/latest/download/zapret2_${ARCH}.${EXT}"
if [ "$EXT" = apk ]; then apk add /tmp/zapret2.apk; else opkg install /tmp/zapret2.ipk; fi
```
### Дополнительно. Режим «Пользовательский JSON»
Подробная документация доступна на странице вики **[Пользовательский JSON конфиг](../../wiki/Custom-JSON-Config-ru)**.
### 4. Запуск службы
```sh
/etc/init.d/homeproxy start
```
Служба запускается автоматически при загрузке системы. Логи доступны в разделе **Службы → Re:HomeProxy → Ядро и службы**.
## Документация
Полные руководства — в **[вики](../../wiki/Home)**:
- **[Первая настройка](../../wiki/Getting-Started-ru)** — от свежей установки до рабочего соединения, по шагам
- **[Управление ядром](../../wiki/Core-Management-ru)** — hiddify-core или sing-box-extended, умный установщик, память и компактная сборка
- **[Поддерживаемые протоколы](../../wiki/Supported-Protocols-ru)** — все протоколы, транспорты и нужные им build-теги
- **[Подписки и импорт узлов](../../wiki/Subscriptions-ru)** — share-ссылки, .conf, Amnezia `vpn://` (AmneziaWG/Xray), подписки, base64
- **[Маршрутизация и контроль доступа](../../wiki/Routing-and-Access-Control-ru)** — режимы маршрутизации, RU Proxy Rules, пер-девайс контроль доступа
- **[Настройки сервера](../../wiki/Server-Settings-ru)** — роутер как прокси-сервер (inbound-ы, типы, TLS/ACME)
- **[DNS и диагностика](../../wiki/DNS-and-Diagnostics-ru)** — чистый и защищённый DNS, утечки IPv6 и страница диагностики
- **[ByeDPI](../../wiki/ByeDPI-ru)** — обход DPI на уровне SOCKS, пресеты стратегий и тестер
- **[Zapret](../../wiki/Zapret-ru)** — обход DPI на уровне пакетов (nfqws2), пресеты, голос Discord и тестер
- **[Пользовательская маршрутизация](../../wiki/Custom-Routing-ru)** — узлы и правила маршрутизации в UI (матч по домену/IP/порту/протоколу/процессу)
- **[Пользовательский JSON конфиг](../../wiki/Custom-JSON-Config-ru)** — режим маршрутизации с «сырым» конфигом hiddify-core
- **[Устранение неполадок](../../wiki/Troubleshooting-ru)** — типичные ошибки и их решение
## Благодарности и используемые проекты
Re:HomeProxy опирается на работу множества вышестоящих проектов. Приложение LuCI распространяется под GPL; ядра и движки обхода DPI скачиваются при установке из их собственных релизов и остаются под своими лицензиями.
**Основа и ядра**
- [ImmortalWrt HomeProxy](https://github.com/immortalwrt/homeproxy) — исходное приложение LuCI, форком которого является проект
- [hiddify-core](https://github.com/hiddify/hiddify-core) — ядро прокси по умолчанию (форк sing-box от команды Hiddify)
- [sing-box-extended](https://github.com/shtorm-7/sing-box-extended) — альтернативное ядро с дополнительными build-тегами (AmneziaWG/WARP, самый широкий набор протоколов)
- [sing-box](https://sing-box.sagernet.org) — вышестоящий движок, на котором основаны оба ядра
**Движки обхода DPI**
- [hufrea/byedpi](https://github.com/hufrea/byedpi) — движок рассинхронизации ByeDPI (`ciadpi`); пакеты для OpenWrt — [1andrevich/ByeDPI-OpenWrt](https://github.com/1andrevich/ByeDPI-OpenWrt)
- [bol-van/zapret2](https://github.com/bol-van/zapret2) — движок рассинхронизации пакетов Zapret / nfqws2 / blockcheck2; пакеты для OpenWrt — [1andrevich/zapret2-openwrt](https://github.com/1andrevich/zapret2-openwrt); часть пресетов адаптирована из [flowseal/zapret-discord-youtube](https://github.com/flowseal/zapret-discord-youtube) (MIT)
**Протоколы** — реализованы ядрами выше (см. [Поддерживаемые протоколы](../../wiki/Supported-Protocols-ru)):
Naive, Mieru, Hysteria/Hysteria2, TUIC, SOCKS, Shadowsocks/Shadowsocks 2022, ShadowTLS, AnyTLS, Trojan, VLESS (Reality, XHTTP), VMess, WireGuard, AmneziaWG/WARP, SSH.
**Списки маршрутизации**
- [Re:Filter](https://github.com/1andrevich/re-filter) — список доменов + IP по реестру РКН
- [itdoginfo/allow-domains](https://github.com/itdoginfo/allow-domains) — «Russia Inside» и сервисные списки (YouTube, Telegram, Discord, Meta и др.)
- [itdoginfo](https://github.com/itdoginfo) — HODCA и другие подобранные списки от itdoginfo
Все товарные знаки и названия сервисов принадлежат их владельцам и упоминаются номинативно — для обозначения трафика, к которому относится правило или список.
@@ -0,0 +1,348 @@
/*
* 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(), {}).then((features) => {
/* If RPC failed or returned no core_type, fall back to core.info file */
if (features.core_type)
return features;
return L.resolveDefault(fs.read_direct('/var/run/homeproxy/core.info', 'text'), null)
.then((text) => {
if (text) {
try {
const info = JSON.parse(text);
features.core_type = info.core_type || features.core_type;
features.version = info.version || features.version;
} catch(e) {}
}
return features;
});
});
},
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 in ['urltest', 'main-out', 'direct-out', 'block-out'])
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
@@ -0,0 +1,571 @@
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2023-2025 ImmortalWrt.org
*/
'use strict';
'require dom';
'require homeproxy as hp';
'require poll';
'require rpc';
'require uci';
'require ui';
'require view';
const css = '\
.diag-section { margin-bottom: 1.5em; }\
.diag-row { display:flex; align-items:baseline; margin:.35em 0; gap:.6em; }\
.diag-label { min-width:11em; color:#666; font-size:.9em; flex-shrink:0; }\
.diag-ok { color: #2a2; font-weight: bold; }\
.diag-fail { color: #c33; font-weight: bold; }\
.diag-warn { color: #c80; font-weight: bold; }\
.diag-gray { color: #888; }\
.diag-pre { font-family:monospace; font-size:.82em; background:#f5f5f5; border:1px solid #ddd;\
padding:.5em .8em; white-space:pre-wrap; word-break:break-all; border-radius:3px;\
margin:.3em 0 0; max-height:14em; overflow-y:auto; }\
.diag-input { padding:.25em .4em; border:1px solid #ccc; border-radius:3px;\
font-size:.9em; width:14em; }\
.diag-btn { margin-right:.4em; }\
.diag-port-line { font-family:monospace; font-size:.82em; margin:.1em 0; }\
#diag-report-area { width:100%; height:22em; font-family:monospace; font-size:.8em;\
white-space:pre; overflow:auto; background:#f5f5f5; border:1px solid #ddd; padding:.5em; }\
';
/* ── RPC declarations ─────────────────────────────────────────────────── */
const callCoreCheck = rpc.declare({
object: 'luci.homeproxy',
method: 'diag_core_check',
expect: { '': {} }
});
const callConfigCheck = rpc.declare({
object: 'luci.homeproxy',
method: 'diag_config_check',
expect: { '': {} }
});
const callDnsRu = rpc.declare({
object: 'luci.homeproxy',
method: 'diag_dns_ru',
expect: { '': {} }
});
const callNftables = rpc.declare({
object: 'luci.homeproxy',
method: 'diag_nftables',
expect: { '': {} }
});
const callReport = rpc.declare({
object: 'luci.homeproxy',
method: 'diag_report',
expect: { '': {} }
});
const callServiceRestart = rpc.declare({
object: 'luci.homeproxy',
method: 'diag_service_restart',
expect: { '': {} }
});
const callConnCheck = rpc.declare({
object: 'luci.homeproxy',
method: 'connection_check',
params: ['site'],
expect: { '': {} }
});
const callIpInfo = rpc.declare({
object: 'luci.homeproxy',
method: 'clash_ip_info',
expect: { '': {} }
});
const callActiveNode = rpc.declare({
object: 'luci.homeproxy',
method: 'clash_active_node',
params: ['tag'],
expect: { '': {} }
});
/* Resolve a sing-box outbound tag (cfg-<section>-out) to its UCI label. */
function resolveTag(tag) {
const m = tag && tag.match(/^cfg-(.+)-out$/);
if (m) {
const label = uci.get('homeproxy', m[1], 'label');
if (label) return label;
}
/* Fixed aggregate tags → name the configured node behind them. */
const specials = { 'main-out': 'main_node', 'main-udp-out': 'main_udp_node' };
if (specials[tag]) {
const v = uci.get('homeproxy', 'config', specials[tag]);
if (v === 'byedpi-out') return 'ByeDPI';
if (v === 'zapret-out') return 'Zapret';
if (v && !['urltest', 'same', 'nil', ''].includes(v)) {
const label = uci.get('homeproxy', v, 'label');
if (label) return label;
}
}
return tag;
}
/* ── DOM helpers ──────────────────────────────────────────────────────── */
function statusBadge(ok, text) {
/* ucode serialises booleans as integers 0/1 — use loose checks so both
JS booleans and integers work. null/undefined → "uncertain" (orange ?). */
const isOk = !!ok;
const isWarn = ok == null; /* catches null AND undefined */
const icon = isOk ? '✓ ' : isWarn ? '? ' : '✗ ';
const cls = isOk ? 'diag-ok' : isWarn ? 'diag-warn' : 'diag-fail';
const label = text || (isOk ? _('OK') : isWarn ? '?' : _('FAIL'));
return E('strong', { 'class': cls }, icon + label);
}
function row(label, value) {
return E('div', { 'class': 'diag-row' }, [
E('span', { 'class': 'diag-label' }, label),
E('span', {}, value)
]);
}
function pre(text) {
return E('div', { 'class': 'diag-pre' }, text || _('(empty)'));
}
function sectionCard(title, id, rows) {
return E('div', { 'class': 'cbi-section diag-section' }, [
E('h3', {}, title),
E('div', { 'id': id }, rows)
]);
}
function spinner(el, label) {
dom.content(el, E('em', { 'class': 'diag-gray' }, label || _('Testing…')));
}
/* ── Section builders ─────────────────────────────────────────────────── */
function buildConnectivitySection(view, coreType) {
const items = []; /* manual checks; the section's run() fires them together */
function siteRow(label, site) {
const resultEl = E('strong', { 'class': 'diag-gray' }, _('unchecked'));
function run() {
dom.content(resultEl, E('em', { 'class': 'diag-gray' }, _('Testing…')));
return L.resolveDefault(callConnCheck(site), {}).then(function(ret) {
dom.content(resultEl, statusBadge(!!ret.result, ret.result ? _('passed') : _('failed')));
});
}
items.push(run);
const btn = E('button', { 'class': 'btn cbi-button cbi-button-action diag-btn',
'click': ui.createHandlerFn(view, run) }, [ _('Check') ]);
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, label), btn, resultEl ]);
}
/* Exit IP / geo — only hiddify-core's Clash API reports it. */
function ipRow(label, type) {
const resultEl = E('strong', { 'class': 'diag-gray' }, _('unchecked'));
function run() {
dom.content(resultEl, E('em', { 'class': 'diag-gray' }, _('Testing…')));
return L.resolveDefault(callIpInfo(), {}).then(function(ret) {
if (ret.error) { dom.content(resultEl, E('span', { 'class': 'diag-fail' }, ret.error)); return; }
const entry = ret[type];
if (!entry || !entry.ip) { dom.content(resultEl, E('span', { 'class': 'diag-gray' }, _('No data'))); return; }
const meta = [entry.country, entry.org].filter(Boolean).join(', ');
const delay = (entry.delay && entry.delay !== 65535) ? ' — ' + entry.delay + ' ms' : '';
const node = (type === 'proxy' && entry.node) ? resolveTag(entry.node) + ': ' : '';
dom.content(resultEl, E('strong', { 'class': 'diag-ok' },
node + entry.ip + (meta ? ' (' + meta + ')' : '') + delay));
});
}
items.push(run);
const btn = E('button', { 'class': 'btn cbi-button cbi-button-action diag-btn',
'click': ui.createHandlerFn(view, run) }, [ _('Check') ]);
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, label), btn, resultEl ]);
}
/* Active node — live status (which node sing-box has selected), auto-updating;
* sing-box can't report exit IP, so this stands in for the IP rows. */
/* Live active-node row. `tag` selects which outbound group to resolve:
undefined → the main (TCP) node via GLOBAL; 'main-udp-out' → the dedicated UDP node. */
function activeNodeRow(labelText, tag) {
const resultEl = E('span', { 'class': 'diag-gray' }, '—');
poll.add(L.bind(function() {
return L.resolveDefault(callActiveNode(tag), {}).then(function(ret) {
if (ret && !ret.error && ret.node) {
const type = ret.type ? ' (' + ret.type + ')' : '';
/* Same 4-colour scheme as the status/client pages: 65535 ms is the
URLTest timeout sentinel (confirmed dead → red); >=3000 ms is
working-but-slow (orange); a real low latency is green; no delay
is unmeasured (gray, no number). */
let cls, dStr = '';
if (ret.delay === 65535) { cls = 'diag-fail'; dStr = ' — ' + _('timeout'); }
else if (ret.delay >= 3000) { cls = 'diag-warn'; dStr = ' — ' + ret.delay + ' ms'; }
else if (ret.delay) { cls = 'diag-ok'; dStr = ' — ' + ret.delay + ' ms'; }
else cls = 'diag-gray';
dom.content(resultEl, E('strong', { 'class': cls }, resolveTag(ret.node) + type + dStr));
} else {
dom.content(resultEl, E('span', { 'class': 'diag-gray' }, _('No active node')));
}
});
}));
return E('div', { 'class': 'diag-row' }, [ E('span', { 'class': 'diag-label' }, labelText), resultEl ]);
}
const rows = [
siteRow(_('Baidu'), 'baidu'),
siteRow(_('Google'), 'google'),
siteRow(_('YouTube'), 'youtube'),
siteRow(_('Yandex'), 'yandex'),
siteRow(_('Speedtest'), 'speedtest')
];
/* hiddify → exit-IP rows; sing-box → live Active Node (it can't report exit IP). */
if (coreType === 'hiddify') {
rows.push(ipRow(_('Direct IP'), 'direct'));
rows.push(ipRow(_('Proxy IP'), 'proxy'));
} else if (coreType === 'singbox') {
rows.push(activeNodeRow(_('Active Node')));
}
/* Dedicated UDP node — only the modes that expose "Main UDP node" in the UI set it
(Global + the bypass_cn/bypass_ir reverse modes; proxy_banned_ru/custom don't, and
'redirect' proxy mode has no UDP path). Show which node currently carries UDP. */
const udpNode = uci.get('homeproxy', 'config', 'main_udp_node');
const proxyMode = uci.get('homeproxy', 'config', 'proxy_mode');
if (udpNode && !['nil', 'same', ''].includes(udpNode) && proxyMode !== 'redirect')
rows.push(activeNodeRow(_('Active UDP Node'), 'main-udp-out'));
return {
el: sectionCard(_('Connectivity'), 'diag-connectivity', rows),
run: function() { return Promise.all(items.map(function(fn) { return fn(); })); }
};
}
function buildCoreSection(view) {
const resultsEl = E('div', {});
const restartMsgEl = E('span', { 'class': 'diag-gray', 'style': 'font-size:.9em' });
function run() {
spinner(resultsEl, _('Checking core…'));
return L.resolveDefault(callCoreCheck(), {}).then(function(ret) {
if (!ret || ret.error) {
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret ? ret.error : _('RPC error')));
return;
}
const portLines = (ret.listen_ports && ret.listen_ports.length)
? ret.listen_ports.map(function(l) { return E('div', { 'class': 'diag-port-line' }, l); })
: [ E('em', { 'class': 'diag-gray' }, _('none detected')) ];
dom.content(resultsEl, [
row(_('Hiddify-core'), statusBadge(ret.hiddify_installed, ret.hiddify_installed ? _('installed') : _('not found'))),
row(_('sing-box'), statusBadge(ret.singbox_installed, ret.singbox_installed ? _('installed') : _('not found'))),
row(_('ByeDPI'), statusBadge(ret.byedpi_installed, ret.byedpi_installed ? _('installed') : _('not found'))),
row(_('Zapret 2'), statusBadge(ret.zapret_installed, ret.zapret_installed ? _('installed') : _('not found'))),
ret.binary ? row(_('Active binary'), E('code', {}, ret.binary)) : null,
ret.version ? row(_('Version'), E('span', { 'class': 'diag-pre', 'style': 'max-height:5em' }, ret.version)) : null,
row(_('Running'), statusBadge(ret.running, ret.running ? _('yes') + (ret.pid ? ' (PID ' + ret.pid + ')' : '') : _('no'))),
ret.byedpi_installed ? row(_('ByeDPI running'), statusBadge(ret.byedpi_running, ret.byedpi_running ? _('yes') + (ret.byedpi_pid ? ' (PID ' + ret.byedpi_pid + ')' : '') : _('no'))) : null,
ret.zapret_installed ? row(_('Zapret 2 running'), statusBadge(ret.zapret_running, ret.zapret_running ? _('yes') + (ret.zapret_pid ? ' (PID ' + ret.zapret_pid + ')' : '') : _('no'))) : null,
row(_('Listening ports'), E('div', {}, portLines))
].filter(Boolean));
});
}
function restart() {
dom.content(restartMsgEl, E('em', { 'class': 'diag-gray' }, _('Restarting… (may take ~5 s)')));
return L.resolveDefault(callServiceRestart(), {}).then(function(ret) {
if (ret && ret.result) {
dom.content(restartMsgEl, statusBadge(true, _('Service restarted')));
/* Refresh core status after restart */
return run();
} else {
dom.content(restartMsgEl, statusBadge(false,
_('Restart failed') + (ret && ret.exit_code != null ? ' (exit ' + ret.exit_code + ')' : '')));
}
});
}
return {
el: sectionCard(_('Core & System'), 'diag-core', [
E('div', { 'class': 'diag-row' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action diag-btn',
'click': ui.createHandlerFn(view, run)
}, _('Check')),
E('button', {
'class': 'btn cbi-button cbi-button-negative diag-btn',
'click': ui.createHandlerFn(view, restart)
}, _('Restart Service')),
restartMsgEl
]),
resultsEl
]),
run: run
};
}
function buildConfigSection(view) {
const resultsEl = E('div', {});
function run() {
spinner(resultsEl, _('Checking config…'));
return L.resolveDefault(callConfigCheck(), {}).then(function(ret) {
if (!ret || ret.error) {
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret ? ret.error : _('RPC error')));
return;
}
dom.content(resultsEl, [
row(_('Valid'), statusBadge(ret.valid)),
ret.size_bytes != null ? row(_('Size'), (ret.size_bytes / 1024).toFixed(1) + ' KB') : null,
ret.stats ? row(_('Inbounds'), String(ret.stats.inbounds)) : null,
ret.stats ? row(_('Outbounds'), String(ret.stats.outbounds)) : null,
ret.stats ? row(_('Rules'), String(ret.stats.rules)) : null,
ret.stats ? row(_('DNS servers'), String(ret.stats.dns_servers)) : null,
ret.check_output ? E('div', {}, [
E('div', { 'class': 'diag-label' }, _('Check output')),
pre(ret.check_output)
]) : null
].filter(Boolean));
});
}
return {
el: sectionCard(_('Configuration'), 'diag-config', [
E('div', { 'class': 'diag-row' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action diag-btn',
'click': ui.createHandlerFn(view, run)
}, _('Check'))
]),
resultsEl
]),
run: run
};
}
function buildDnsSection(view) {
const resultsEl = E('div', {});
const cardEl = sectionCard(_('DNS Tests'), 'diag-dns', [
E('div', { 'class': 'diag-row' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action diag-btn',
'click': ui.createHandlerFn(view, run)
}, _('Test'))
]),
resultsEl
]);
cardEl.style.display = 'none';
function run() {
spinner(resultsEl, _('Testing DNS…'));
return L.resolveDefault(callDnsRu(), {}).then(function(ret) {
if (ret.skip) {
cardEl.style.display = 'none';
return;
}
cardEl.style.display = '';
if (ret.error) {
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret.error));
return;
}
const regionDomain = ret.region_domain || '?';
dom.content(resultsEl, [
E('div', { 'style': 'font-weight:bold; margin:.4em 0 .2em' },
(ret.region_label ? ret.region_label + ' ' : '') + _('DNS (direct)')),
row(_('Server'), E('code', {}, ret.region_server || '?')),
row(regionDomain, statusBadge(ret.region_ok,
ret.region_ok ? _('Resolved') : _('No answer — check server address'))),
ret.region_output ? E('div', {}, [
E('div', { 'class': 'diag-label' }, 'nslookup ' + regionDomain),
pre(ret.region_output)
]) : null,
E('div', { 'style': 'font-weight:bold; margin:.8em 0 .2em' }, _('Secure DNS')),
ret.secure_server ? row(_('Server'), E('code', {}, ret.secure_server)) : null,
row(_('Bootstrap'), E('code', {}, ret.bootstrap || '?')),
row('andrevi.ch', statusBadge(ret.secure_ok,
ret.secure_ok ? _('Resolved via proxy') : _('No answer — check proxy and proxy list'))),
ret.secure_output ? E('div', {}, [
E('div', { 'class': 'diag-label' }, 'nslookup andrevi.ch'),
pre(ret.secure_output)
]) : null
].filter(Boolean));
});
}
return { el: cardEl, run: run };
}
function buildNftSection(view) {
const resultsEl = E('div', {});
function run() {
spinner(resultsEl, _('Reading nftables…'));
return L.resolveDefault(callNftables(), {}).then(function(ret) {
if (ret.error) {
dom.content(resultsEl, E('span', { 'class': 'diag-fail' }, ret.error));
return;
}
dom.content(resultsEl, [
row(_('Re:HomeProxy chains'), statusBadge(ret.nft_present,
ret.nft_present ? _('found') : _('not found — firewall rules may be missing'))),
ret.nft_rules ? E('div', {}, [
E('div', { 'class': 'diag-label' }, _('Matched lines in fw4')),
pre(ret.nft_rules)
]) : null,
ret.udp_tproxy ? E('div', {}, [
E('div', { 'class': 'diag-label' }, _('UDP TPROXY counters (UDP into HomeProxy)')),
pre(ret.udp_tproxy)
]) : null,
ret.uci_firewall ? E('div', {}, [
E('div', { 'class': 'diag-label' }, _('UCI firewall settings')),
pre(ret.uci_firewall)
]) : null,
ret.zapret_enabled ? row(_('Zapret (nfqws2)'), statusBadge(ret.zapret_running,
ret.zapret_running ? _('running') : _('enabled, but nfqws2 not running'))) : null,
(ret.zapret_enabled && ret.zapret_queue) ? E('div', {}, [
E('div', { 'class': 'diag-label' }, _('Zapret NFQUEUE counters (mark 110 → queue)')),
pre(ret.zapret_queue)
]) : null
].filter(Boolean));
});
}
return {
el: sectionCard(_('Network Intercept'), 'diag-nft', [
E('div', { 'class': 'diag-row' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action diag-btn',
'click': ui.createHandlerFn(view, run)
}, _('Check'))
]),
resultsEl
]),
run: run
};
}
function buildReportSection(view) {
const textArea = E('textarea', {
'id': 'diag-report-area',
'readonly': true,
'placeholder': _('Click Generate to collect diagnostics…')
});
function run() {
textArea.value = _('Collecting diagnostics — this may take a few seconds…');
return L.resolveDefault(callReport(), {}).then(function(ret) {
textArea.value = ret.report || _('(empty)');
});
}
function copy() {
textArea.select();
document.execCommand('copy');
}
function download() {
const text = textArea.value;
if (!text || text === _('Click Generate to collect diagnostics…')) return;
const now = new Date();
const pad = (n) => String(n).padStart(2, '0');
const stamp = now.getFullYear() + '-' + pad(now.getMonth()+1) + '-' + pad(now.getDate()) +
'_' + pad(now.getHours()) + '-' + pad(now.getMinutes()) + '-' + pad(now.getSeconds());
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = E('a', { href: url, download: 'homeproxy-diagnostics-' + stamp + '.txt' });
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
return {
el: sectionCard(_('Diagnostics Report'), 'diag-report', [
E('div', { 'class': 'diag-row' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action diag-btn',
'click': ui.createHandlerFn(view, run)
}, _('Generate')),
E('button', {
'class': 'btn cbi-button diag-btn',
'click': ui.createHandlerFn(view, copy)
}, _('Copy')),
E('button', {
'class': 'btn cbi-button diag-btn',
'click': ui.createHandlerFn(view, download)
}, _('Download')),
E('em', { 'class': 'diag-gray', 'style': 'font-size:.85em' },
_('Sensitive values (passwords, keys, UUIDs) are redacted.'))
]),
textArea
])
};
}
/* ── View ─────────────────────────────────────────────────────────────── */
return view.extend({
load: function() {
return Promise.all([
L.resolveDefault(hp.getBuiltinFeatures(), {}),
uci.load('homeproxy')
]);
},
render: function(data) {
const features = (data && data[0]) || {};
document.head.appendChild(E('style', {}, [ css ]));
const core = buildCoreSection(this);
const config = buildConfigSection(this);
const dns = buildDnsSection(this);
const nft = buildNftSection(this);
const conn = buildConnectivitySection(this, features.core_type || null);
const report = buildReportSection(this);
dns.run();
const runAll = ui.createHandlerFn(this, function() {
return Promise.all([
core.run(),
config.run(),
nft.run(),
conn.run()
]);
});
return E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('Re:HomeProxy Diagnostics')),
E('div', { 'class': 'cbi-section', 'style': 'padding-bottom:.5em' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action important',
'click': runAll
}, _('Run All Tests')),
E('em', { 'class': 'diag-gray', 'style': 'margin-left:.8em; font-size:.9em' },
_('Runs Core, Config, Network, and Connectivity checks simultaneously.'))
]),
core.el,
config.el,
dns.el,
nft.el,
conn.el,
report.el
]);
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,936 @@
/*
* 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']['hiddify-c']['running'];
} catch (e) { }
return isRunning;
});
}
function renderStatus(isRunning, features) {
let coreName = features.core_type === 'singbox' ? 'sing-box' :
features.core_type === 'hiddify' ? 'hiddify-core' : null;
let verStr = features.version ? 'v' + features.version : _('unknown');
let coreStr = coreName ? ('%s %s').format(coreName, verStr) : _('no core installed');
let spanTemp = '<em><span style="color:%s"><strong>%s (%s) %s</strong></span></em>';
let renderHTML;
if (isRunning)
renderHTML = spanTemp.format('green', _('Re:HomeProxy Server'), coreStr, _('RUNNING'));
else
renderHTML = spanTemp.format('red', _('Re:HomeProxy Server'), coreStr, _('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', _('Re:HomeProxy Server'),
_('The modern multi-core proxy platform. Fork of ImmortalWrt.'));
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);
});
});
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'));
if (features.core_type === 'singbox')
o.value('mtproxy', _('MTProxy'));
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;
/* MTProxy config start */
o = s.option(form.DynamicList, 'mtproxy_secrets', _('Secrets'),
_('List of MTProxy secrets. Each secret is a 32-character hex string (16 bytes). One secret per user.'));
o.depends('type', 'mtproxy');
o.validate = function(section_id, value) {
if (section_id && value && !/^[0-9a-fA-F]{32}$/.test(value))
return _('Expecting: %s').format(_('32 hex characters'));
return true;
};
o.rmempty = false;
o.modalonly = true;
o = s.option(form.Value, 'domain_fronting_host', _('Domain fronting host'),
_('Target hostname for FakeTLS domain fronting. Clients will see TLS to this domain.'));
o.datatype = 'hostname';
o.depends('type', 'mtproxy');
o.modalonly = true;
o = s.option(form.Value, 'domain_fronting_port', _('Domain fronting port'));
o.datatype = 'port';
o.default = '443';
o.depends({'type': 'mtproxy', 'domain_fronting_host': /[\s\S]/});
o.modalonly = true;
o = s.option(form.Value, 'mtproxy_idle_timeout', _('Idle timeout'),
_('Close idle connections after this duration (e.g. 5m, 30s).'));
o.placeholder = '5m';
o.depends('type', 'mtproxy');
o.modalonly = true;
o = s.option(form.Value, 'mtproxy_handshake_timeout', _('Handshake timeout'),
_('Maximum time to complete handshake (e.g. 10s).'));
o.placeholder = '10s';
o.depends('type', 'mtproxy');
o.modalonly = true;
o = s.option(form.Value, 'mtproxy_concurrency', _('Concurrency'),
_('Worker pool size. Leave empty for default.'));
o.datatype = 'uinteger';
o.depends('type', 'mtproxy');
o.modalonly = true;
/* MTProxy config end */
/* 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'),
_('HTTP3 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', _('DNS01 challenge'))
o.depends('tls_acme', '1');
o.modalonly = true;
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
o.value('alidns', _('Alibaba Cloud DNS'));
o.value('cloudflare', _('Cloudflare'));
o.depends('tls_dns01_challenge', '1');
o.default = 'cloudflare';
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', 'cloudflare');
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();
}
});
@@ -0,0 +1,926 @@
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2022-2025 ImmortalWrt.org
*/
'use strict';
'require dom';
'require form';
'require fs';
'require homeproxy as hp';
'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';
/* Map a sing-box outbound tag to its human-readable UCI label */
function resolveTag(tag) {
const m = tag && tag.match(/^cfg-(.+)-out$/);
if (m) {
const label = uci.get('homeproxy', m[1], 'label');
if (label) return label;
}
/* Fixed tags: look up the backing UCI section via the 'main' config section */
const specials = { 'main-out': 'main_node', 'main-udp-out': 'main_udp_node' };
if (specials[tag]) {
const section = uci.get('homeproxy', 'main', specials[tag]);
if (section) {
const label = uci.get('homeproxy', section, 'label');
if (label) return label;
}
}
return tag;
}
function getIPInfo(o, type) {
const callIPInfo = rpc.declare({
object: 'luci.homeproxy',
method: 'clash_ip_info',
expect: { '': {} }
});
const resultEl = E('strong', { 'style': 'color:gray' }, _('unchecked'));
const formatIPInfo = (entry, nodeTag) => {
if (!entry)
return E('strong', { 'style': 'color:red' }, _('No data'));
const lines = [];
if (nodeTag)
lines.push(E('span', {}, [ _('Node') + ': ', E('strong', {}, resolveTag(nodeTag)) ]));
if (entry.ip) {
const delayStr = (entry.delay && entry.delay !== 65535) ? ` — ${entry.delay} ms` : '';
const meta = [entry.country, entry.org].filter(Boolean).join(', ');
const label = entry.ip + (meta ? ` (${meta})` : '') + delayStr;
lines.push(E('span', {}, [ 'IP: ', E('strong', { 'style': 'color:green' }, label) ]));
}
return E('span', {}, lines.map((l) => E('div', {}, [ l ])));
};
o.default = E('div', { 'style': 'cbi-value-field' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, () => {
return L.resolveDefault(callIPInfo(), {}).then((ret) => {
const el = o.default.firstElementChild.nextElementSibling;
if (ret.error) {
dom.content(el, E('span', { 'style': 'color:red' }, ret.error));
return;
}
const entry = ret[type];
dom.content(el, formatIPInfo(entry, type === 'proxy' ? entry?.node : null));
});
})
}, [ _('Check') ]),
' ',
resultEl
]);
}
function getConnStat(o, site) {
const callConnStat = rpc.declare({
object: 'luci.homeproxy',
method: 'connection_check',
params: ['site'],
expect: { '': {} }
});
o.default = E('div', { 'style': 'cbi-value-field' }, [
E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': ui.createHandlerFn(this, () => {
return L.resolveDefault(callConnStat(site), {}).then((ret) => {
let ele = o.default.firstElementChild.nextElementSibling;
if (ret.result) {
ele.style.setProperty('color', 'green');
ele.innerHTML = _('passed');
} else {
ele.style.setProperty('color', 'red');
ele.innerHTML = _('failed');
}
});
})
}, [ _('Check') ]),
' ',
E('strong', { 'style': 'color:gray' }, _('unchecked')),
]);
}
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 'hiddify-c':
section = 'config';
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))
)
])
])
]);
}
const CORE_MGMT = '/usr/share/homeproxy/scripts/core_mgmt.uc';
const callCurlStatus = rpc.declare({
object: 'luci.homeproxy',
method: 'curl_status',
expect: { '': {} }
});
const callCurlInstall = rpc.declare({
object: 'luci.homeproxy',
method: 'curl_install',
expect: { '': {} }
});
const callCurlRemove = rpc.declare({
object: 'luci.homeproxy',
method: 'curl_remove',
expect: { '': {} }
});
const callByeDPIStatus = rpc.declare({
object: 'luci.homeproxy',
method: 'byedpi_status',
expect: { '': {} }
});
const callByeDPIPrepareInstall = rpc.declare({
object: 'luci.homeproxy',
method: 'byedpi_prepare_install',
expect: { '': {} }
});
const callByeDPIInstallPkg = rpc.declare({
object: 'luci.homeproxy',
method: 'byedpi_install_pkg',
params: ['tmp_path', 'pkg_manager'],
expect: { '': {} }
});
const callByeDPIRemove = rpc.declare({
object: 'luci.homeproxy',
method: 'byedpi_remove',
expect: { '': {} }
});
const callZapretStatus = rpc.declare({
object: 'luci.homeproxy',
method: 'zapret_status',
expect: { '': {} }
});
const callZapretPrepareInstall = rpc.declare({
object: 'luci.homeproxy',
method: 'zapret_prepare_install',
expect: { '': {} }
});
const callZapretInstallPkg = rpc.declare({
object: 'luci.homeproxy',
method: 'zapret_install_pkg',
params: ['tmp_path', 'pkg_manager'],
expect: { '': {} }
});
const callZapretRemove = rpc.declare({
object: 'luci.homeproxy',
method: 'zapret_remove',
expect: { '': {} }
});
function buildByeDPICard(byedpi, isMainNode) {
let installed = byedpi?.installed || false;
let version = byedpi?.version || null;
const running = byedpi?.running || false;
const pkgMgr = byedpi?.pkg_manager || null;
const canInstall = !!pkgMgr;
const statusEl = E('strong', {
style: installed ? 'color:green' : 'color:gray'
}, installed ? (version ? 'v' + version : _('Installed')) : _('Not installed'));
const runEl = E('span', {
style: 'margin-left:6px; font-size:0.9em; color:' + (running ? 'green' : 'gray')
}, running ? _('running') : _('stopped'));
const msgEl = E('span', { style: 'margin-left:8px; font-size:0.9em' }, '');
const setMsg = (txt, color) => { msgEl.textContent = txt; msgEl.style.color = color || 'gray'; };
const installBtn = E('button', {
class: 'btn cbi-button cbi-button-action',
style: 'margin-left:4px',
disabled: !canInstall || null,
title: canInstall ? '' : _('No supported package manager detected'),
click: async function() {
const prevInstalled = installed;
const prevVersion = version;
installBtn.disabled = true;
removeBtn.disabled = true;
statusEl.textContent = _('Installing...');
statusEl.style.color = 'gray';
const fail = (msg) => {
installed = prevInstalled;
version = prevVersion;
statusEl.textContent = installed ? (version ? 'v' + version : _('Installed')) : _('Not installed');
statusEl.style.color = installed ? 'green' : 'gray';
installBtn.disabled = false;
removeBtn.disabled = !installed;
setMsg(msg, 'red');
};
setMsg(_('Checking requirements...'), 'gray');
const prep = await L.resolveDefault(callByeDPIPrepareInstall(), {});
if (prep.error) return fail(prep.error);
setMsg(_('Downloading...'), 'gray');
const dl = await L.resolveDefault(callCoreDownload(prep.dl_url, prep.tmp_path), {});
if (!dl.result) return fail(dl.error || _('Download failed'));
setMsg(_('Installing package...'), 'gray');
const inst = await L.resolveDefault(callByeDPIInstallPkg(prep.tmp_path, prep.pkg_manager), {});
if (!inst.result) return fail(inst.error || _('Installation failed'));
const fresh = await L.resolveDefault(callByeDPIStatus(), {});
installed = fresh.installed || false;
version = fresh.version || null;
statusEl.textContent = installed ? (version ? 'v' + version : _('Installed')) : _('Unknown');
statusEl.style.color = installed ? 'green' : 'gray';
installBtn.textContent = _('Update');
installBtn.disabled = false;
removeBtn.disabled = false;
setMsg(_('Installed successfully'), 'green');
}
}, [ installed ? _('Update') : _('Install') ]);
const removeBtn = E('button', {
class: 'btn cbi-button cbi-button-negative',
style: 'margin-left:4px',
disabled: !installed || isMainNode || null,
title: isMainNode ? _('Cannot remove: ByeDPI is selected as Main Node. Change Main Node first.') : '',
click: async function() {
removeBtn.disabled = true;
installBtn.disabled = true;
setMsg(_('Removing...'), 'gray');
const ret = await L.resolveDefault(callByeDPIRemove(), {});
installBtn.disabled = false;
if (ret.result) {
installed = false;
version = null;
statusEl.textContent = _('Not installed');
statusEl.style.color = 'gray';
installBtn.textContent = _('Install');
setMsg(_('Removed successfully'), 'green');
} else {
removeBtn.disabled = false;
setMsg(ret.error || _('Removal failed'), 'red');
}
}
}, [ _('Remove') ]);
return E('div', { style: 'margin-bottom:12px; padding:8px 10px; border:1px solid #ddd; border-radius:4px' }, [
E('div', { style: 'display:flex; align-items:center; flex-wrap:wrap; gap:6px' }, [
E('strong', {}, 'ciadpi (ByeDPI)'),
statusEl,
runEl,
installBtn,
removeBtn,
msgEl
]),
E('div', { style: 'margin-top:4px; font-size:0.9em; color:#666' },
_('Local SOCKS5 DPI bypass proxy by <a href="https://github.com/hufrea/byedpi" target="_blank">hufrea</a>. ' +
'Packages by <a href="https://github.com/1andrevich/ByeDPI-OpenWrt" target="_blank">1andrevich/ByeDPI-OpenWrt</a>. ' +
'Configure in the Client → ByeDPI tab.'))
]);
}
function buildZapretCard(zapret) {
let installed = zapret?.installed || false;
let version = zapret?.version || null;
const running = zapret?.running || false;
const pkgMgr = zapret?.pkg_manager || null;
let kmodOk = (zapret?.kmod_ok != null) ? zapret.kmod_ok : true;
const canInstall = !!pkgMgr;
const statusEl = E('strong', {
style: installed ? 'color:green' : 'color:gray'
}, installed ? (version ? 'v' + version : _('Installed')) : _('Not installed'));
const runEl = E('span', {
style: 'margin-left:6px; font-size:0.9em; color:' + (running ? 'green' : 'gray')
}, running ? _('running') : _('stopped'));
const msgEl = E('span', { style: 'margin-left:8px; font-size:0.9em' }, '');
const setMsg = (txt, color) => { msgEl.textContent = txt; msgEl.style.color = color || 'gray'; };
const installBtn = E('button', {
class: 'btn cbi-button cbi-button-action',
style: 'margin-left:4px',
disabled: !canInstall || null,
title: canInstall ? '' : _('No supported package manager detected'),
click: async function() {
const prevInstalled = installed;
const prevVersion = version;
installBtn.disabled = true;
removeBtn.disabled = true;
statusEl.textContent = _('Installing...');
statusEl.style.color = 'gray';
const fail = (msg) => {
installed = prevInstalled;
version = prevVersion;
statusEl.textContent = installed ? (version ? 'v' + version : _('Installed')) : _('Not installed');
statusEl.style.color = installed ? 'green' : 'gray';
installBtn.disabled = false;
removeBtn.disabled = !installed;
setMsg(msg, 'red');
};
setMsg(_('Checking requirements...'), 'gray');
const prep = await L.resolveDefault(callZapretPrepareInstall(), {});
if (prep.error) return fail(prep.error);
setMsg(_('Downloading...'), 'gray');
const dl = await L.resolveDefault(callCoreDownload(prep.dl_url, prep.tmp_path), {});
if (!dl.result) return fail(dl.error || _('Download failed'));
setMsg(_('Installing package...'), 'gray');
const inst = await L.resolveDefault(callZapretInstallPkg(prep.tmp_path, prep.pkg_manager), {});
if (!inst.result) return fail(inst.error || _('Installation failed'));
const fresh = await L.resolveDefault(callZapretStatus(), {});
installed = fresh.installed || false;
version = fresh.version || null;
kmodOk = (fresh.kmod_ok != null) ? fresh.kmod_ok : true;
statusEl.textContent = installed ? (version ? 'v' + version : _('Installed')) : _('Unknown');
statusEl.style.color = installed ? 'green' : 'gray';
installBtn.textContent = _('Update');
installBtn.disabled = false;
removeBtn.disabled = false;
if (installed && !kmodOk)
setMsg(_('Installed, but kmod-nft-queue is missing — Zapret cannot intercept traffic without it.'), 'red');
else
setMsg(_('Installed successfully'), 'green');
}
}, [ installed ? _('Update') : _('Install') ]);
const removeBtn = E('button', {
class: 'btn cbi-button cbi-button-negative',
style: 'margin-left:4px',
disabled: !installed || null,
click: async function() {
removeBtn.disabled = true;
installBtn.disabled = true;
setMsg(_('Removing...'), 'gray');
const ret = await L.resolveDefault(callZapretRemove(), {});
installBtn.disabled = false;
if (ret.result) {
installed = false;
version = null;
statusEl.textContent = _('Not installed');
statusEl.style.color = 'gray';
installBtn.textContent = _('Install');
setMsg(_('Removed successfully'), 'green');
} else {
removeBtn.disabled = false;
setMsg(ret.error || _('Removal failed'), 'red');
}
}
}, [ _('Remove') ]);
/* nfqws2's NFQUEUE rule needs kmod-nft-queue; warn up-front if it's missing. */
if (installed && !kmodOk)
setMsg(_('Warning: kmod-nft-queue is not installed — Zapret cannot intercept traffic without it.'), 'red');
return E('div', { style: 'margin-bottom:12px; padding:8px 10px; border:1px solid #ddd; border-radius:4px' }, [
E('div', { style: 'display:flex; align-items:center; flex-wrap:wrap; gap:6px' }, [
E('strong', {}, 'nfqws2 (Zapret 2)'),
statusEl,
runEl,
installBtn,
removeBtn,
msgEl
]),
E('div', { style: 'margin-top:4px; font-size:0.9em; color:#666' },
_('Packet-level (NFQUEUE) DPI bypass by <a href="https://github.com/bol-van/zapret2" target="_blank">bol-van</a> (nfqws2). ' +
'Packages by <a href="https://github.com/1andrevich/zapret2-openwrt" target="_blank">1andrevich/zapret2-openwrt</a>. ' +
'Configure in the Node Settings → Zapret tab.'))
]);
}
function buildCurlCard(curl) {
let installed = curl?.installed || false;
const pkgMgr = curl?.pkg_manager || null;
const canInstall = !!pkgMgr;
const statusEl = E('strong', {
style: installed ? 'color:green' : 'color:gray'
}, installed ? _('Installed') : _('Not installed'));
const msgEl = E('span', { style: 'margin-left:8px; font-size:0.9em' }, '');
const setMsg = (txt, color) => { msgEl.textContent = txt; msgEl.style.color = color || 'gray'; };
const installBtn = E('button', {
class: 'btn cbi-button cbi-button-action',
style: 'margin-left:4px',
disabled: !canInstall || null,
title: canInstall ? '' : _('No supported package manager detected'),
click: async function() {
installBtn.disabled = true;
removeBtn.disabled = true;
statusEl.textContent = _('Installing...');
statusEl.style.color = 'gray';
setMsg('', 'gray');
const ret = await L.resolveDefault(callCurlInstall(), {});
if (ret.result) {
installed = true;
statusEl.textContent = _('Installed');
statusEl.style.color = 'green';
installBtn.textContent = _('Reinstall');
removeBtn.disabled = false;
setMsg(_('Installed successfully'), 'green');
} else {
statusEl.textContent = _('Not installed');
statusEl.style.color = 'gray';
installBtn.disabled = false;
setMsg(ret.error || _('Installation failed'), 'red');
}
}
}, [ _('Install') ]);
const removeBtn = E('button', {
class: 'btn cbi-button cbi-button-negative',
style: 'margin-left:4px',
disabled: !installed || null,
click: async function() {
removeBtn.disabled = true;
installBtn.disabled = true;
setMsg(_('Removing...'), 'gray');
const ret = await L.resolveDefault(callCurlRemove(), {});
installBtn.disabled = false;
if (ret.result) {
installed = false;
statusEl.textContent = _('Not installed');
statusEl.style.color = 'gray';
installBtn.textContent = _('Install');
setMsg(_('Removed successfully'), 'green');
} else {
removeBtn.disabled = false;
setMsg(ret.error || _('Removal failed'), 'red');
}
}
}, [ _('Remove') ]);
return E('div', { style: 'margin-bottom:12px; padding:8px 10px; border:1px solid #ddd; border-radius:4px' }, [
E('div', { style: 'display:flex; align-items:center; flex-wrap:wrap; gap:6px' }, [
E('strong', {}, 'curl'),
statusEl,
installBtn,
removeBtn,
msgEl
]),
E('div', { style: 'margin-top:4px; font-size:0.9em; color:#666' },
_('Enables real HTTP-based ByeDPI strategy testing. Required for the "Test all strategies" feature in Client → ByeDPI tab.'))
]);
}
function callCoreInfo() {
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'info'], 'json');
}
function callCoreCheckRemote(core) {
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'check_remote', core], 'json');
}
function callCorePrepare(core, variant) {
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'prepare_install', core, variant || ''], 'json');
}
function callCoreDownload(url, tmpPath) {
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'download_pkg', url, tmpPath], 'json');
}
function callCoreInstallPkg(core, tmpPath, pkgManager) {
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'install_pkg', core, tmpPath, pkgManager], 'json');
}
function callCoreInstallKmods(pkgManager) {
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'install_kmods', pkgManager], 'json');
}
function callCoreRemove(core) {
return fs.exec_direct('/usr/bin/ucode', [CORE_MGMT, 'remove', core], 'json');
}
function buildCoreCard(core, coreInfo) {
const isHiddify = core === 'hiddify';
const name = isHiddify ? 'hiddify-core' : 'sing-box-extended';
const pkgMgr = coreInfo.pkg_manager;
const coreData = (isHiddify ? coreInfo.hiddify : coreInfo.singbox) || {};
const canInstall = !!pkgMgr;
const desc = isHiddify
? _('hiddify-core with sing-box syntax compatibility. Supports Hiddify App protocols and advanced features. Best compatibility with Hiddify Manager protocols. Does not support AmneziaWG.')
: _('Extended sing-box with additional protocols including AmneziaWG and TrustTunnel support. Created by shtorm-7.');
let installed = coreData.installed || false;
let version = coreData.version || null;
const statusEl = E('strong', {
style: installed ? 'color:green' : 'color:gray'
}, installed ? 'v' + version : _('Not installed'));
const msgEl = E('span', { style: 'margin-left:8px; font-size:0.9em' }, '');
const setMsg = (txt, color) => { msgEl.textContent = txt; msgEl.style.color = color || 'gray'; };
const remoteEl = E('span', { style: 'font-size:0.9em; color:gray' }, '');
const checkBtn = E('button', {
class: 'btn cbi-button',
click: async function() {
checkBtn.disabled = true;
remoteEl.textContent = _('Checking...');
remoteEl.style.color = 'gray';
const ret = await L.resolveDefault(callCoreCheckRemote(core), {});
checkBtn.disabled = false;
if (ret.error) {
remoteEl.textContent = ret.error;
remoteEl.style.color = 'red';
} else {
remoteEl.textContent = _('Latest') + ': v' + ret.version;
remoteEl.style.color = installed && version === ret.version ? 'green' : 'darkorange';
}
}
}, [ _('Check update') ]);
/* One smart Install: the backend auto-picks the build that fits this device's flash
* (and RAM for the compact one) — the user never sees "UPX" or has to choose. */
const doInstall = async () => {
const prevInstalled = installed;
const prevVersion = version;
installBtn.disabled = true;
removeBtn.disabled = true;
statusEl.textContent = _('Installing...');
statusEl.style.color = 'gray';
const fail = (msg) => {
installed = prevInstalled;
version = prevVersion;
statusEl.textContent = installed ? 'v' + (version || '?') : _('Not installed');
statusEl.style.color = installed ? 'green' : 'gray';
installBtn.disabled = false;
removeBtn.disabled = !installed;
setMsg(msg, 'red');
};
setMsg(_('Checking requirements...'), 'gray');
const prep = await L.resolveDefault(callCorePrepare(core, ''), {});
if (prep.error) return fail(prep.error);
const compact = (prep.variant === 'upx');
setMsg(prep.note || _('Downloading...'), prep.note ? 'darkorange' : 'gray');
const dl = await L.resolveDefault(callCoreDownload(prep.dl_url, prep.tmp_path), {});
if (!dl.result) return fail(dl.error || _('Download failed'));
setMsg(_('Installing package...'), 'gray');
const inst = await L.resolveDefault(callCoreInstallPkg(core, prep.tmp_path, prep.pkg_manager), {});
if (!inst.result) return fail(inst.error || _('Installation failed'));
setMsg(_('Installing kernel modules...'), 'gray');
await L.resolveDefault(callCoreInstallKmods(prep.pkg_manager), {});
const fresh = await L.resolveDefault(callCoreInfo(), {});
const fd = (isHiddify ? fresh.hiddify : fresh.singbox) || {};
installed = fd.installed || false;
version = fd.version || null;
statusEl.textContent = installed ? 'v' + version : _('Unknown');
statusEl.style.color = installed ? 'green' : 'gray';
installBtn.textContent = _('Update');
installBtn.disabled = false;
removeBtn.disabled = false;
setMsg(compact ? _('Installed successfully (compact build)') : _('Installed successfully'), 'green');
};
const installBtn = E('button', {
class: 'btn cbi-button cbi-button-action',
style: 'margin-left:4px',
disabled: !canInstall || null,
title: canInstall ? '' : _('No supported package manager detected'),
click: function() { return doInstall(); }
}, [ installed ? _('Update') : _('Install') ]);
const removeBtn = E('button', {
class: 'btn cbi-button cbi-button-negative',
style: 'margin-left:4px',
disabled: !installed || null,
click: async function() {
removeBtn.disabled = true;
installBtn.disabled = true;
setMsg(_('Removing...'), 'gray');
const ret = await L.resolveDefault(callCoreRemove(core), {});
installBtn.disabled = false;
if (ret.result) {
installed = false;
version = null;
statusEl.textContent = _('Not installed');
statusEl.style.color = 'gray';
installBtn.textContent = _('Install');
setMsg(_('Removed successfully'), 'green');
} else {
removeBtn.disabled = false;
setMsg(ret.error || _('Removal failed'), 'red');
}
}
}, [ _('Remove') ]);
return E('div', { style: 'margin-bottom:12px; padding:8px 10px; border:1px solid #ddd; border-radius:4px' }, [
E('div', { style: 'display:flex; align-items:center; flex-wrap:wrap; gap:6px' }, [
E('strong', {}, name),
statusEl,
checkBtn,
remoteEl,
installBtn,
removeBtn,
msgEl
]),
E('div', { style: 'margin-top:4px; font-size:0.9em; color:#666' }, desc)
]);
}
return view.extend({
load() {
return Promise.all([
hp.getBuiltinFeatures(),
L.resolveDefault(callCoreInfo(), {}),
uci.load('homeproxy'),
L.resolveDefault(callByeDPIStatus(), {}),
L.resolveDefault(callCurlStatus(), {}),
L.resolveDefault(callZapretStatus(), {})
]);
},
render([features, coreInfo, _uci, byedpiStatus, curlStatus, zapretStatus]) {
const routingMode = uci.get('homeproxy', 'config', 'routing_mode') || '';
const isRuMode = routingMode === 'proxy_banned_ru';
let m, s, o;
m = new form.Map('homeproxy');
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
s.anonymous = true;
o = s.option(form.DummyValue, '_active_core', _('Active core'));
const coreName = features.core_type === 'hiddify' ? 'hiddify-core' :
features.core_type === 'singbox' ? 'sing-box' : null;
const coreVer = features.version ? ' v' + features.version : '';
const coreCustomSuffix = features.core_custom ? ' (custom)' : '';
if (!features.core_type) {
const callDetectCustomCore = rpc.declare({
object: 'luci.homeproxy',
method: 'detect_custom_core',
params: ['path'],
expect: { '': {} }
});
const savedPath = uci.get('homeproxy', 'config', 'custom_core_path') || '';
const pathInput = E('input', {
'type': 'text',
'class': 'cbi-input-text',
'value': savedPath,
'placeholder': '/path/to/hiddify-core',
'style': 'width:260px; margin-right:4px'
});
const detectMsg = E('span', { 'style': 'margin-left:8px; font-size:0.9em; color:gray' }, '');
const detectBtn = E('button', {
'class': 'btn cbi-button cbi-button-action',
'click': async function() {
const path = pathInput.value.trim();
if (!path) return;
detectBtn.disabled = true;
detectMsg.textContent = _('Detecting...');
detectMsg.style.color = 'gray';
const ret = await L.resolveDefault(callDetectCustomCore(path), {});
detectBtn.disabled = false;
if (ret.result) {
const typeName = ret.type === 'hiddify' ? 'hiddify-core' : 'sing-box';
detectMsg.textContent = _('Detected') + ': ' + typeName + (ret.version ? ' v' + ret.version : '') + ' — ' + _('reload page to apply');
detectMsg.style.color = 'green';
} else {
detectMsg.textContent = ret.error || _('Detection failed');
detectMsg.style.color = 'red';
}
}
}, [ _('Detect') ]);
o.default = E('div', {}, [
E('strong', { 'style': 'color:red' }, _('No core installed')),
E('details', { 'style': 'margin-top:6px' }, [
E('summary', { 'style': 'cursor:pointer; color:#666; font-size:0.9em' }, _('I have a custom core path')),
E('div', { 'style': 'margin-top:6px' }, [ pathInput, detectBtn, detectMsg ])
])
]);
} else {
o.default = E('strong', { 'style': 'color:green' }, coreName + coreVer + coreCustomSuffix);
}
/* Region rule-sets (geosite/geoip .srs) are versioned and refreshed by the core itself,
* so there are no local list-version files to display here. */
if (!isRuMode) {
o = s.option(form.Value, 'github_token', _('GitHub token'));
o.description = _('Used to check for ByeDPI and Zapret updates. Without a token, GitHub limits anonymous requests to 60/hour. Create at github.com → Settings → Developer settings → Personal access tokens (no scopes needed).');
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', _('Core management'));
s.anonymous = true;
o = s.option(form.DummyValue, '_core_env');
const tmpMB = coreInfo.tmp_free_kb != null ? Math.round(coreInfo.tmp_free_kb / 1024) : '?';
const overlayMB = coreInfo.overlay_free_kb != null ? Math.round(coreInfo.overlay_free_kb / 1024) : '?';
o.default = E('div', { style: 'font-size:0.9em; color:#555; padding:2px 0 6px' }, [
_('Package manager') + ': ',
E('strong', {}, coreInfo.pkg_manager || _('none detected')),
E('span', { style: 'margin:0 8px' }, '|'),
_('Architecture') + ': ',
E('strong', {}, coreInfo.arch || '?'),
E('span', { style: 'margin:0 8px' }, '|'),
_('Free /tmp') + ': ',
E('strong', {
style: (coreInfo.tmp_free_kb != null && coreInfo.tmp_free_kb < 30720) ? 'color:red' : 'color:green'
}, tmpMB + ' MB'),
E('span', { style: 'margin:0 8px' }, '|'),
_('Free overlay') + ': ',
E('strong', {
style: (coreInfo.overlay_free_kb != null && coreInfo.overlay_free_kb < 30720) ? 'color:red' : 'color:green'
}, overlayMB + ' MB')
]);
o = s.option(form.DummyValue, '_core_hiddify');
o.default = buildCoreCard('hiddify', coreInfo);
o = s.option(form.DummyValue, '_core_singbox');
o.default = buildCoreCard('singbox', coreInfo);
s = m.section(form.NamedSection, 'config', 'homeproxy', _('AntiDPI'));
s.anonymous = true;
o = s.option(form.DummyValue, '_byedpi_card');
o.default = buildByeDPICard(byedpiStatus, uci.get('homeproxy', 'config', 'main_node') === 'byedpi-out');
o = s.option(form.DummyValue, '_curl_card', _('ByeDPI strategy tester'));
o.default = buildCurlCard(curlStatus);
o = s.option(form.DummyValue, '_zapret_card');
o.default = buildZapretCard(zapretStatus);
s = m.section(form.NamedSection, 'config', 'homeproxy');
s.anonymous = true;
o = s.option(form.DummyValue, '_homeproxy_logview');
o.render = L.bind(getRuntimeLog, this, o, _('Re:HomeProxy'));
o = s.option(form.DummyValue, '_hiddify-c_logview');
o.render = L.bind(getRuntimeLog, this, o, _('core client'));
return m.render();
},
handleSaveApply: null,
handleSave: null,
handleReset: null
});
+220
View File
@@ -0,0 +1,220 @@
#!/bin/sh
# Установщик Re:HomeProxy для OpenWrt (в одном скрипте: APK / opkg / 23.05 legacy)
# https://github.com/1andrevich/homeproxy-hiddify
#
# Вручную ставится только LuCI-приложение + ключ подписи, а ядро, ByeDPI и Zapret
# устанавливаются через собственный бэкенд приложения (core_mgmt.uc + rpcd-объект
# luci.homeproxy) — поэтому определение архитектуры, компактные сборки под малую
# флеш-память, проверка подписи и резерв через зеркало GitHub работают той же
# проверенной логикой, что и графический интерфейс.
#
# Установка (одной строкой — ввод читается из /dev/tty, пайп остаётся интерактивным):
# wget -qO- https://raw.githubusercontent.com/1andrevich/homeproxy-hiddify/master/install.sh | sh
# Либо в два шага:
# wget -O /tmp/install.sh https://raw.githubusercontent.com/1andrevich/homeproxy-hiddify/master/install.sh
# sh /tmp/install.sh
#
# При заблокированном/замедленном GitHub можно указать зеркало:
# GH_MIRROR=https://my.mirror sh install.sh
# (зеркало также пишется в uci, чтобы делегированные загрузки тоже его использовали).
# Внимание: `sh <(wget -O- ...)` на OpenWrt НЕ работает — в busybox ash нет
# process substitution; используйте форму с пайпом выше.
G='\033[0;32m'; R='\033[0;31m'; Y='\033[0;33m'; C='\033[0;36m'; N='\033[0m'
ok() { echo -e "${G}$1${N}"; }
info() { echo -e "${C}$1${N}"; }
warn() { echo -e "${Y}$1${N}"; }
die() { echo -e "${R}$1${N}"; exit 1; }
ask() { printf "${C}%s${N} " "$1"; read -r REPLY </dev/tty 2>/dev/null || REPLY=""; }
is_yes() { case "$1" in y|Y|yes|YES|да|Да|д|Д) return 0;; *) return 1;; esac; }
# --- Разбор JSON (на устройстве нет jq): достать строковое поле / проверить result:true
jget() { printf '%s\n' "$1" | sed -n "s/.*\"$2\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\".*/\1/p" | head -1; }
jtrue() { printf '%s' "$1" | grep -qE "\"result\"[[:space:]]*:[[:space:]]*true"; }
jerr() { printf '%s\n' "$1" | grep -qE "\"error\""; }
jfalse(){ printf '%s' "$1" | grep -qE "\"$2\"[[:space:]]*:[[:space:]]*false"; }
# --- Загрузка «сначала GitHub, при сбое — зеркало» для первого хопа (app + ключ),
# пока ещё нет собственного gh_fetch приложения. dl <url> <файл>
dl() {
wget -qO "$2" --timeout=20 "$1" 2>/dev/null && [ -s "$2" ] && return 0
if [ -n "$GH_MIRROR" ]; then
m=$(echo "$1" | sed "s#https://github.com#${GH_MIRROR}#")
wget -qO "$2" --timeout=20 "$m" 2>/dev/null && [ -s "$2" ] && return 0
fi
return 1
}
api() { wget -qO- --timeout=20 "$1" 2>/dev/null; } # GitHub API (без зеркала)
echo
ok "===== Установщик Re:HomeProxy ====="
# ---------------------------------------------------------------- 0. окружение
[ "$(id -u)" = 0 ] || die "Запустите от root."
[ -r /etc/openwrt_release ] || die "Это не OpenWrt (нет /etc/openwrt_release)."
. /etc/openwrt_release 2>/dev/null
ARCH="$DISTRIB_ARCH"; VER="$DISTRIB_RELEASE"
[ -n "$ARCH" ] || die "Не удалось определить архитектуру пакетов (DISTRIB_ARCH)."
if command -v apk >/dev/null 2>&1; then PM=apk; EXT=apk
elif command -v opkg >/dev/null 2>&1; then PM=opkg; EXT=ipk
else die "Не найден поддерживаемый менеджер пакетов (apk/opkg)."; fi
case "$VER" in
23.05*) LEGACY=1 ;;
24.10*|25.*|*SNAPSHOT*) LEGACY=0 ;;
22.*|21.*|19.*) die "OpenWrt $VER слишком старая — нужна 23.05 или новее." ;;
*) LEGACY=0; warn "Непроверенная версия OpenWrt $VER — продолжаю." ;;
esac
SUFFIX="_all"; [ "$LEGACY" = 1 ] && SUFFIX="_all-legacy"
info "Версия: OpenWrt $VER | Архитектура: $ARCH | Менеджер пакетов: $PM | legacy=$LEGACY"
# ----------------------------------------------------------- 1. LuCI-приложение + ключ
ok "[1/5] Устанавливаю LuCI-приложение Re:HomeProxy..."
if [ "$PM" = apk ]; then
if [ ! -f /etc/apk/keys/homeproxy-hiddify.pub ]; then
dl "https://github.com/1andrevich/homeproxy-hiddify/releases/latest/download/homeproxy-hiddify.pub" /tmp/hp.pub \
&& cp /tmp/hp.pub /etc/apk/keys/ && rm -f /tmp/hp.pub && ok " ключ подписи добавлен в доверенные" \
|| warn " не удалось скачать ключ подписи — поставлю без проверки подписи"
fi
fi
APPURL=$(api 'https://api.github.com/repos/1andrevich/homeproxy-hiddify/releases' \
| grep -o "https://github\.com/[^\"]*luci-app-re-homeproxy[^\"]*${SUFFIX}\.${EXT}" | head -1)
[ -n "$APPURL" ] || die "Не нашёл пакет luci-app-re-homeproxy${SUFFIX}.${EXT} (GitHub заблокирован? попробуйте GH_MIRROR=...)."
dl "$APPURL" /tmp/app.$EXT || die "Не удалось скачать приложение (попробуйте GH_MIRROR=...)."
if [ "$PM" = apk ]; then
apk add /tmp/app.$EXT 2>/dev/null || apk add --allow-untrusted /tmp/app.$EXT || die "apk add завершился ошибкой."
else
opkg update >/dev/null 2>&1; opkg install /tmp/app.$EXT || die "opkg install завершился ошибкой."
fi
rm -f /tmp/app.$EXT
ok " приложение установлено."
# Русский язык интерфейса (по умолчанию — да)
ask " Установить пакет русского языка? [Y/n] (по умолчанию Y):"
case "$REPLY" in
n|N|no|NO|нет|Нет|н|Н) ;; # отказ — пропускаем
*)
info " ставлю русский язык LuCI..."
# базовый перевод интерфейса LuCI (из фида, best-effort)
if [ "$PM" = apk ]; then apk add luci-i18n-base-ru >/dev/null 2>&1; else opkg install luci-i18n-base-ru >/dev/null 2>&1; fi
# перевод самого приложения (из релиза homeproxy)
LURL=$(api 'https://api.github.com/repos/1andrevich/homeproxy-hiddify/releases' \
| grep -o "https://github\.com/[^\"]*luci-i18n-homeproxy-ru[^\"]*\.${EXT}" | head -1)
if [ -n "$LURL" ] && dl "$LURL" /tmp/i18n.$EXT; then
if [ "$PM" = apk ]; then apk add /tmp/i18n.$EXT 2>/dev/null || apk add --allow-untrusted /tmp/i18n.$EXT; \
else opkg install /tmp/i18n.$EXT; fi
else warn " перевод приложения не найден — пропускаю"; fi
rm -f /tmp/i18n.$EXT
uci set luci.main.lang=ru; uci commit luci
ok " русский язык установлен" ;;
esac
# Прописываем зеркало в бэкенд, чтобы делегированные загрузки тоже его использовали
if [ -n "$GH_MIRROR" ]; then uci set homeproxy.config.github_mirror="$GH_MIRROR"; uci commit homeproxy; fi
# rpcd нужно перезапустить, чтобы появились ubus-методы приложения (установка ByeDPI/Zapret)
/etc/init.d/rpcd restart >/dev/null 2>&1; sleep 2
CM=/usr/share/homeproxy/scripts/core_mgmt.uc
[ -f "$CM" ] || die "core_mgmt.uc не найден после установки — прерываю."
# ------------------------------------------------------- 2. ядро прокси (обязательно)
ok "[2/5] Ядро прокси (обязательно — выберите одно)"
echo " 1) hiddify-core (по умолчанию; на малой флеш-памяти выберет компактную сборку)"
echo " 2) sing-box-extended (AmneziaWG / WARP, самый широкий набор протоколов)"
ask " Выбор [1/2] (по умолчанию 1):"
case "$REPLY" in 2) CORE=singbox ;; *) CORE=hiddify ;; esac
PREP=$(ucode "$CM" prepare_install "$CORE")
jerr "$PREP" && die " подготовка ядра не удалась: $(jget "$PREP" error)"
DLURL=$(jget "$PREP" dl_url); TMP=$(jget "$PREP" tmp_path); PMG=$(jget "$PREP" pkg_manager)
[ -n "$DLURL" ] && [ -n "$TMP" ] && [ -n "$PMG" ] || die " подготовка ядра не вернула данные для загрузки."
info " скачиваю $CORE..."
jtrue "$(ucode "$CM" download_pkg "$DLURL" "$TMP")" || die " не удалось скачать ядро (попробуйте GH_MIRROR=...)."
jtrue "$(ucode "$CM" install_pkg "$CORE" "$TMP" "$PMG")" || die " установка ядра не удалась."
jtrue "$(ucode "$CM" install_kmods "$PMG")" || warn " не удалось поставить kmod — без kmod-nft-tproxy/kmod-tun прокси не будет маршрутизировать."
ok " $CORE установлен."
# --------------------------------------------------------------- 3. Zapret (опционально)
ask "[3/5] Установить Zapret 2 (обход DPI на уровне пакетов — видео/QUIC, звонки)? [y/N]:"
if is_yes "$REPLY"; then
info " ставлю модуль ядра NFQUEUE..."
if [ "$PM" = apk ]; then apk add kmod-nft-queue >/dev/null 2>&1; else opkg install kmod-nft-queue >/dev/null 2>&1; fi
ZP=$(ubus call luci.homeproxy zapret_prepare_install 2>/dev/null)
if [ -z "$ZP" ] || jerr "$ZP"; then warn " не удалось подготовить Zapret — пропускаю. ($(jget "$ZP" error))"
else
ZURL=$(jget "$ZP" dl_url); ZTMP=$(jget "$ZP" tmp_path); ZPMG=$(jget "$ZP" pkg_manager)
if [ -n "$ZURL" ] && dl "$ZURL" "$ZTMP"; then
RES=$(ubus call luci.homeproxy zapret_install_pkg "{\"tmp_path\":\"$ZTMP\",\"pkg_manager\":\"$ZPMG\"}" 2>/dev/null)
if jtrue "$RES"; then
ok " Zapret установлен."
ZST=$(ubus call luci.homeproxy zapret_status 2>/dev/null)
if jfalse "$ZST" kmod_ok; then
warn " модуль NFQUEUE не загружен — Zapret не включаю (иначе сломается firewall). Включите вручную после kmod-nft-queue."
else
uci set homeproxy.config.zapret_enabled=1; uci commit homeproxy; ok " Zapret включён."
fi
else warn " установка Zapret не удалась."; fi
else warn " не удалось скачать Zapret — пропускаю."; fi
fi
fi
# --------------------------------------------------------------- 4. ByeDPI (опционально)
ask "[4/5] Установить ByeDPI (обход DPI на уровне SOCKS, нужен curl)? [y/N]:"
if is_yes "$REPLY"; then
info " ставлю curl (его использует тестер стратегий ByeDPI)..."
if [ "$PM" = apk ]; then apk add curl >/dev/null 2>&1; else opkg install curl >/dev/null 2>&1; fi
BP=$(ubus call luci.homeproxy byedpi_prepare_install 2>/dev/null)
if [ -z "$BP" ] || jerr "$BP"; then warn " не удалось подготовить ByeDPI — пропускаю. ($(jget "$BP" error))"
else
BURL=$(jget "$BP" dl_url); BTMP=$(jget "$BP" tmp_path); BPMG=$(jget "$BP" pkg_manager)
if [ -n "$BURL" ] && dl "$BURL" "$BTMP"; then
RES=$(ubus call luci.homeproxy byedpi_install_pkg "{\"tmp_path\":\"$BTMP\",\"pkg_manager\":\"$BPMG\"}" 2>/dev/null)
if jtrue "$RES"; then
ok " ByeDPI установлен."
uci set homeproxy.config.byedpi_enabled=1; uci commit homeproxy; ok " ByeDPI включён."
else warn " установка ByeDPI не удалась."; fi
else warn " не удалось скачать ByeDPI — пропускаю."; fi
fi
fi
# ------------------------------------------------- 5. режим маршрутизации (пресет)
ask "[5/5] Настроить режим «Россия — раздельное туннелирование» (Re:filter через основной сервер)? [Д/н] (по умолчанию да):"
case "$REPLY" in
n|N|no|NO|нет|Нет|н|Н) ;; # отказ — оставляем как есть
*)
CURMODE=$(uci -q get homeproxy.config.routing_mode)
if [ -z "$CURMODE" ]; then
uci set homeproxy.config.routing_mode=proxy_banned_ru
uci set homeproxy.config.proxy_calls=1
uci set homeproxy.config.no_proxy_torrents=1
uci set homeproxy.config.ipv6_support=0
uci commit homeproxy
CURMODE=proxy_banned_ru
fi
# Re:filter через основной пул — только если режим РФ и правил ещё нет
if [ "$CURMODE" = proxy_banned_ru ] && ! uci show homeproxy 2>/dev/null | grep -q "=proxy_ru_rule$"; then
SEC=$(uci add homeproxy proxy_ru_rule)
uci set "homeproxy.$SEC.source=refilter"
uci set "homeproxy.$SEC.node=main-out"
uci set "homeproxy.$SEC.enabled=1"
uci commit homeproxy
ok " режим настроен: Re:filter через основной сервер"
else
info " режим/правила уже заданы — не меняю"
fi ;;
esac
# ------------------------------------------------------------------- 6. финал
/etc/init.d/homeproxy enable >/dev/null 2>&1
/etc/init.d/homeproxy start >/dev/null 2>&1
# LAN-адрес роутера для прямой ссылки на страницу LuCI (обрезаем /маску на 25.12+)
LANIP=$(uci -q get network.lan.ipaddr | cut -d/ -f1)
[ -n "$LANIP" ] || LANIP=$(ip -4 addr show br-lan 2>/dev/null | sed -n 's#.*inet \([0-9.]*\).*#\1#p' | head -1)
[ -n "$LANIP" ] || LANIP="192.168.1.1"
echo
ok "===== Готово ====="
info "Откройте Re:HomeProxy в браузере:"
URL="http://$LANIP/cgi-bin/luci/admin/services/homeproxy"
# OSC 8: кликабельная ссылка в поддерживающих терминалах; в остальных просто виден URL
printf '\033[0;36m \033]8;;%s\033\\%s\033]8;;\033\\\033[0m\n' "$URL" "$URL"
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,32 @@
{
"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"
]
}
@@ -0,0 +1,65 @@
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 sniff_override '1'
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 '8.8.8.8'
option china_dns_server '223.5.5.5'
option routing_mode 'proxy_banned_ru'
option routing_port 'all'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option proxy_calls '1'
option no_proxy_torrents '1'
option github_token ''
option log_level 'warn'
config homeproxy 'control'
option lan_proxy_mode 'disabled'
config homeproxy 'routing'
option sniff_override '1'
option default_outbound 'direct-out'
option default_outbound_dns 'default-dns'
config homeproxy 'dns'
option dns_strategy 'prefer_ipv4'
option default_server 'local-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'subscription'
option auto_update '0'
option allow_insecure '0'
option packet_encoding 'xudp'
option update_via_proxy '0'
option filter_nodes 'blacklist'
list filter_keywords '重置|到期|过期|剩余|套餐'
list filter_keywords 'Expiration|Remaining'
config homeproxy 'server'
option enabled '0'
option log_level 'warn'
+18
View File
@@ -0,0 +1,18 @@
#!/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"
hiddifyc_log_file="/var/run/$NAME/hiddify-c.log"
while true; do
sleep 180
for i in "$main_log_file" "$hiddifyc_log_file"; do
[ -s "$i" ] || continue
[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
done
done
@@ -0,0 +1,600 @@
#!/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') || 'proxy_banned_ru';
let outbound_node, outbound_udp_node;
if (routing_mode !== 'custom') {
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
} else {
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
}
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;
/* ByeDPI runs as root (needed for TCP_MD5SIG etc.) but under a dedicated group, so its
* egress can be excluded from the redirect by socket group (skgid) to break the proxy
* loop — ciadpi has no fwmark option, and root traffic can't be told apart by skuid. */
const byedpi_enabled = uci.get(cfgname, 'config', 'byedpi_enabled') || '0';
const byedpi_gid = uci.get(cfgname, 'infra', 'byedpi_gid') || '8181';
/* zapret: sing-box's `zapret-out` direct outbound stamps egress with zapret_mark (a
* routing_mark that OVERRIDES default_mark). That mark must (a) escape the redirect loop
* like self_mark does, and (b) be steered into NFQUEUE so nfqws can desync the handshake.
* nfqws reinjects its fake packets carrying its own desync mark (0x40000000) — those must
* also escape the redirect, and must NOT be re-queued. */
const zapret_enabled = uci.get(cfgname, 'config', 'zapret_enabled') || '0';
const zapret_mark = uci.get(cfgname, 'config', 'zapret_mark') || '110';
const zapret_qnum = uci.get(cfgname, 'config', 'zapret_qnum') || '200';
const zapret_desync_mark = '0x40000000'; /* nfqws DESYNC_MARK (its reinjected fakes) */
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' || routing_mode === 'proxy_banned_ru') {
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 %}
{# 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
{% if (zapret_enabled === '1'): %}
meta mark {{ zapret_mark }} counter return
meta mark and {{ zapret_desync_mark }} != 0 counter return
{% endif /* zapret_enabled */ %}
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 (!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
{% if (byedpi_enabled === '1'): %}
meta skgid {{ byedpi_gid }} counter return
{% endif %}
meta nfproto { {{ (ipv6_support === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
}
{% endif %}
{# NFQUEUE chain: feed ALL Zapret-marked (mark 110) flows to nfqws2 — any port, TCP
and UDP — and let the strategy's own --filter-tcp/--filter-udp profiles decide what
to desync (the rest passes through via `bypass`). This mirrors how a list→outbound
rule routes all matching traffic: once sing-box marks a flow for zapret-out, the
queue feeds it, regardless of port (so discord.media on 2053/8443, voice UDP, QUIC,
etc. all work with no per-port firewall rules). `ct original packets 1-12` caps it
to each flow's handshake; nfqws's reinjected fakes carry DESYNC_MARK (not mark 110)
so they are never re-queued. #}
{% if (zapret_enabled === '1'): %}
chain homeproxy_zapret_queue {
type filter hook postrouting priority mangle; policy accept;
meta mark {{ zapret_mark }} meta l4proto tcp ct original packets 1-12 counter queue num {{ zapret_qnum }} bypass
meta mark {{ zapret_mark }} meta l4proto udp ct original packets 1-12 counter queue num {{ zapret_qnum }} bypass
}
{% endif %}
{# UDP tproxy #}
{% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom' || routing_mode === 'proxy_banned_ru')): %}
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 (!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 {
{% if (byedpi_enabled === '1'): %}
meta skgid {{ byedpi_gid }} counter return
{% endif %}
meta mark {{ self_mark }} counter return
{% if (zapret_enabled === '1'): %}
meta mark {{ zapret_mark }} counter return
meta mark and {{ zapret_desync_mark }} != 0 counter return
{% endif /* zapret_enabled */ %}
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 */ %}
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 (!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 %}
@@ -0,0 +1,51 @@
#!/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') || 'proxy_banned_ru',
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');
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,235 @@
/*
* 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) {
if (isEmpty(str))
return null;
str = '' + str;
/* A subscription may already deliver a unit-bearing duration (e.g. sing-box
* "10s", "1m30s"); only append 's' to a bare number so we never emit "10ss". */
return match(str, /[a-zA-Z]$/) ? str : (str + 's');
};
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 */
@@ -0,0 +1,89 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Headless import of proxy share-link(s) (vless://, vmess://, trojan://, ss://,
* hysteria://, hysteria2://, tuic://, …) into homeproxy `node` sections. This is
* the SSH/agent-callable equivalent of the browser-only "Import share links"
* button (handleLinkImport / parseShareLink) in the LuCI node view. It reuses the
* shared parser in node_parse.uc — the SAME parse_uri the subscription importer
* uses — so there is no second link-parsing implementation to drift out of sync.
*
* Usage: ucode import_link.uc <path-to-file-with-one-link-per-line | single-link> [label]
* Output: one JSON line, e.g.
* { "result": true, "imported": [ { "section": "…", "label": "…", "type": "vless" } ], "failed": 0 }
* or { "error": "…" } with a non-zero exit code.
*
* Imported nodes carry NO grouphash, so they are treated as user-created and are
* never removed by a subsequent subscription update.
*/
'use strict';
import { readfile } from 'fs';
import { cursor } from 'uci';
import { parse_uri } from 'node_parse';
const uciconfig = 'homeproxy';
function emit(o) { printf('%s\n', o); }
/* Dependency-free stable hash (FNV-1a, 32-bit) — md5/digest is absent on the
* OpenWrt 23.05 legacy build, so keep section naming pure-ucode like import_conf.uc. */
function strhash(s) {
let h = 2166136261;
const n = length(s);
for (let i = 0; i < n; i++) {
h = (h ^ ord(s, i)) & 0xFFFFFFFF;
h = (h * 16777619) & 0xFFFFFFFF;
}
return sprintf('%08x', h);
}
const arg = ARGV[0];
const want_label = ARGV[1];
if (!arg) {
emit({ error: 'usage: import_link.uc <file-with-links|link> [label]' });
exit(1);
}
/* arg is either a readable file (one link per line) or the link itself. */
let text = readfile(arg);
if (!text)
text = arg;
const lines = split(trim(text), '\n');
const uci = cursor();
let imported = [];
let failed = 0;
for (let line in lines) {
line = trim(line);
if (!length(line))
continue;
const config = parse_uri(line);
if (type(config) !== 'object' || !config.type) {
failed++;
continue;
}
const label = (want_label && length(lines) === 1)
? want_label
: (config.label || (config.type + '-' + (config.address || 'node')));
config.label = label;
const name = strhash(label + '|' + (config.address || '') + ':' + (config.port || ''));
uci.set(uciconfig, name, 'node');
for (let k in config)
if (config[k] !== null && config[k] !== '')
uci.set(uciconfig, name, k, config[k]);
push(imported, { section: name, label: label, type: config.type });
}
if (length(imported))
uci.commit(uciconfig);
emit({ result: length(imported) > 0, imported: imported, failed: failed });
@@ -0,0 +1,263 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
*/
'use strict';
import { cursor } from 'uci';
import { isEmpty, parseURL } 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 (china_dns_server === 'wan_114')
uci.set(uciconfig, ucimain, 'china_dns_server', '114.114.114.114');
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');
/* 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');
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');
}
const dns_server_migration = {};
/* DNS servers options */
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
/* 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));
/* 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)
return;
map(cfg.outbound, (outbound) => {
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, cfg.outbound, 'domain_resolver', cfg.server);
break;
}
});
return;
}
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
if (cfg.rule_set_ipcidr_match_source === '1')
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) => {
/* 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 (cfg.rule_set_ipcidr_match_source === '1')
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);
@@ -0,0 +1,680 @@
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Shared share-link parser: turns a single proxy share-link (vless://, vmess://,
* trojan://, ss://, hysteria://, hysteria2://, tuic://, …) into a homeproxy node
* config object. Extracted verbatim from update_subscriptions.uc so the
* subscription importer AND the standalone link importer (import_link.uc) use one
* implementation. `log` is an injected callback (no-op by default) so callers can
* route diagnostics where they like.
*/
'use strict';
import { urldecode, urlencode, urldecode_params } from 'luci.http';
import { parseURL, decodeBase64Str, validation, isEmpty } from 'homeproxy';
import { connect } from 'ubus';
const sing_features = connect().call('luci.homeproxy', 'singbox_get_features', {}) || {};
export function parse_uri(uri, log) {
if (type(log) != 'function') log = function() {};
let config, url, params;
if (type(uri) === 'object') {
if (uri.nodetype === 'sip008') {
/* https://shadowsocks.org/guide/sip008.html */
config = {
label: uri.remarks,
type: 'shadowsocks',
address: uri.server,
port: uri.server_port,
shadowsocks_encrypt_method: uri.method,
password: uri.password,
shadowsocks_plugin: uri.plugin,
shadowsocks_plugin_opts: uri.plugin_opts
};
} else {
/* Pre-parsed sing-box JSON outbound from parse_singbox_outbound() */
config = uri;
}
} else if (type(uri) === 'string') {
uri = split(trim(uri), '://');
switch (uri[0]) {
case 'anytls':
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'anytls',
address: url.hostname,
port: url.port,
password: urldecode(url.username),
tls: '1',
tls_sni: params.sni,
tls_insecure: (params.insecure === '1') ? '1' : '0'
};
break;
case 'http':
case 'https':
url = parseURL('http://' + uri[1]) || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'http',
address: url.hostname,
port: url.port,
username: url.username ? urldecode(url.username) : null,
password: url.password ? urldecode(url.password) : null,
tls: (uri[0] === 'https') ? '1' : '0'
};
break;
case 'hysteria':
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
if (!sing_features.with_quic || (params.protocol && params.protocol !== 'udp')) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
if (!sing_features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'hysteria',
address: url.hostname,
port: url.port,
hysteria_protocol: params.protocol || 'udp',
hysteria_auth_type: params.auth ? 'string' : null,
hysteria_auth_payload: params.auth,
hysteria_obfs_password: params.obfsParam,
hysteria_down_mbps: params.downmbps,
hysteria_up_mbps: params.upmbps,
tls: '1',
tls_insecure: (params.insecure in ['true', '1']) ? '1' : '0',
tls_sni: params.peer,
tls_alpn: params.alpn
};
break;
case 'hysteria2':
case 'hy2':
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
if (!sing_features.with_quic) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'hysteria2',
address: url.hostname,
port: url.port,
password: url.username ? (
urldecode(url.username + (url.password ? (':' + url.password) : ''))
) : null,
hysteria_obfs_type: params.obfs,
hysteria_obfs_password: params['obfs-password'],
tls: '1',
tls_insecure: (params.insecure === '1') ? '1' : '0',
tls_sni: params.sni
};
break;
case 'naive':
case 'naive+http':
case 'naive+https':
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
let naive_extra_headers = null;
if (params.header) {
const hdr = split(params.header, ':', 2);
if (length(hdr) === 2) {
let hdrs = {};
hdrs[trim(hdr[0])] = trim(hdr[1]);
naive_extra_headers = sprintf('%J', hdrs);
}
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'naive',
address: url.hostname,
port: url.port,
username: url.username ? urldecode(url.username) : null,
password: url.password ? urldecode(url.password) : null,
tls: (uri[0] === 'naive+https' || params.security === 'tls') ? '1' : '0',
tls_sni: params.sni || url.hostname,
naive_udp_over_tcp: (params.uot === '1') ? '1' : null,
naive_quic: (params.quic === '1') ? '1' : null,
naive_extra_headers: naive_extra_headers
};
break;
case 'socks':
case 'socks4':
case 'socks4a':
case 'socsk5':
case 'socks5h':
url = parseURL('http://' + uri[1]) || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'socks',
address: url.hostname,
port: url.port,
username: url.username ? urldecode(url.username) : null,
password: url.password ? urldecode(url.password) : null,
socks_version: (match(uri[0], /4/)) ? '4' : '5'
};
break;
case 'ss':
/* "Lovely" Shadowrocket format */
const ss_suri = split(uri[1], '#');
let ss_slabel = '';
if (length(ss_suri) <= 2) {
if (length(ss_suri) === 2)
ss_slabel = '#' + urlencode(ss_suri[1]);
if (decodeBase64Str(ss_suri[0]))
uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel;
}
/* Legacy format is not supported, it should be never appeared in modern subscriptions */
/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
url = parseURL('http://' + uri[1]) || {};
let ss_userinfo = {};
if (url.username && url.password)
/* User info encoded with URIComponent */
ss_userinfo = [url.username, urldecode(url.password)];
else if (url.username)
/* User info encoded with base64 */
ss_userinfo = split(decodeBase64Str(urldecode(url.username)), ':', 2);
let ss_plugin, ss_plugin_opts;
if (url.search && url.searchParams.plugin) {
const ss_plugin_info = split(url.searchParams.plugin, ';', 2);
ss_plugin = ss_plugin_info[0];
if (ss_plugin === 'simple-obfs')
/* Fix non-standard plugin name */
ss_plugin = 'obfs-local';
ss_plugin_opts = ss_plugin_info[1];
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'shadowsocks',
address: url.hostname,
port: url.port,
shadowsocks_encrypt_method: ss_userinfo[0],
password: ss_userinfo[1],
shadowsocks_plugin: ss_plugin,
shadowsocks_plugin_opts: ss_plugin_opts
};
break;
case 'shadowtls':
/* shadowtls://STLS_PASS@HOST:PORT?version=V&sni=SNI&fp=FP&method=SS_METHOD&password=SS_PASS#Label
* Represents a Shadowsocks 2022 connection wrapped in ShadowTLS transport. */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'shadowsocks',
address: url.hostname,
port: url.port,
shadowsocks_encrypt_method: params.method || '2022-blake3-aes-256-gcm',
password: params.password ? urldecode(params.password) : null,
shadowtls_enabled: '1',
shadowtls_password: url.username ? urldecode(url.username) : null,
shadowtls_version: params.version || '3',
tls_sni: params.sni || null,
tls_utls: params.fp || null
};
break;
case 'ssh':
/* Manual parse to avoid parseURL corruption on long base64 query values */
let ssh_str = trim(uri[1]);
let ssh_label = null;
const ssh_hash_idx = index(ssh_str, '#');
if (ssh_hash_idx >= 0) {
ssh_label = urldecode(substr(ssh_str, ssh_hash_idx + 1));
ssh_str = substr(ssh_str, 0, ssh_hash_idx);
}
let ssh_params = {};
const ssh_q = index(ssh_str, '?');
if (ssh_q >= 0) {
ssh_params = urldecode_params(substr(ssh_str, ssh_q + 1)) || {};
ssh_str = substr(ssh_str, 0, ssh_q);
}
ssh_str = replace(ssh_str, /\/+$/, '');
const ssh_at = index(ssh_str, '@');
let ssh_user = null, ssh_pass = null, ssh_host = null, ssh_port = null;
if (ssh_at >= 0) {
const ssh_userinfo = substr(ssh_str, 0, ssh_at);
const ssh_hostport = substr(ssh_str, ssh_at + 1);
const ssh_colon = index(ssh_userinfo, ':');
if (ssh_colon >= 0) {
ssh_user = urldecode(substr(ssh_userinfo, 0, ssh_colon));
ssh_pass = urldecode(substr(ssh_userinfo, ssh_colon + 1));
} else {
ssh_user = urldecode(ssh_userinfo);
}
const ssh_hp = split(ssh_hostport, ':');
ssh_port = pop(ssh_hp);
ssh_host = join(':', ssh_hp) || null;
}
let ssh_host_key = null;
/* Hiddify: hk=key1\n,key2\n,key3\n — urldecode_params already decodes values */
const raw_hk = ssh_params.hk || ssh_params.host_key || ssh_params.hostKey;
if (raw_hk) {
const ssh_hk_lines = filter(split(raw_hk, ','), (l) => length(trim(l)) > 0);
ssh_host_key = length(ssh_hk_lines) ? ssh_hk_lines : null;
}
config = {
label: ssh_label,
type: 'ssh',
address: ssh_host,
port: ssh_port,
username: ssh_user,
password: length(ssh_pass) ? ssh_pass : null,
/* Hiddify uses pk= (short) or private_key= ; urldecode_params already decoded */
ssh_priv_key: ssh_params.pk || ssh_params.private_key || ssh_params.privateKey || null,
ssh_priv_key_pp: length(ssh_params.passphrase) ? ssh_params.passphrase : null,
ssh_host_key: ssh_host_key,
/* Hiddify always enables udp_over_tcp for SSH; only disable if explicitly set to 0 */
ssh_udp_over_tcp: (ssh_params.uot === '0' || ssh_params.udp_over_tcp in ['0', 'false']) ? null : '1'
};
break;
case 'trojan':
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'trojan',
address: url.hostname,
port: url.port,
password: urldecode(url.username),
transport: (params.type && params.type !== 'tcp') ? params.type : null,
tls: '1',
tls_sni: params.sni,
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
tls_reality: (params.security === 'reality') ? '1' : '0',
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
tls_reality_short_id: params.sid,
tls_utls: sing_features.with_utls ? params.fp : null
};
switch(params.type) {
case 'grpc':
config.grpc_servicename = params.serviceName;
break;
case 'http':
case 'tcp':
if (params.type === 'http' || params.headerType === 'http') {
config.http_host = params.host ? split(urldecode(params.host), ',') : null;
config.http_path = params.path ? urldecode(params.path) : null;
}
break;
case 'httpupgrade':
config.httpupgrade_host = params.host ? urldecode(params.host) : null;
config.http_path = params.path ? urldecode(params.path) : null;
break;
case 'ws':
config.ws_host = params.host ? urldecode(params.host) : null;
config.ws_path = params.path ? urldecode(params.path) : null;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
case 'xhttp':
config.http_path = params.path ? urldecode(params.path) : null;
config.http_host = params.host ? urldecode(params.host) : null;
config.xhttp_mode = params.mode || null;
break;
}
if (params.hiddify === '1') {
if (params.fragment) {
const fparts = split(urldecode(params.fragment), ',');
if (length(fparts) >= 2) {
config.tls_fragment = '1';
config.tls_fragment_size = fparts[0];
config.tls_fragment_sleep = fparts[1];
config.tls_fragment_type = fparts[2] || null;
}
}
if (params.allowInsecure === 'true' || params.insecure === 'true')
config.tls_insecure = '1';
}
break;
case 'tuic':
/* https://github.com/daeuniverse/dae/discussions/182 */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
if (!sing_features.with_quic) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'tuic',
address: url.hostname,
port: url.port,
uuid: url.username,
password: url.password ? urldecode(url.password) : null,
tuic_congestion_control: params.congestion_control,
tuic_udp_relay_mode: params.udp_relay_mode,
tuic_enable_zero_rtt: params.zero_rtt_handshake || null,
tuic_heartbeat: params.heartbeat || null,
tls: '1',
tls_sni: params.sni,
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
};
break;
case 'vless':
/* https://github.com/XTLS/Xray-core/discussions/716 */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
/* Unsupported protocol */
if (params.type === 'kcp') {
log(sprintf('Skipping sunsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
return null;
} else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !sing_features.with_quic)) {
log(sprintf('Skipping sunsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
if (!sing_features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'vless',
address: url.hostname,
port: url.port,
uuid: url.username,
transport: (params.type !== 'tcp') ? params.type : null,
tls: (params.security in ['tls', 'xtls', 'reality']) ? '1' : '0',
tls_sni: params.sni,
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
tls_reality: (params.security === 'reality') ? '1' : '0',
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
tls_reality_short_id: params.sid,
tls_utls: sing_features.with_utls ? params.fp : null,
vless_flow: (params.security in ['tls', 'reality']) ? params.flow : null
};
switch(params.type) {
case 'grpc':
config.grpc_servicename = params.serviceName;
break;
case 'http':
case 'tcp':
if (params.type === 'http' || params.headerType === 'http') {
config.http_host = params.host ? split(urldecode(params.host), ',') : null;
config.http_path = params.path ? urldecode(params.path) : null;
}
break;
case 'httpupgrade':
config.httpupgrade_host = params.host ? urldecode(params.host) : null;
config.http_path = params.path ? urldecode(params.path) : null;
break;
case 'ws':
config.ws_host = params.host ? urldecode(params.host) : null;
config.ws_path = params.path ? urldecode(params.path) : null;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
case 'xhttp':
config.http_path = params.path ? urldecode(params.path) : null;
config.http_host = params.host ? urldecode(params.host) : null;
config.xhttp_mode = params.mode || null;
break;
}
if (params.hiddify === '1') {
if (params.fragment) {
const fparts = split(urldecode(params.fragment), ',');
if (length(fparts) >= 2) {
config.tls_fragment = '1';
config.tls_fragment_size = fparts[0];
config.tls_fragment_sleep = fparts[1];
config.tls_fragment_type = fparts[2] || null;
}
}
if (params.allowInsecure === 'true' || params.insecure === 'true')
config.tls_insecure = '1';
if (params.extra) {
try {
const extra = json(urldecode(params.extra));
if (extra.headers && length(keys(extra.headers)) > 0)
config.xhttp_headers = sprintf('%J', extra.headers);
if (extra.downloadSettings) {
const dl = extra.downloadSettings;
config.xhttp_download_server = dl.address;
config.xhttp_download_port = '' + dl.port;
if (dl.xhttpSettings) {
config.xhttp_download_path = dl.xhttpSettings.path;
config.xhttp_download_host = dl.xhttpSettings.host;
config.xhttp_download_mode = dl.xhttpSettings.mode;
}
config.xhttp_download_security = dl.security;
if (dl.security === 'reality' && dl.realitySettings) {
config.xhttp_download_sni = dl.realitySettings.serverName;
config.xhttp_download_fp = dl.realitySettings.fingerprint;
config.xhttp_download_pbk = dl.realitySettings.publicKey;
config.xhttp_download_sid = dl.realitySettings.shortId;
} else if (dl.security === 'tls' && dl.tlsSettings) {
config.xhttp_download_sni = dl.tlsSettings.serverName;
config.xhttp_download_alpn = dl.tlsSettings.alpn;
}
}
} catch(e) {}
}
}
break;
case 'vmess':
/* "Lovely" shadowrocket format */
if (match(uri, /&/)) {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
}
/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
try {
uri = json(decodeBase64Str(uri[1])) || {};
} catch(e) {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
}
if (uri.v != '2') {
log(sprintf('Skipping unsupported %s format.', uri[0]));
return null;
/* Unsupported protocol */
} else if (uri.net === 'kcp') {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
return null;
} else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || uri.path || !sing_features.with_quic)) {
log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
if (!sing_features.with_quic)
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
return null;
}
/*
* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
* else if (uri.aid && int(uri.aid) !== 0) {
* log(sprintf('Skipping unsupported %s node: %s.', uri[0], uri.ps || uri.add));
* return null;
* }
*/
config = {
label: uri.ps ? urldecode(uri.ps) : null,
type: 'vmess',
address: uri.add,
port: uri.port,
uuid: uri.id,
vmess_alterid: uri.aid,
vmess_encrypt: uri.scy || 'auto',
vmess_global_padding: '1',
transport: (uri.net !== 'tcp') ? uri.net : null,
tls: (uri.tls === 'tls') ? '1' : '0',
tls_sni: uri.sni || uri.host,
tls_alpn: uri.alpn ? split(uri.alpn, ',') : null,
tls_utls: sing_features.with_utls ? uri.fp : null
};
switch (uri.net) {
case 'grpc':
config.grpc_servicename = uri.path;
break;
case 'h2':
case 'tcp':
if (uri.net === 'h2' || uri.type === 'http') {
config.transport = 'http';
config.http_host = uri.host ? split(uri.host, ',') : null;
config.http_path = uri.path;
}
break;
case 'httpupgrade':
config.httpupgrade_host = uri.host;
config.http_path = uri.path;
break;
case 'ws':
config.ws_host = uri.host;
config.ws_path = uri.path;
if (config.ws_path && match(config.ws_path, /\?ed=/)) {
config.websocket_early_data_header = 'Sec-WebSocket-Protocol';
config.websocket_early_data = split(config.ws_path, '?ed=')[1];
config.ws_path = split(config.ws_path, '?ed=')[0];
}
break;
}
break;
case 'mieru':
/* https://github.com/enfein/mieru */
url = parseURL('http://' + uri[1]) || {};
params = url.searchParams || {};
config = {
label: url.hash ? urldecode(url.hash) : null,
type: 'mieru',
address: url.hostname,
port: '0',
username: url.username ? urldecode(url.username) : null,
password: url.password ? urldecode(url.password) : null,
mieru_protocol: params.protocol || null,
mieru_port_range: params.port || null,
mieru_multiplexing: params.multiplexing || null,
mieru_handshake_mode: params['handshake-mode'] || null
};
break;
case 'wg':
case 'wireguard':
/* Manual parse: WireGuard private key may contain URL-encoded chars
* incompatible with parseURL's userinfo regex */
let wg_str = trim(uri[1]);
let wg_label = null;
const wg_hash = index(wg_str, '#');
if (wg_hash >= 0) {
wg_label = urldecode(substr(wg_str, wg_hash + 1));
wg_str = substr(wg_str, 0, wg_hash);
}
let wg_params = {};
const wg_q = index(wg_str, '?');
if (wg_q >= 0) {
wg_params = urldecode_params(substr(wg_str, wg_q + 1)) || {};
wg_str = substr(wg_str, 0, wg_q);
}
/* Strip trailing slash left by "KEY@HOST:PORT/?params" format */
wg_str = replace(wg_str, /\/+$/, '');
const wg_at = index(wg_str, '@');
let wg_priv_key = null, wg_host = null, wg_port = null;
if (wg_at >= 0) {
wg_priv_key = urldecode(substr(wg_str, 0, wg_at));
const wg_hp_parts = split(substr(wg_str, wg_at + 1), ':');
wg_port = pop(wg_hp_parts);
wg_host = join(':', wg_hp_parts) || null;
} else {
/* wg://HOST:PORT?privateKey=...&publicKey=... format (no userinfo) */
const wg_hp_parts = split(wg_str, ':');
wg_port = pop(wg_hp_parts);
wg_host = join(':', wg_hp_parts) || null;
wg_priv_key = wg_params.privateKey || wg_params.privatekey || null;
}
const wg_local_addr = wg_params.address || wg_params.ip || null;
config = {
label: wg_label,
type: 'wireguard',
address: wg_host,
port: wg_port,
wireguard_private_key: wg_priv_key,
wireguard_peer_public_key: wg_params.publicKey || wg_params.publickey || null,
wireguard_pre_shared_key: wg_params.presharedKey || wg_params.presharedkey || null,
wireguard_local_address: wg_local_addr ? split(wg_local_addr, ',') : null,
wireguard_mtu: wg_params.mtu || null,
wireguard_reserved: wg_params.reserved ? split(wg_params.reserved, ',') : null
};
break;
}
}
if (!isEmpty(config)) {
if (config.address)
config.address = replace(config.address, /\[|\]/g, '');
if (!validation('host', config.address) || (config.type !== 'mieru' && !validation('port', config.port))) {
log(sprintf('Skipping invalid %s node: %s.', config.type, config.label || 'NULL'));
return null;
} else if (!config.label)
config.label = (validation('ip6addr', config.address) ?
`[${config.address}]` : config.address) + ':' + config.port;
}
return config;
};
@@ -0,0 +1,11 @@
#!/bin/sh
# SPDX-License-Identifier: GPL-2.0-only
#
# Copyright (C) 2023 ImmortalWrt.org
SCRIPTS_DIR="/etc/homeproxy/scripts"
# Region rule-sets (geosite/geoip .srs) are remote sing-box rule_sets — the core downloads
# and refreshes them itself (update_interval). No firewall list files to cron-update anymore.
"$SCRIPTS_DIR"/update_subscriptions.uc
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,52 @@
#!/bin/sh
# Resolve the Zapret tester's fixed test hosts to one IPv4 each — ONCE, up front,
# so the full-test sweep doesn't re-hit DNS for every one of its candidates.
#
# Resolution uses the router's normal resolver: the SAME secure-DNS / direct-egress
# path production uses for these blocked domains (generate_client.uc), so the IP is
# representative for the TLS-handshake probe — a foreign resolver could hand back a
# different CDN edge and make the result meaningless.
#
# Each lookup is time-bounded (busybox has no `timeout`: bg + watchdog kill) so a
# wedged resolver can't hang the rpcd call (which otherwise surfaces in LuCI as
# "ubus call error"), and the whole pass is retried once because applying Zapret
# restarts homeproxy's DNS resolver and it's briefly unavailable right after.
#
# Output: one JSON line —
# {"ok":1,"ips":{"yt":"1.2.3.4","tg":"…","dc":"…","st":"…"}}
# {"ok":0,"error":"…"}
# tag|host — mirrors zapret_test.sh's HOSTS (far/near spread).
HOSTS="yt|redirector.googlevideo.com tg|telegram.org dc|discord.com st|www.speedtest.net"
TMP=/tmp/zapret_resolve.$$
mkdir -p "$TMP" 2>/dev/null
trap 'rm -rf "$TMP" 2>/dev/null' EXIT INT TERM
resolve_one() { # $1=host -> echoes one non-loopback IPv4 (or empty); bounded ~2s
nslookup "$1" > "$TMP/o" 2>/dev/null & np=$!
( sleep 2; kill "$np" 2>/dev/null ) >/dev/null 2>&1 & sw=$!
wait "$np" 2>/dev/null
kill "$sw" 2>/dev/null
grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' "$TMP/o" 2>/dev/null | grep -vE '^127\.' | tail -1
}
JSON=""; OK=0; n=0
while [ "$n" -lt 2 ]; do
JSON=""; ANY=""
for h in $HOSTS; do
tag=${h%%|*}; host=${h##*|}
ip=$(resolve_one "$host")
[ -n "$ip" ] && ANY=1
[ -n "$JSON" ] && JSON="$JSON,"
JSON="$JSON\"$tag\":\"$ip\""
done
[ -n "$ANY" ] && { OK=1; break; }
n=$((n+1)); sleep 1
done
if [ "$OK" = 1 ]; then
echo "{\"ok\":1,\"ips\":{$JSON}}"
else
echo '{"ok":0,"error":"could not resolve any test host (DNS not ready? wait a few seconds after Apply, then retry)"}'
fi
@@ -0,0 +1,137 @@
#!/bin/sh
# Isolated nfqws2 strategy tester.
#
# nfqws2 is a packet mangler (NFQUEUE), not a proxy, so it can't be probed
# "through" like ByeDPI's SOCKS port. Instead we queue ONLY the resolved
# test-site IPs to a TEMP NFQUEUE running the candidate strategy, probe the TLS
# handshake with curl, and tear everything down via a trap — without disturbing
# the live homeproxy queue (qnum 200) or other traffic.
#
# Usage: zapret_test.sh "<strategy opts>"
# Output: a single JSON line: {"ok":N,"total":M,"results":[{tag,label,host,ip,tls,ok,reason},...]}
#
# Pass signal = curl time_appconnect > 0 (the TLS handshake completed = the desync
# got the ClientHello past the DPI). HTTP status is irrelevant (a 404 is a pass).
STRAT="$1"
IPS_ARG="$2" # optional pre-resolved "tag=ip tag=ip …" (full-test
# passes these so 36 candidates don't each re-hit DNS)
QNUM=209 # temp queue, distinct from the live 200
DMARK=0x40000000 # nfqws2 DESYNC_MARK (its reinjected fakes)
NFQWS=/opt/zapret2/nfq2/nfqws2
LUA="--lua-init=@/opt/zapret2/lua/zapret-lib.lua --lua-init=@/opt/zapret2/lua/zapret-antidpi.lua --lua-init=@/opt/zapret2/lua/zapret-auto.lua"
TABLE=zapret_test
CMT=zapret_test # comment tag on our output_redir bypass rules
TMP=/tmp/zapret_test.$$
# tag|label|host — far/near spread (mirrors the ByeDPI tester)
HOSTS="yt|YouTube|redirector.googlevideo.com tg|Telegram|telegram.org dc|Discord|discord.com st|Speedtest|www.speedtest.net"
NFQ_PID=""
WATCH_PID=""
cleanup() {
# nfqws2 daemonizes (so $! is stale) and busybox has no pkill. Match nfqws2 by
# process NAME (never the shell/grep itself), then kill only the one on our temp
# qnum (sparing the live qnum 200 instance).
for p in $(pgrep nfqws2 2>/dev/null); do
tr '\0' ' ' < "/proc/$p/cmdline" 2>/dev/null | grep -q "qnum=$QNUM" && kill "$p" 2>/dev/null
done
[ -n "$NFQ_PID" ] && kill "$NFQ_PID" 2>/dev/null
nft delete table inet "$TABLE" 2>/dev/null
# remove our surgical bypass rules from the LIVE output_redir chain (by comment)
for hnd in $(nft -a list chain inet fw4 homeproxy_output_redir 2>/dev/null | grep "$CMT" | grep -oE 'handle [0-9]+' | grep -oE '[0-9]+'); do
nft delete rule inet fw4 homeproxy_output_redir handle "$hnd" 2>/dev/null
done
[ -n "$WATCH_PID" ] && kill "$WATCH_PID" 2>/dev/null
rm -rf "$TMP" 2>/dev/null
}
trap cleanup EXIT INT TERM
fail() { echo "{\"ok\":0,\"total\":0,\"error\":\"$1\"}"; exit 0; }
[ -x "$NFQWS" ] || fail "zapret2 not installed"
command -v curl >/dev/null 2>&1 || fail "curl not installed"
mkdir -p "$TMP" 2>/dev/null
# Watchdog: guarantee teardown within 30s even if curl/nslookup wedges, so the
# live firewall is never left modified (busybox has no `timeout` applet).
# Redirect its fds to /dev/null: otherwise the backgrounded `sleep` keeps the
# caller's stdout pipe open and rpcd/popen blocks on read() for the full 30s
# (the result is ready in ~2s — the watchdog just held the pipe).
( sleep 30; kill -TERM $$ 2>/dev/null ) >/dev/null 2>&1 &
WATCH_PID=$!
# 1. Test-host IPs — BEFORE touching the firewall, so a slow resolver never runs
# while the redirect is modified. Prefer IPs passed in via "$2" (the full-test
# sweep resolves ONCE up front via zapret_resolve.sh and reuses them for every
# candidate); fall back to resolving here for a standalone call.
IPSET=""
if [ -n "$IPS_ARG" ]; then
for kv in $IPS_ARG; do
tag=${kv%%=*}; ip=${kv#*=}
[ -n "$tag" ] && echo "$ip" > "$TMP/ip_$tag"
[ -n "$ip" ] && IPSET="$IPSET$ip,"
done
else
for h in $HOSTS; do
tag=${h%%|*}; host=${h##*|}
ip=$(nslookup "$host" 2>/dev/null | grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' | grep -vE '^127\.' | tail -1)
echo "$ip" > "$TMP/ip_$tag"
[ -n "$ip" ] && IPSET="$IPSET$ip,"
done
fi
IPSET=$(echo "$IPSET" | sed 's/[[:space:]]//g; s/,$//')
[ -z "$IPSET" ] && fail "could not resolve any test host"
# 2. Surgically send ONLY the test IPs' TCP/443 direct (return from output_redir,
# so router-origin curl egresses direct where our temp queue can catch it).
# Everything else stays redirected. Tagged with a comment for clean teardown.
if nft list chain inet fw4 homeproxy_output_redir >/dev/null 2>&1; then
nft insert rule inet fw4 homeproxy_output_redir ip daddr "{ $IPSET }" tcp dport 443 counter return comment "\"$CMT\"" 2>/dev/null
fi
# 3. Temp table: queue test-IP TCP/443 handshakes to QNUM (exclude nfqws's own
# reinjected DMARK fakes so they aren't re-queued); notrack those fakes.
nft add table inet "$TABLE" 2>/dev/null
nft add chain inet "$TABLE" pre "{ type filter hook postrouting priority mangle; policy accept; }" 2>/dev/null
nft add rule inet "$TABLE" pre "meta mark and $DMARK == 0 ip daddr { $IPSET } tcp dport 443 ct original packets 1-12 counter queue num $QNUM bypass" 2>/dev/null
nft add chain inet "$TABLE" out "{ type filter hook output priority -150; policy accept; }" 2>/dev/null
nft add rule inet "$TABLE" out "meta mark and $DMARK != 0 notrack" 2>/dev/null
# 4. Start the candidate nfqws2 on the temp queue.
$NFQWS --qnum=$QNUM --user=daemon --fwmark=$DMARK $LUA $STRAT >/dev/null 2>&1 &
NFQ_PID=$!
sleep 1
pgrep -f "qnum=$QNUM" >/dev/null 2>&1 || fail "nfqws2 did not start - check strategy"
# 5. Probe every host in parallel: curl forced to the resolved IP (keeps SNI),
# write "time_appconnect curl_exit". Wait ONLY on the curl jobs — a bare `wait`
# would also block on the 30s watchdog child.
CPIDS=""
for h in $HOSTS; do
tag=${h%%|*}; host=${h##*|}
ip=$(cat "$TMP/ip_$tag" 2>/dev/null)
if [ -z "$ip" ]; then echo "0 6" > "$TMP/r_$tag"; continue; fi
( out=$(curl -s --resolve "${host}:443:${ip}" -o /dev/null -w '%{time_appconnect}' --connect-timeout 4 --max-time 10 "https://${host}" 2>/dev/null); echo "$out $?" > "$TMP/r_$tag" ) >/dev/null 2>&1 &
CPIDS="$CPIDS $!"
done
[ -n "$CPIDS" ] && wait $CPIDS
# 6. Build JSON.
JSON=""; OKC=0; TOT=0
for h in $HOSTS; do
tag=${h%%|*}; rest=${h#*|}; label=${rest%%|*}; host=${rest##*|}
ip=$(cat "$TMP/ip_$tag" 2>/dev/null)
raw=$(cat "$TMP/r_$tag" 2>/dev/null); tls=${raw%% *}; rc=${raw##* }
TOT=$((TOT+1))
ok=0; case "$tls" in 0|0.000000|"") ok=0;; *) ok=1;; esac
[ "$ok" = 1 ] && OKC=$((OKC+1))
reason=""
if [ "$ok" = 0 ]; then
case "$rc" in 6) reason=dns;; 7) reason=refused;; 28) reason=timeout;; 35|51|53|56|58|59|60) reason=tls;; *) reason=fail;; esac
fi
[ -n "$JSON" ] && JSON="$JSON,"
JSON="$JSON{\"tag\":\"$tag\",\"label\":\"$label\",\"host\":\"$host\",\"ip\":\"$ip\",\"tls\":\"$tls\",\"ok\":$ok,\"reason\":\"$reason\"}"
done
echo "{\"ok\":$OKC,\"total\":$TOT,\"results\":[$JSON]}"
@@ -0,0 +1,185 @@
{
"version": 1,
"candidates": [
{
"name": "Default (fake + multidisorder)",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld",
"group": "recommended"
},
{
"name": "Multisplit",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multisplit:pos=1,midsld",
"group": "recommended"
},
{
"name": "Fake repeats (x6)",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:repeats=6 --lua-desync=multidisorder:pos=1,midsld",
"group": "recommended"
},
{
"name": "Fake only",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5",
"group": "recommended"
},
{
"name": "YouTube (Google seqovl + fake-QUIC)",
"args": "--blob=tls_google:@/opt/zapret2/files/fake/tls_clienthello_www_google_com.bin --blob=quic_google:@/opt/zapret2/files/fake/quic_initial_www_google_com.bin --filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=2,sniext+1:seqovl=679:seqovl_pattern=tls_google --new --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=fake:blob=quic_google:repeats=6",
"group": "recommended"
},
{
"name": "Discord (TLS seqovl + STUN voice)",
"args": "--blob=tls_google:@/opt/zapret2/files/fake/tls_clienthello_www_google_com.bin --blob=stun_fake:@/opt/zapret2/files/fake/stun.bin --filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multidisorder:pos=1,sniext+1:seqovl=336:seqovl_pattern=tls_google --new --filter-udp=443 --filter-l7=stun,discord --lua-desync=fake:blob=stun_fake:repeats=6",
"group": "recommended"
},
{
"name": "YouTube + Discord (combined)",
"args": "--blob=tls_google:@/opt/zapret2/files/fake/tls_clienthello_www_google_com.bin --blob=quic_google:@/opt/zapret2/files/fake/quic_initial_www_google_com.bin --blob=stun_fake:@/opt/zapret2/files/fake/stun.bin --filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=2,sniext+1:seqovl=679:seqovl_pattern=tls_google --new --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=fake:blob=quic_google:repeats=6 --new --filter-udp=443 --filter-l7=stun,discord --lua-desync=fake:blob=stun_fake:repeats=6",
"group": "recommended"
},
{
"name": "Fake TLS auto (rnd SNI)",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_seq=-10000:tls_mod=rnd,dupsid:repeats=11 --lua-desync=multidisorder:pos=1,midsld",
"group": "recommended"
},
{
"name": "Fake + multisplit seqovl",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_seq=-10000:repeats=8 --lua-desync=multisplit:pos=1:seqovl=679:seqovl_pattern=fake_default_tls",
"group": "recommended"
},
{
"name": "Hostfakesplit",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=hostfakesplit:host=www.google.com:tcp_ts=-1000:repeats=4",
"group": "recommended"
},
{
"name": "Syndata + multidisorder",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=syndata --lua-desync=multidisorder:pos=1,midsld",
"group": "recommended"
},
{
"name": "Split @2",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=2",
"group": "auto"
},
{
"name": "Split @1,midsld",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld",
"group": "auto"
},
{
"name": "Split @sniext+1",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=sniext+1",
"group": "auto"
},
{
"name": "Split @midsld",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=midsld",
"group": "auto"
},
{
"name": "Split multi-pos (7pt)",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=1,sniext+1,host+1,midsld-2,midsld,midsld+2,endhost-1",
"group": "auto"
},
{
"name": "Disorder @1",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multidisorder:pos=1",
"group": "auto"
},
{
"name": "Disorder @sniext+1",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multidisorder:pos=sniext+1",
"group": "auto"
},
{
"name": "Disorder @midsld",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multidisorder:pos=midsld",
"group": "auto"
},
{
"name": "Seqovl split @10",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=10:seqovl=1",
"group": "auto"
},
{
"name": "Seqovl split @10,sniext+1",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:pos=10,sniext+1:seqovl=1",
"group": "auto"
},
{
"name": "Seqovl disorder @2",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multidisorder:pos=2:seqovl=1",
"group": "auto"
},
{
"name": "Seqovl disorder @midsld",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multidisorder:pos=midsld:seqovl=1",
"group": "auto"
},
{
"name": "Tcpseg seqovl + drop",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=tcpseg:pos=0,-1:seqovl=1 --lua-desync=drop",
"group": "auto"
},
{
"name": "Fake TLS + badsum",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:badsum",
"group": "auto"
},
{
"name": "Fake TLS + bad-seq",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_seq=-3000",
"group": "auto"
},
{
"name": "Fake TLS + bad-ack/ts",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_ack=-66000:tcp_ts_up",
"group": "auto"
},
{
"name": "Fake TLS + bad-ts",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_ts=-1000",
"group": "auto"
},
{
"name": "Fake TLS + SYN flag",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_flags_set=SYN",
"group": "auto"
},
{
"name": "Fake zero-payload + md5",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=0x00000000:tcp_md5",
"group": "auto"
},
{
"name": "Fake md5 + split @2",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5 --lua-desync=multisplit:pos=2",
"group": "auto"
},
{
"name": "Fake badsum + disorder @midsld",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:badsum --lua-desync=multidisorder:pos=midsld",
"group": "auto"
},
{
"name": "Fake bad-seq + disorder @1,midsld",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_seq=-3000 --lua-desync=multidisorder:pos=1,midsld",
"group": "auto"
},
{
"name": "Fake-in-split @2 (nodrop)",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=multisplit:blob=fake_default_tls:tcp_md5:pos=2:nodrop",
"group": "auto"
},
{
"name": "Wssize + split @1,midsld",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=wssize:wsize=1:scale=6 --lua-desync=multisplit:pos=1,midsld",
"group": "auto"
},
{
"name": "Syndata (TLS marker)",
"args": "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=syndata:blob=0x1603",
"group": "auto"
}
]
}
+332
View File
@@ -0,0 +1,332 @@
#!/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_HIDDIFY="/usr/bin/hiddify-core"
PROG_SINGBOX="/usr/bin/sing-box"
HP_DIR="/etc/homeproxy"
RUN_DIR="/var/run/homeproxy"
LOG_PATH="$RUN_DIR/homeproxy.log"
# 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"
}
start_service() {
mkdir -p "$RUN_DIR"
# Self-heal a missing/empty config: /etc/config/homeproxy is a conffile, so a manual
# delete (or a wiped overlay) leaves the service with nothing and it won't regenerate.
# Restore defaults from the packaged backup. Only when there are no homeproxy sections,
# so a populated (even if odd) user config is never overwritten.
if ! grep -q "config homeproxy" "/etc/config/$CONF" 2>/dev/null; then
if [ -f "/usr/share/homeproxy/homeproxy.default" ]; then
cp "/usr/share/homeproxy/homeproxy.default" "/etc/config/$CONF"
log "config /etc/config/$CONF was missing/empty — restored defaults."
fi
fi
config_load "$CONF"
local preferred_core
config_get preferred_core "config" "preferred_core" "auto"
# Try the preferred core first; fall back to whichever is installed
case "$preferred_core" in
hiddify)
if [ -x "$PROG_HIDDIFY" ]; then
PROG="$PROG_HIDDIFY"; RUN_CMD="srun"; CORE_TYPE="hiddify"
fi
;;
singbox)
if [ -x "$PROG_SINGBOX" ]; then
PROG="$PROG_SINGBOX"; RUN_CMD="run"; CORE_TYPE="singbox"
fi
;;
esac
if [ -z "$PROG" ]; then
if [ -x "$PROG_HIDDIFY" ]; then
PROG="$PROG_HIDDIFY"; RUN_CMD="srun"; CORE_TYPE="hiddify"
elif [ -x "$PROG_SINGBOX" ]; then
PROG="$PROG_SINGBOX"; RUN_CMD="run"; CORE_TYPE="singbox"
else
log "Error: No supported core found. Install hiddify-core or sing-box first."
return 1
fi
fi
local routing_mode proxy_mode
config_get routing_mode "config" "routing_mode" "proxy_banned_ru"
config_get proxy_mode "config" "proxy_mode" "redirect_tproxy"
# Write core info for LuCI
local core_version
core_version="$("$PROG" version 2>/dev/null | head -1 | awk '{print $3}')"
printf '{"core_type":"%s","version":"%s"}\n' "$CORE_TYPE" "$core_version" > "$RUN_DIR/core.info"
# Generate or copy client config
if [ "$routing_mode" = "custom_json" ]; then
if [ -e "$HP_DIR/hiddify-c.json" ]; then
cp "$HP_DIR/hiddify-c.json" "$RUN_DIR/hiddify-c.json"
elif [ ! -e "$RUN_DIR/hiddify-c.json" ]; then
log "Error: client config not found. Place your config at $HP_DIR/hiddify-c.json"
return 1
fi
else
rm -f "$RUN_DIR/hiddify-c.json"
ucode "$HP_DIR/scripts/generate_client.uc"
if [ ! -e "$RUN_DIR/hiddify-c.json" ]; then
log "Error: failed to generate client config."
return 1
fi
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_cn"|"bypass_ir"|"custom"|"custom_json"|"global"|"proxy_banned_ru")
# All modes do DNS inside sing-box; dnsmasq just redirects to its inbound.
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
no-poll
no-resolv
server=127.0.0.1#$dns_port
EOF
;;
esac
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
# Setup tproxy routing table
local table_mark tproxy_mark
config_get table_mark "infra" "table_mark" "100"
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
# hiddify-core (client)
procd_open_instance "hiddify-c"
procd_set_param command "$PROG"
procd_append_param command $RUN_CMD --config "$RUN_DIR/hiddify-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"
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
# log-cleaner
procd_open_instance "log-cleaner"
procd_set_param command "$HP_DIR/scripts/clean_log.sh"
procd_set_param respawn
procd_close_instance
echo > "$RUN_DIR/hiddify-c.log"
# Setup firewall
# Strip CR: a CRLF-tainted template (e.g. transferred from Windows) would otherwise
# emit CRLF into the nft file, which nft rejects wholesale — silently dropping every
# homeproxy chain since fw4 loads all-or-nothing.
ucode "$HP_DIR/scripts/firewall_pre.uc"
utpl -S "$HP_DIR/scripts/firewall_post.ut" | tr -d '\r' > "$RUN_DIR/fw4_post.nft"
fw4 reload >"/dev/null" 2>&1
# ByeDPI
local byedpi_enabled byedpi_cmd_opts byedpi_block_quic byedpi_gid
config_get_bool byedpi_enabled "config" "byedpi_enabled" "0"
if [ "$byedpi_enabled" = "1" ] && [ -x "/usr/bin/ciadpi" ]; then
config_get byedpi_cmd_opts "config" "byedpi_cmd_opts" "--disorder 1"
config_get byedpi_gid "infra" "byedpi_gid" "8181"
/etc/init.d/ciadpi stop 2>/dev/null; /etc/init.d/ciadpi disable 2>/dev/null; true
# ByeDPI runs as root (needed for TCP_MD5SIG and other desync) but under a dedicated
# group, so its egress can be excluded from the proxy redirect by socket group (skgid).
# ciadpi has no fwmark option, and without this exclusion its outbound connections
# loop back through sing-box (conntrack table full / packet flood).
if ! grep -q "^byedpi:" /etc/group; then
echo "byedpi:x:${byedpi_gid}:" >> /etc/group
fi
procd_open_instance "byedpi"
procd_set_param command /usr/bin/ciadpi -i 127.0.0.1 -p 5335
# Quote-aware split so an argument value containing spaces survives intact —
# e.g. an inline hostlist -H:"host1 host2 host3" must stay a single argument,
# not be torn into separate tokens by plain word-splitting. eval re-parses the
# quoting; the string is the admin's own root-owned UCI option, so this is
# argument parsing, not a trust boundary. The leading -- protects against an
# option-like first token.
eval "set -- $byedpi_cmd_opts"
for _arg in "$@"; do
procd_append_param command "$_arg"
done
procd_set_param group "byedpi"
procd_set_param respawn
procd_close_instance
config_get_bool byedpi_block_quic "config" "byedpi_block_quic" "0"
if [ "$byedpi_block_quic" = "1" ]; then
nft 'add chain inet fw4 homeproxy_quic { type filter hook prerouting priority -150; policy accept; }' 2>/dev/null || true
nft 'add rule inet fw4 homeproxy_quic ip daddr { 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 } return' 2>/dev/null || true
nft 'add rule inet fw4 homeproxy_quic udp dport 443 drop' 2>/dev/null || true
fi
fi
# Zapret (nfqws2): HomeProxy runs its OWN nfqws2 on a dedicated NFQUEUE. Which flows
# reach the queue is decided upstream by sing-box (zapret-out routing_mark) + nft
# (firewall_post.ut), so nfqws2 runs WITHOUT hostlists — it desyncs whatever it's fed.
# The zapret2 package only supplies the binary + lua; its own service/firewall stays
# disabled so it can't fight us for the queue or install conflicting nft rules.
local zapret_enabled zapret_cmd_opts zapret_qnum zapret_default_opts
config_get_bool zapret_enabled "config" "zapret_enabled" "0"
if [ "$zapret_enabled" = "1" ] && [ -x "/opt/zapret2/nfq2/nfqws2" ]; then
config_get zapret_qnum "config" "zapret_qnum" "200"
# Default strategy = zapret2's shipped recipe minus hostlists, TCP http+tls only.
# The strategy is the single source of truth: if it has a --filter-udp profile,
# firewall_post.ut queues QUIC to it automatically; if not, QUIC is left alone.
zapret_default_opts="--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld"
config_get zapret_cmd_opts "config" "zapret_cmd_opts" "$zapret_default_opts"
/etc/init.d/zapret2 stop 2>/dev/null; /etc/init.d/zapret2 disable 2>/dev/null; true
procd_open_instance "zapret"
# Fixed args: drop privileges to daemon, tag reinjected fakes with DESYNC_MARK
# 0x40000000 (firewall_post.ut lets that mark escape the redirect and not be
# re-queued), and load the lua desync library (nfqws2 reads the shipped .gz directly).
procd_set_param command /opt/zapret2/nfq2/nfqws2 \
--qnum="$zapret_qnum" \
--user=daemon \
--fwmark=0x40000000 \
--lua-init=@/opt/zapret2/lua/zapret-lib.lua \
--lua-init=@/opt/zapret2/lua/zapret-antidpi.lua \
--lua-init=@/opt/zapret2/lua/zapret-auto.lua
# Strategy opts: quote-aware split so a value with spaces survives intact. The string
# is the admin's own root-owned UCI option (argument parsing, not a trust boundary);
# leading -- guards an option-like first token. Same rationale as ByeDPI above.
eval "set -- $zapret_cmd_opts"
for _arg in "$@"; do
procd_append_param command "$_arg"
done
procd_set_param respawn
procd_close_instance
fi
# Subscription auto-update cron. This install half was dropped in the init rewrite
# (commit 4f3511f) while the stop_service cleanup below was kept, leaving the UI
# "Auto update" checkbox wired to nothing. Restored verbatim from upstream: when
# enabled, run update_crond.sh daily at the chosen hour (refreshes subscriptions and,
# in bypass_mainland_china mode, the China resources). The #${CONF}_autosetup marker
# lets stop_service remove only our line without touching user cron jobs.
local auto_update auto_update_time
config_get_bool auto_update "subscription" "auto_update" "0"
if [ "$auto_update" = "1" ]; then
config_get auto_update_time "subscription" "auto_update_time" "2"
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null"
echo -e "0 $auto_update_time * * * $HP_DIR/scripts/update_crond.sh #${CONF}_autosetup" >> "/etc/crontabs/root"
/etc/init.d/cron restart >"/dev/null" 2>&1
fi
log "$(basename $PROG) started."
}
stop_service() {
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null"
/etc/init.d/cron restart >"/dev/null" 2>&1
# Load config
config_load "$CONF"
local table_mark tproxy_mark tun_name
config_get table_mark "infra" "table_mark" "100"
config_get tproxy_mark "infra" "tproxy_mark" "101"
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"
# 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" \
"homeproxy_quic"; 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_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
# Only remove the log; hiddify-c.json is manually placed by the user
rm -f "$RUN_DIR/hiddify-c.log"
log "Service stopped."
}
service_stopped() {
config_load "$CONF"
local tun_name
config_get tun_name "infra" "tun_name" "singtun0"
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
}
@@ -0,0 +1,31 @@
#!/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
@@ -0,0 +1,5 @@
#!/bin/sh
ucode "/etc/homeproxy/scripts/migrate_config.uc"
exit 0
@@ -0,0 +1,65 @@
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 sniff_override '1'
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 '8.8.8.8'
option china_dns_server '223.5.5.5'
option routing_mode 'proxy_banned_ru'
option routing_port 'all'
option proxy_mode 'redirect_tproxy'
option ipv6_support '0'
option proxy_calls '1'
option no_proxy_torrents '1'
option github_token ''
option log_level 'warn'
config homeproxy 'control'
option lan_proxy_mode 'disabled'
config homeproxy 'routing'
option sniff_override '1'
option default_outbound 'direct-out'
option default_outbound_dns 'default-dns'
config homeproxy 'dns'
option dns_strategy 'prefer_ipv4'
option default_server 'local-dns'
option disable_cache '0'
option disable_cache_expire '0'
config homeproxy 'subscription'
option auto_update '0'
option allow_insecure '0'
option packet_encoding 'xudp'
option update_via_proxy '0'
option filter_nodes 'blacklist'
list filter_keywords '重置|到期|过期|剩余|套餐'
list filter_keywords 'Expiration|Remaining'
config homeproxy 'server'
option enabled '0'
option log_level 'warn'
@@ -0,0 +1,353 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2022-2025 ImmortalWrt.org
*/
'use strict';
import { access, popen, readfile } from 'fs';
function shellquote(s) {
return `'${replace(s, "'", "'\\''")}'`;
}
/* Optional GitHub mirror (set by a provisioning tool or the user as
* uci homeproxy.config.github_mirror). Used ONLY as a FALLBACK: every fetch tries
* GitHub first and only swaps to the mirror if GitHub fails — so a healthy GitHub is
* never bypassed, but a blocked/failed one still gets the file via the mirror. */
function gh_mirror_base() {
let base = null;
const fd = popen('uci -q get homeproxy.config.github_mirror 2>/dev/null');
if (fd) { base = trim(fd.read('all')); fd.close(); }
return (base && length(base)) ? replace(base, /\/+$/, '') : null;
}
function gh_mirror_of(url, base) {
const m = match(url, /^https:\/\/github\.com(\/[^\/]+\/[^\/]+\/releases\/.+)$/);
return m ? `${base}${m[1]}` : null;
}
/* GitHub-first, mirror-FALLBACK download. Returns wget exit code (0 = success). */
function gh_fetch(url, dest, timeout_ms) {
let rc = system(`wget -qO ${shellquote(dest)} --timeout=15 ${shellquote(url)} 2>/dev/null`, timeout_ms);
if (rc !== 0) {
const base = gh_mirror_base();
const mu = base ? gh_mirror_of(url, base) : null;
if (mu)
rc = system(`wget -qO ${shellquote(dest)} --timeout=15 ${shellquote(mu)} 2>/dev/null`, timeout_ms);
}
return rc;
}
function detect_pkg_manager() {
for (let p in ['/usr/bin/apk', '/sbin/apk', '/usr/sbin/apk'])
if (access(p)) return 'apk';
for (let p in ['/bin/opkg', '/usr/bin/opkg'])
if (access(p)) return 'opkg';
return null;
}
function detect_arch() {
const os_rel = readfile('/etc/os-release') || '';
const m = match(os_rel, /OPENWRT_ARCH="([^"]+)"/) ||
match(os_rel, /OPENWRT_ARCH=([^\n]+)/);
return m ? trim(m[1]) : '';
}
function free_kb(path) {
const fd = popen(`df -k ${path} 2>/dev/null | awk 'NR==2{print $4}'`);
if (!fd) return 0;
const v = int(trim(fd.read('all'))); fd.close();
return v || 0;
}
function free_ram_kb() {
const m = match(readfile('/proc/meminfo') || '', /MemAvailable:\s+([0-9]+)/);
return m ? int(m[1]) : 0;
}
/* Does the overlay filesystem transparently compress? jffs2/ubifs do (~3x on a Go binary),
* ext4/f2fs do not. This decides how much flash the FULL binary actually occupies. */
function overlay_compresses() {
const fd = popen("mount 2>/dev/null | awk '$3==\"/overlay\"{print $5; exit}'");
let t = '';
if (fd) { t = trim(fd.read('all')); fd.close(); }
return (t in ['jffs2', 'ubifs']);
}
/* Footprint thresholds (KB). The UPX/compact build is already compressed (~20 MB on ANY fs)
* but decompresses the whole binary into RAM at every launch (trades flash for RAM). The full
* Go binary is ~76 MB raw → ~76 MB on ext4/f2fs, but only ~26 MB on a compressing overlay. */
const FULL_OVERLAY_RAW_KB = 81920; /* ~80 MB free for the full binary on an uncompressed overlay */
const FULL_OVERLAY_COMP_KB = 32768; /* ~32 MB on a compressing overlay (binary stores ~3x smaller) */
const COMPACT_OVERLAY_KB = 25600; /* ~25 MB for the compact (UPX) build — fs-independent */
const COMPACT_RAM_KB = 98304; /* ~96 MB free RAM to decompress + run the compact build */
const action = ARGV[0];
let result;
if (action === 'info') {
const pkg_manager = detect_pkg_manager();
const arch = detect_arch();
const tmp_free_kb = free_kb('/tmp');
let overlay_free_kb = free_kb('/overlay');
if (!overlay_free_kb) overlay_free_kb = free_kb('/');
const ram_free_kb = free_ram_kb();
const hiddify_installed = !!access('/usr/bin/hiddify-core');
let hiddify_version = null;
if (hiddify_installed) {
const fd = popen('/usr/bin/hiddify-core version 2>/dev/null');
if (fd) {
const out = fd.read('all'); fd.close();
const m = match(out, /version v?(\S+)/);
if (m) hiddify_version = m[1];
}
}
const singbox_installed = !!access('/usr/bin/sing-box');
let singbox_version = null;
let singbox_extended = false;
if (singbox_installed) {
const fd = popen('/usr/bin/sing-box version 2>/dev/null');
if (fd) {
const out = fd.read('all'); fd.close();
const m = match(out, /version v?(\S+)/);
if (m) singbox_version = m[1];
singbox_extended = !!match(out, /amneziawg|with_amnezia/);
}
}
result = {
pkg_manager, arch, tmp_free_kb, overlay_free_kb, ram_free_kb,
hiddify: { installed: hiddify_installed, version: hiddify_version },
singbox: { installed: singbox_installed, version: singbox_version, extended: singbox_extended }
};
} else if (action === 'check_remote') {
const core = ARGV[1];
if (!(core in ['hiddify', 'singbox'])) {
result = { error: 'illegal core' };
} else {
const api_url = core === 'hiddify'
? 'https://api.github.com/repos/1andrevich/hiddify-core/releases/latest'
: 'https://api.github.com/repos/shtorm-7/sing-box-extended/releases/latest';
const fd = popen('wget -qO- --timeout=10 ' + shellquote(api_url) + ' 2>/dev/null');
if (!fd) {
result = { error: 'wget failed' };
} else {
const raw = trim(fd.read('all')); fd.close();
if (!length(raw)) {
result = { error: 'no response from GitHub API' };
} else {
let data;
try { data = json(raw); } catch(e) { data = null; }
if (!data)
result = { error: 'invalid JSON from GitHub API' };
else if (!data?.tag_name)
result = { error: 'could not read tag_name from response' };
else
result = { tag: data.tag_name, version: replace(data.tag_name, /^v/, '') };
}
}
}
} else if (action === 'prepare_install') {
const core = ARGV[1];
const variant = ARGV[2] || ''; /* 'upx' = the compressed hiddify-core build (small flash, decompresses into RAM at launch) */
if (!(core in ['hiddify', 'singbox'])) {
result = { error: 'illegal core' };
} else {
const pkg_manager = detect_pkg_manager();
if (!pkg_manager) {
result = { error: 'no supported package manager found (apk or opkg)' };
} else {
const arch = detect_arch();
if (!arch || !match(arch, /^[a-zA-Z0-9_-]+$/)) {
result = { error: 'could not detect device architecture' };
} else {
const tmp_free_kb = free_kb('/tmp');
if (tmp_free_kb < 30720) {
result = { error: `not enough /tmp space: ${tmp_free_kb} KB free, need 30 MB` };
} else {
let overlay_free_kb = free_kb('/overlay');
if (!overlay_free_kb) overlay_free_kb = free_kb('/');
const ram_free_kb = free_ram_kb();
const ext = pkg_manager === 'apk' ? '.apk' : '.ipk';
/* full-binary footprint depends on whether the overlay compresses */
const full_need = overlay_compresses() ? FULL_OVERLAY_COMP_KB : FULL_OVERLAY_RAW_KB;
if (core === 'hiddify') {
/* Auto-pick the build from free overlay (+ free RAM for the compact one).
* ARGV[2] ('standard'|'upx') is an advanced override; size-checked either way.
* This is the guardrail for the "full binary truncates on a small overlay →
* SIGBUS" trap: never offer a build the device can't actually hold/run. */
let chosen = variant;
let note = null;
/* UPX-compressed binaries are crash-prone (SIGILL on startup) on these
* soft-float MIPS targets, so never AUTO-pick the compact build there —
* fall back to standard, or error cleanly. An explicit variant='upx'
* override is still honored (advanced users). Non-MIPS arches (incl.
* aarch64 like MT7622) are unaffected. */
const mips_no_auto_upx = (arch === 'mipsel_24kc' || arch === 'mips_24kc');
if (chosen !== 'standard' && chosen !== 'upx') {
if (overlay_free_kb >= full_need)
chosen = 'standard';
else if (mips_no_auto_upx)
chosen = null; /* don't squeeze a crash-prone compact build onto tight-flash MIPS */
else if (overlay_free_kb >= COMPACT_OVERLAY_KB && ram_free_kb >= COMPACT_RAM_KB) {
chosen = 'upx';
note = 'Limited storage — installing the compact build (decompresses into RAM at launch).';
} else
chosen = null;
}
if (!chosen) {
result = mips_no_auto_upx
? { error: `Not enough overlay for the full build: ${overlay_free_kb} KB free, need ~${full_need} KB. The compact build is not offered on MIPS (UPX is unreliable there). Free up space, or bake the core into a custom firmware image.` }
: (overlay_free_kb >= COMPACT_OVERLAY_KB)
? { error: `Device too constrained: ${overlay_free_kb} KB free overlay (full build needs ~${full_need}) and only ${ram_free_kb} KB free RAM (compact build needs ~${COMPACT_RAM_KB}). Use sing-box-extended instead.` }
: { error: `Not enough storage: ${overlay_free_kb} KB free overlay, need at least ~${COMPACT_OVERLAY_KB} KB (~25 MB).` };
} else {
const need = (chosen === 'upx') ? COMPACT_OVERLAY_KB : full_need;
if (overlay_free_kb < need) {
result = { error: `Not enough overlay space for the ${chosen === 'upx' ? 'compact' : 'full'} build: ${overlay_free_kb} KB free, need ~${need} KB.` };
} else {
const hp_path = (chosen === 'upx') ? 'download/upx' : 'latest/download';
result = {
pkg_manager, arch, variant: chosen, note,
dl_url: `https://github.com/1andrevich/hiddify-core/releases/${hp_path}/hiddify-core_${arch}${ext}`,
tmp_path: `/tmp/hiddify-core${ext}`
};
}
}
} else if (overlay_free_kb < full_need) {
result = { error: `Not enough overlay space: ${overlay_free_kb} KB free, sing-box-extended needs ~${full_need} KB.` };
} else {
const api_fd = popen('wget -qO- --timeout=10 https://api.github.com/repos/shtorm-7/sing-box-extended/releases/latest 2>/dev/null');
if (!api_fd) {
result = { error: 'failed to contact GitHub API' };
} else {
const api_raw = trim(api_fd.read('all')); api_fd.close();
let api_data;
try { api_data = json(api_raw); } catch(e) { api_data = null; }
if (!api_data?.tag_name) {
result = { error: 'could not determine latest version from GitHub' };
} else {
let dl_url = null;
for (let asset in (api_data?.assets || [])) {
const n = asset?.name || '';
if (!match(n, /openwrt/)) continue;
if (length(split(n, arch)) < 2) continue;
if (ext === '.apk' && !match(n, /\.apk$/)) continue;
if (ext === '.ipk' && !match(n, /\.ipk$/)) continue;
dl_url = asset?.browser_download_url;
break;
}
if (!dl_url)
result = { error: `no package found for arch ${arch} in latest release` };
else
result = { pkg_manager, arch, dl_url, tmp_path: `/tmp/sing-box-extended${ext}` };
}
}
}
}
}
}
}
} else if (action === 'download_pkg') {
/* UI calls (url, tmp_path). Also tolerate an optional leading core arg
* (download_pkg <core> <url> <tmp_path>) so callers that mirror the
* prepare_install/install_pkg core-first signature don't silently fail. */
let url = ARGV[1];
let tmp_path = ARGV[2];
if (ARGV[3] && (ARGV[1] in ['hiddify', 'singbox'])) {
url = ARGV[2];
tmp_path = ARGV[3];
}
if (!url || !tmp_path) {
result = { result: false, error: 'missing arguments' };
} else {
const exit_code = gh_fetch(url, tmp_path, 300000); /* GitHub first, mirror fallback */
result = exit_code === 0 ? { result: true } : { result: false, error: 'download failed' };
}
} else if (action === 'install_pkg') {
const core = ARGV[1];
const tmp_path = ARGV[2];
const pkg_manager = ARGV[3];
if (!(core in ['hiddify', 'singbox']) || !tmp_path || !pkg_manager) {
result = { result: false, error: 'invalid arguments' };
} else {
let exit_code;
if (core === 'hiddify' && pkg_manager === 'apk') {
/* Signing key: skip the fetch if already present (a provisioning tool may pre-place
* it); else GitHub-first, mirror-FALLBACK via gh_fetch, best-effort (a
* throttled/blocked GitHub must NOT fail the install — the package already
* arrived over HTTPS). Install trusted if the key is there, else untrusted. */
if (!access('/etc/apk/keys/homeproxy-hiddify.pub')) {
if (gh_fetch('https://github.com/1andrevich/homeproxy-hiddify/releases/latest/download/homeproxy-hiddify.pub', '/tmp/homeproxy-hiddify.pub', 20000) === 0)
system('[ -s /tmp/homeproxy-hiddify.pub ] && cp /tmp/homeproxy-hiddify.pub /etc/apk/keys/ 2>/dev/null; rm -f /tmp/homeproxy-hiddify.pub');
}
if (access('/etc/apk/keys/homeproxy-hiddify.pub'))
exit_code = system(`apk add ${shellquote(tmp_path)} >/dev/null 2>&1; RC=$?; rm -f ${shellquote(tmp_path)}; exit $RC`, 120000);
else
exit_code = system(`apk add --allow-untrusted ${shellquote(tmp_path)} >/dev/null 2>&1; RC=$?; rm -f ${shellquote(tmp_path)}; exit $RC`, 120000);
} else if (pkg_manager === 'apk') {
/* sing-box-extended has no signing key — allow-untrusted is unavoidable */
exit_code = system(
`{ apk add --allow-untrusted ${shellquote(tmp_path)}; } >/dev/null 2>&1` +
`; RC=$?; rm -f ${shellquote(tmp_path)}; exit $RC`,
120000
);
} else {
exit_code = system(
`{ opkg install --force-reinstall ${shellquote(tmp_path)}; } >/dev/null 2>&1` +
`; RC=$?; rm -f ${shellquote(tmp_path)}; exit $RC`,
120000
);
}
result = exit_code === 0 ? { result: true } : { result: false, error: 'package installation failed' };
}
} else if (action === 'install_kmods') {
const pkg_manager = ARGV[1] || detect_pkg_manager();
let exit_code = 1;
/* No --no-cache: it forces apk to refetch the package index over the network
* (slow / >60s on poor uplinks, and the index is usually already cached from the
* app install). The kmods are tiny and resolve from the local index in seconds. */
if (pkg_manager === 'apk')
exit_code = system('apk add kmod-nft-tproxy kmod-tun >/dev/null 2>&1', 120000);
else if (pkg_manager === 'opkg')
exit_code = system('opkg install kmod-nft-tproxy kmod-tun >/dev/null 2>&1', 60000);
result = (exit_code === 0)
? { result: true }
: { result: false, error: `kmod install failed (pkg_manager=${pkg_manager || 'none'})` };
} else if (action === 'remove') {
const core = ARGV[1];
if (!(core in ['hiddify', 'singbox'])) {
result = { result: false, error: 'illegal core' };
} else {
const pkg_manager = detect_pkg_manager();
if (!pkg_manager) {
result = { result: false, error: 'no supported package manager found' };
} else {
const pkg_name = core === 'hiddify' ? 'hiddify-core' : 'sing-box-extended';
const exit_code = pkg_manager === 'apk'
? system(`apk del ${pkg_name} >/dev/null 2>&1`, 30000)
: system(`opkg remove ${pkg_name} >/dev/null 2>&1`, 30000);
result = { result: (exit_code === 0) };
}
}
} else {
result = { error: `unknown action: ${action}` };
}
printf('%s\n', result);
@@ -0,0 +1,120 @@
#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Headless import of a WireGuard / AmneziaWG `.conf` file into a homeproxy
* `node` section. This is the SSH/agent-callable equivalent of the browser-only
* `parseWireGuardConf()` + "Import .conf" button in the LuCI node view — kept
* field-for-field in sync with it.
*
* Usage: ucode import_conf.uc <path-to-.conf> [label]
* Output: one JSON line, e.g. { "result": true, "section": "awg…", "label": "…", "type": "amneziawg" }
* or { "error": "…" } with a non-zero exit code.
*/
'use strict';
import { readfile } from 'fs';
import { cursor } from 'uci';
const uciconfig = 'homeproxy';
function emit(o) { printf('%s\n', o); }
/* Dependency-free stable hash (FNV-1a, 32-bit) for an idempotent section name.
* Deliberately NOT using `digest`/md5: ucode-mod-digest is absent on OpenWrt 23.05
* (the legacy build strips it), and that target's build patch only rewrites
* update_subscriptions.uc — not this script. Pure ucode works everywhere. */
function strhash(s) {
let h = 2166136261;
const n = length(s);
for (let i = 0; i < n; i++) {
h = (h ^ ord(s, i)) & 0xFFFFFFFF;
h = (h * 16777619) & 0xFFFFFFFF;
}
return sprintf('%08x', h);
}
const path = ARGV[0];
const want_label = ARGV[1];
if (!path) {
emit({ error: 'usage: import_conf.uc <path-to-.conf> [label]' });
exit(1);
}
const text = readfile(path);
if (!text) {
emit({ error: `cannot read file: ${path}` });
exit(1);
}
/* ---- parse (mirror of parseWireGuardConf in node.js) ---- */
const rawlines = split(text, '\n');
let section = null;
let iface = {}, peer = {};
for (let i = 0; i < length(rawlines); i++) {
let line = trim(rawlines[i]);
if (!length(line) || substr(line, 0, 1) === '#')
continue;
if (line === '[Interface]') { section = 'interface'; continue; }
if (line === '[Peer]') { section = 'peer'; continue; }
let eq = index(line, '=');
if (eq < 0)
continue;
let key = trim(substr(line, 0, eq));
let val = trim(substr(line, eq + 1));
if (section === 'interface') iface[key] = val;
else if (section === 'peer') peer[key] = val;
}
if (!iface.PrivateKey || !peer.PublicKey || !peer.Endpoint) {
emit({ error: 'not a valid WireGuard/AmneziaWG .conf (missing PrivateKey / PublicKey / Endpoint)' });
exit(1);
}
/* split Endpoint host:port on the LAST colon (IPv6 literal safe) */
const ep = peer.Endpoint;
const lc = rindex(ep, ':');
let host = (lc >= 0) ? substr(ep, 0, lc) : ep;
const port = (lc >= 0) ? substr(ep, lc + 1) : '';
host = replace(host, '[', '');
host = replace(host, ']', '');
const isAWG = !!(iface.Jc || iface.Jmin || iface.Jmax || iface.H1);
let node = {
label: want_label || (isAWG ? 'AmneziaWG' : 'WireGuard'),
type: isAWG ? 'amneziawg' : 'wireguard',
address: host,
port: port,
wireguard_private_key: iface.PrivateKey,
wireguard_peer_public_key: peer.PublicKey
};
if (peer.PresharedKey) node.wireguard_pre_shared_key = peer.PresharedKey;
if (iface.Address) node.wireguard_local_address = map(split(iface.Address, ','), (a) => trim(a));
if (iface.MTU) node.wireguard_mtu = iface.MTU;
if (isAWG) {
const awg = {
amnezia_jc: iface.Jc, amnezia_jmin: iface.Jmin, amnezia_jmax: iface.Jmax,
amnezia_s1: iface.S1, amnezia_s2: iface.S2, amnezia_s3: iface.S3, amnezia_s4: iface.S4,
amnezia_h1: iface.H1, amnezia_h2: iface.H2, amnezia_h3: iface.H3, amnezia_h4: iface.H4,
amnezia_i1: iface.I1, amnezia_i2: iface.I2, amnezia_i3: iface.I3, amnezia_i4: iface.I4, amnezia_i5: iface.I5
};
for (let k in awg)
if (awg[k] != null && awg[k] !== '')
node[k] = awg[k];
}
/* ---- write the node section (idempotent: same file → same section name) ---- */
const uci = cursor();
uci.load(uciconfig);
const sid = 'awg' + strhash(text);
uci.set(uciconfig, sid, 'node');
for (let k in node)
uci.set(uciconfig, sid, k, node[k]);
uci.commit(uciconfig);
emit({ result: true, section: sid, label: node.label, type: node.type });
@@ -0,0 +1,53 @@
{
"admin/services/homeproxy": {
"title": "Re:HomeProxy",
"order": 10,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-re-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": 30,
"action": {
"type": "view",
"path": "homeproxy/server"
}
},
"admin/services/homeproxy/status": {
"title": "Core & Tools",
"order": 20,
"action": {
"type": "view",
"path": "homeproxy/status"
}
},
"admin/services/homeproxy/diagnostics": {
"title": "Diagnostics",
"order": 40,
"action": {
"type": "view",
"path": "homeproxy/diagnostics"
}
}
}
@@ -0,0 +1,25 @@
{
"luci-app-re-homeproxy": {
"description": "Grant access to homeproxy configuration",
"read": {
"file": {
"/etc/homeproxy/scripts/update_subscriptions.uc": [ "exec" ],
"/usr/bin/ucode": [ "exec" ],
"/etc/homeproxy/zapret_candidates.json": [ "read" ],
"/var/run/homeproxy/homeproxy.log": [ "read" ],
"/var/run/homeproxy/hiddify-c.log": [ "read" ]
},
"ubus": {
"service": [ "list" ],
"luci.homeproxy": [ "*" ]
},
"uci": [ "homeproxy" ]
},
"write": {
"file": {
"/tmp/homeproxy_certificate.tmp": [ "write" ]
},
"uci": [ "homeproxy" ]
}
}
}
File diff suppressed because it is too large. Load diff
+102
View File
@@ -0,0 +1,102 @@
🇬🇧 [English](ByeDPI-en) | 🇷🇺 [Русский](ByeDPI-ru)
# ByeDPI
**ByeDPI** is a built-in DPI-bypass that can un-throttle and unblock sites **without any VPN subscription**. Re:HomeProxy bundles the [ByeDPI](https://github.com/hufrea/byedpi) engine (`ciadpi`) and manages it for you.
---
## What it is — and what it is not
ByeDPI runs a tiny local proxy that **desyncs the TLS handshake** of your connections (splitting, reordering, fake packets, OOB data, etc.) so that your ISP's Deep Packet Inspection can no longer recognise — and therefore can no longer throttle or block — the traffic.
| | |
|---|---|
| ✅ **Un-throttles DPI-filtered sites** | The common case for YouTube and similar services that load slowly or buffer endlessly under DPI throttling. |
| ✅ **No subscription, no server** | It's a local trick — you don't need a VPN node, account, or any remote server. |
| ❌ **Does not encrypt or hide traffic** | Your ISP still sees *which* sites you visit; ByeDPI only changes *how* the first packets look. It is **not a VPN**. |
| ❌ **Cannot reach fully IP-blocked sites** | If a site is blocked by IP/DNS (not just throttled), only a real proxy/VPN node can reach it. |
| ❌ **Not a tunnel** | ByeDPI carries your traffic but does not resolve DNS like a proxy — see *DNS* below. |
Think of ByeDPI as a complement to a VPN node (offload throttled sites like YouTube to ByeDPI to save VPN bandwidth), or as a standalone tool when the only problem is throttling.
---
## Enabling ByeDPI
1. Go to **Services → Re:HomeProxy → Node Settings** and open the **ByeDPI** section.
2. If `ciadpi` is not installed yet, use the **Install** button — Re:HomeProxy fetches the right `byedpi` package for your architecture.
3. Tick **Enable**, pick a strategy (see below), and **Save & Apply**.
That's all that's needed for the engine. Re:HomeProxy starts `ciadpi` for you and prevents it from looping back through the proxy automatically — you do not need to touch the firewall or process settings.
---
## Strategies and presets
A "strategy" is the set of command-line options ByeDPI uses to mangle the handshake. There is no universal best strategy — **what works depends on your ISP's DPI**. Re:HomeProxy ships **47 ready-made presets** grouped by technique (named by group letter, e.g. `C3`, `I1`):
| Group | Examples | Idea |
|-------|----------|------|
| Disorder | `--disorder 1`, `--disorder 1+s` | Reorder the first segments so DPI can't reassemble the SNI |
| Fake TTL | `--fake -1 --ttl 6…15` | Inject a fake packet that dies (low TTL) before reaching the server |
| Fake MD5 | `--fake -1 --md5sig` | Fake packet carrying an invalid TCP MD5 signature |
| TLS record | `--tlsrec 1+s` | Split at the TLS record layer |
| OOB / DisOOB | `--oob 1+s`, `--disoob 1+s` | Out-of-band byte tricks at the SNI |
| Split | `--split 1+s` | Split the ClientHello at the SNI |
| HTTP mix | `--mod-http hcsmix,…` | Case/format mixing for plain-HTTP hosts |
| **Adaptive** | `--auto=ssl_err`, `--auto=torst` | **Self-correcting** — retries per connection on TLS error / reset |
| Fake SNI/TLS | `--fake-sni`, `--fake-tls-mod` | Send a decoy SNI / TLS fingerprint |
| Aggressive combos | split + OOB + disorder together | When single techniques aren't enough |
You can also pick **Custom** and type your own option string.
### Recommended starting point: preset I1 (adaptive)
**Preset I1 — "Auto SSL Error Fallback"** (`--fake -1 --ttl 8 --auto=ssl_err --fake -1 --ttl 5`) is the safest default, **especially for YouTube**.
Why adaptive matters: a **fixed** fake-TTL strategy (the Fake-TTL group, presets C1–C5) works for *far* servers but corrupts the handshake on *near* CDN/edge servers — e.g. some YouTube video segments served from a local Google cache. A fixed strategy that fails on near servers not only stalls the page, it can produce a storm of failed-handshake retries that **fills the router's connection-tracking table and freezes SSH/LuCI**. The adaptive `--auto=ssl_err` strategy retries per-connection when it sees a TLS error, so it self-corrects across near *and* far destinations.
---
## The strategy tester
Because the right strategy is ISP-specific, the ByeDPI section includes a **tester**:
- **Test all strategies** — probes four reference sites in parallel and shows a result per site as dots:
- **YouTube** and **Telegram** — *far* servers
- **Discord** and **Speedtest.net** — *near* servers
- `●●●●` = all four reachable with that strategy; `●●○○` = far sites work but near sites fail (a sign the strategy is destination-sensitive, like a fixed fake-TTL).
- **Test current** — tests only the strategy currently in the command field and shows full site names with ✓ / ✗.
Pick a strategy that scores **all-green (`●●●●`)** for your network. If several do, prefer an adaptive one (I1/I2).
> The tester runs ByeDPI on a separate temporary port, so it does not disturb live traffic.
---
## Routing traffic through ByeDPI
ByeDPI appears as a selectable node (**ByeDPI**) once enabled. Two common setups:
- **Alongside a VPN node (recommended):** in `proxy_banned_ru` mode, open **RU Proxy Rules** and set a *specific list's* node to **ByeDPI** — e.g. route YouTube through ByeDPI (saving VPN bandwidth) while everything else uses your VPN.
- **Standalone (no VPN):** set ByeDPI as the **main node**. All proxied traffic then goes through the local desync.
---
## DNS
ByeDPI is a desync, not a tunnel, so DNS does **not** go through it. When ByeDPI carries traffic, Re:HomeProxy resolves names **directly** (your configured Secure DNS detours direct-out for ByeDPI). Trying to push DoH/DoT or UDP DNS *through* ByeDPI fails — the handshake mangling corrupts the encrypted DNS session. This is handled automatically; there is nothing to configure.
---
## Troubleshooting
| Symptom | Likely cause / fix |
|---------|--------------------|
| A site still loads slowly | The strategy isn't right for your ISP — run **Test all strategies** and pick an all-green one. |
| YouTube buffers on a fixed fake-TTL preset | Switch to an adaptive preset (**I1/I2**); fixed TTL stalls on near Google cache nodes. |
| Router/LuCI/SSH becomes laggy | A destination-sensitive strategy is causing retry storms — switch to an adaptive preset. |
| A site is unreachable, not just slow | It may be IP-blocked, not throttled — ByeDPI can't help; use a proxy/VPN node for it. |
See also: [Zapret](Zapret-en) · [Core Management](Core-Management-en) · [Supported Protocols](Supported-Protocols-en) · [Troubleshooting](Troubleshooting-en)
+102
View File
@@ -0,0 +1,102 @@
🇬🇧 [English](ByeDPI-en) | 🇷🇺 [Русский](ByeDPI-ru)
# ByeDPI
**ByeDPI** — встроенный обход DPI, который позволяет разблокировать сайты и снять троттлинг **без какой-либо VPN-подписки**. Re:HomeProxy включает движок [ByeDPI](https://github.com/hufrea/byedpi) (`ciadpi`) и управляет им за вас.
---
## Что это — и чем оно не является
ByeDPI запускает крошечный локальный прокси, который **рассинхронизирует TLS-рукопожатие** ваших соединений (разбиение, переупорядочивание, фейковые пакеты, OOB-данные и т. п.), чтобы система DPI провайдера больше не распознавала трафик — а значит, не могла его замедлять или блокировать.
| | |
|---|---|
| ✅ **Снимает троттлинг сайтов под DPI** | Типичный случай — YouTube и подобные сервисы, которые медленно грузятся или бесконечно буферизуются из-за DPI-троттлинга. |
| ✅ **Без подписки и сервера** | Это локальный трюк — не нужен VPN-узел, аккаунт или удалённый сервер. |
| ❌ **Не шифрует и не скрывает трафик** | Провайдер по-прежнему видит, *какие* сайты вы посещаете; ByeDPI меняет только то, *как выглядят* первые пакеты. Это **не VPN**. |
| ❌ **Не открывает сайты, заблокированные по IP** | Если сайт блокируется по IP/DNS (а не просто троттлится), достучаться до него может только настоящий прокси/VPN-узел. |
| ❌ **Не туннель** | ByeDPI переносит трафик, но не резолвит DNS как прокси — см. раздел *DNS*. |
Рассматривайте ByeDPI как дополнение к VPN-узлу (перенаправьте троттлящиеся сайты вроде YouTube на ByeDPI, чтобы экономить трафик VPN) либо как самостоятельный инструмент, когда проблема только в троттлинге.
---
## Включение ByeDPI
1. Откройте **Сервисы → Re:HomeProxy → Настройки узлов** и раздел **ByeDPI**.
2. Если `ciadpi` ещё не установлен, нажмите **Установить** — Re:HomeProxy скачает подходящий пакет `byedpi` для вашей архитектуры.
3. Отметьте **Включить**, выберите стратегию (см. ниже) и нажмите **Сохранить и применить**.
Этого достаточно. Re:HomeProxy сам запускает `ciadpi` и автоматически предотвращает зацикливание его трафика через прокси — настраивать межсетевой экран или процесс вручную не нужно.
---
## Стратегии и пресеты
«Стратегия» — это набор параметров командной строки, которыми ByeDPI искажает рукопожатие. Универсально лучшей стратегии нет — **что сработает, зависит от DPI вашего провайдера**. Re:HomeProxy включает **47 готовых пресетов**, сгруппированных по технике (именуются по букве группы, например `C3`, `I1`):
| Группа | Примеры | Идея |
|--------|---------|------|
| Disorder | `--disorder 1`, `--disorder 1+s` | Переупорядочить первые сегменты, чтобы DPI не собрал SNI |
| Fake TTL | `--fake -1 --ttl 6…15` | Вставить фейковый пакет, «умирающий» (низкий TTL) до сервера |
| Fake MD5 | `--fake -1 --md5sig` | Фейковый пакет с неверной TCP MD5-подписью |
| TLS record | `--tlsrec 1+s` | Разбиение на уровне TLS-записи |
| OOB / DisOOB | `--oob 1+s`, `--disoob 1+s` | Трюки с внеполосным байтом у SNI |
| Split | `--split 1+s` | Разбить ClientHello по SNI |
| HTTP mix | `--mod-http hcsmix,…` | Смешивание регистра/формата для обычного HTTP |
| **Адаптивные** | `--auto=ssl_err`, `--auto=torst` | **Самокорректирующиеся** — повторяют попытку на каждом соединении при TLS-ошибке/сбросе |
| Fake SNI/TLS | `--fake-sni`, `--fake-tls-mod` | Отправить ложный SNI / TLS-отпечаток |
| Агрессивные комбо | split + OOB + disorder вместе | Когда одиночных техник недостаточно |
Также можно выбрать **Custom** и ввести свою строку параметров.
### Рекомендуемая отправная точка: пресет I1 (адаптивный)
**Пресет I1 — «Auto SSL Error Fallback»** (`--fake -1 --ttl 8 --auto=ssl_err --fake -1 --ttl 5`) — самый безопасный вариант по умолчанию, **особенно для YouTube**.
Почему важна адаптивность: **фиксированная** стратегия fake-TTL (группа Fake TTL, пресеты C1–C5) работает для *далёких* серверов, но портит рукопожатие на *близких* CDN/edge-серверах — например, на сегментах видео YouTube из локального кэша Google. Фиксированная стратегия, дающая сбой на близких серверах, не только подвешивает страницу, но и может породить шторм повторов неудачных рукопожатий, который **переполняет таблицу отслеживания соединений роутера и подвешивает SSH/LuCI**. Адаптивная `--auto=ssl_err` повторяет попытку на каждом соединении при TLS-ошибке и потому сама подстраивается под близкие *и* далёкие адреса.
---
## Тестер стратегий
Поскольку нужная стратегия зависит от провайдера, в разделе ByeDPI есть **тестер**:
- **Проверить все стратегии** — параллельно опрашивает четыре эталонных сайта и показывает результат по каждому точками:
- **YouTube** и **Telegram** — *далёкие* серверы
- **Discord** и **Speedtest.net** — *близкие* серверы
- `●●●●` = все четыре доступны с этой стратегией; `●●○○` = далёкие работают, близкие — нет (признак того, что стратегия чувствительна к адресу, как фиксированный fake-TTL).
- **Проверить текущую стратегию** — проверяет только стратегию из поля команды и показывает полные имена сайтов с ✓ / ✗.
Выбирайте стратегию с результатом **«всё зелёное» (`●●●●`)** для вашей сети. Если таких несколько — предпочтите адаптивную (I1/I2).
> Тестер запускает ByeDPI на отдельном временном порту, поэтому не мешает «живому» трафику.
---
## Маршрутизация трафика через ByeDPI
После включения ByeDPI появляется как выбираемый узел (**ByeDPI**). Две частые схемы:
- **Вместе с VPN-узлом (рекомендуется):** в режиме `proxy_banned_ru` откройте **RU Proxy Rules** и задайте для *конкретного списка* узел **ByeDPI** — например, направьте YouTube через ByeDPI (экономя трафик VPN), а остальное оставьте на VPN.
- **Самостоятельно (без VPN):** назначьте ByeDPI **основным узлом**. Тогда весь проксируемый трафик идёт через локальную рассинхронизацию.
---
## DNS
ByeDPI — это рассинхронизация, а не туннель, поэтому DNS через него **не** идёт. Когда ByeDPI несёт трафик, Re:HomeProxy резолвит имена **напрямую** (настроенный Secure DNS для ByeDPI идёт через direct-out). Попытка пропустить DoH/DoT или UDP DNS *через* ByeDPI проваливается — искажение рукопожатия ломает зашифрованную DNS-сессию. Это обрабатывается автоматически, настраивать ничего не нужно.
---
## Устранение неполадок
| Симптом | Вероятная причина / решение |
|---------|------------------------------|
| Сайт всё ещё грузится медленно | Стратегия не подходит вашему провайдеру — запустите **Проверить все стратегии** и выберите полностью зелёную. |
| YouTube буферизуется на фиксированном пресете fake-TTL | Переключитесь на адаптивный пресет (**I1/I2**); фиксированный TTL подвешивает близкие узлы кэша Google. |
| Роутер/LuCI/SSH начинают тормозить | Стратегия, чувствительная к адресу, вызывает шторм повторов — переключитесь на адаптивный пресет. |
| Сайт недоступен, а не просто медленный | Возможно, он заблокирован по IP, а не троттлится — ByeDPI не поможет; используйте для него прокси/VPN-узел. |
См. также: [Zapret](Zapret-ru) · [Управление ядром](Core-Management-ru) · [Поддерживаемые протоколы](Supported-Protocols-ru) · [Устранение неполадок](Troubleshooting-ru)
@@ -0,0 +1,79 @@
🇬🇧 [English](Core-Management-en) | 🇷🇺 [Русский](Core-Management-ru)
# Core Management
Re:HomeProxy is **multi-core**: the LuCI app is the interface, and a separate **core** binary does the actual proxying. You install, update, and switch cores from **Services → Re:HomeProxy → Core & Tools → Core management** — no SSH required.
---
## Choosing a core
| | **hiddify-core** (default) | **sing-box-extended** |
|---|---|---|
| Engine | Fork of sing-box by the Hiddify team | Fork of sing-box with extra build tags |
| Footprint | Lighter; a **compact build** exists for small devices | Larger (~26 MB installed) |
| Protocols | Hiddify-app protocols, TLS fragment, XHTTP, Mieru, etc. | The widest protocol set… |
| **AmneziaWG / WARP** | ❌ **Not supported** | ✅ **Supported** |
**Rule of thumb:** if you need **AmneziaWG/WARP**, or want the broadest protocol coverage and have ~40 MB free, choose **sing-box-extended**. Otherwise **hiddify-core** is the lighter default and is the only one with a compact build for tight-storage routers.
Which protocols appear in the node editor depends on the core you install — see [Supported Protocols](Supported-Protocols-en).
---
## Installing — one smart button
Core Management has a single **Install** button per core. It does **not** ask you to choose between "standard" and "compressed" builds — instead it inspects your device and picks a build that actually fits:
1. It reads the free space on `/overlay` (persistent flash) and your free RAM.
2. For hiddify-core: if the full build fits → it installs the **standard** build. If storage is tight but there's enough RAM → it installs the **compact** build and tells you so.
3. If neither can fit → it stops with a clear message instead of installing something broken.
### Why the guardrail matters
A core binary that is **larger than the free overlay** gets **truncated** as it's written, and then crashes with a **"bus error" (SIGBUS)** the moment it's launched — a confusing failure that looks like a corrupt download. The installer's size check exists specifically to prevent this: it never offers a build the device can't hold.
### The compact build
The compact (UPX-compressed) build of hiddify-core is much smaller on flash, but it **decompresses into RAM each time it launches** — trading flash space for memory. The installer only picks it when there's enough free RAM, and shows a note like *"Limited storage — installing the compact build (decompresses into RAM at launch)."* On most routers this is invisible in day-to-day use.
---
## Storage at a glance
| Free on `/overlay` | What installs |
|--------------------|---------------|
| ~40 MB+ | hiddify-core or sing-box-extended (full build) |
| ~25–40 MB | hiddify-core (compact build, if RAM allows) |
| < ~25 MB | Not enough — free space, or bake the core into a custom firmware image |
> On a **compressing** overlay (jffs2/ubifs) the full build needs less free space than the raw figure, because the filesystem compresses it. The installer accounts for this automatically — trust its check over the table above.
Tight on flash but building your own image? Large cores fit comfortably when **baked into the SquashFS** root at image-build time (the SquashFS root is compressed and read-only), rather than installed into the writable overlay.
---
## Switching cores and updating
- **Update:** press **Install** again — it fetches the latest release and reinstalls.
- **Switch cores:** install the other core; if both are present, Re:HomeProxy uses your **Preferred core** setting (on **Client → Routing Settings**). The config generator and the service honour the same preference, so the generated config always matches the core that will run it.
- **Custom / external core:** instead of the managed install you can point Re:HomeProxy at a **self-provided core binary** (a build you compiled, or a version not in Releases) — it detects and uses it. Advanced setups only.
- **Version / status:** the **Core management** section shows the installed core and version. If it shows all `?`, the backend (rpcd) is stale — restart it (`/etc/init.d/rpcd restart`) and reload the page.
---
## Resources & updates
The **Core & Tools** tab also has a **Resources management** section for the data files routing depends on — the GeoIP / Geosite databases and the RU rule-sets:
- **Check update** fetches the latest rule-set and geo data on demand.
- The RU routing lists also **self-refresh** on a schedule, so in normal use you don't need to touch this.
- **Subscriptions** update separately — on demand from the Subscriptions tab, or automatically on a cron; see [Subscriptions & Node Import](Subscriptions-en).
---
## Kernel modules
The proxy needs `kmod-nft-tproxy` and `kmod-tun` for transparent routing. The installer pulls these in; if routing doesn't work after a fresh install, confirm they're present.
See also: [ByeDPI](ByeDPI-en) · [Zapret](Zapret-en) · [Supported Protocols](Supported-Protocols-en) · [Troubleshooting](Troubleshooting-en)
@@ -0,0 +1,79 @@
🇬🇧 [English](Core-Management-en) | 🇷🇺 [Русский](Core-Management-ru)
# Управление ядром
Re:HomeProxy **многоядерный**: приложение LuCI — это интерфейс, а само проксирование выполняет отдельный бинарник-**ядро**. Установка, обновление и переключение ядер выполняются на вкладке **Сервисы → Re:HomeProxy → Ядро и службы → Управление ядром** — без SSH.
---
## Выбор ядра
| | **hiddify-core** (по умолчанию) | **sing-box-extended** |
|---|---|---|
| Движок | Форк sing-box от команды Hiddify | Форк sing-box с доп. флагами сборки |
| Размер | Легче; есть **компактная сборка** для слабых устройств | Крупнее (~26 МБ установленного) |
| Протоколы | Протоколы Hiddify-app, TLS fragment, XHTTP, Mieru и др. | Самый широкий набор протоколов… |
| **AmneziaWG / WARP** | ❌ **Не поддерживается** | ✅ **Поддерживается** |
**Правило:** если нужен **AmneziaWG/WARP** или хочется максимально широкий набор протоколов и есть ~40 МБ свободного места — выбирайте **sing-box-extended**. Иначе **hiddify-core** — более лёгкий вариант по умолчанию и единственный с компактной сборкой для роутеров с малым объёмом памяти.
Какие протоколы появятся в редакторе узлов — зависит от установленного ядра; см. [Поддерживаемые протоколы](Supported-Protocols-ru).
---
## Установка — одна умная кнопка
На странице «Управление ядром» для каждого ядра есть единственная кнопка **Установить**. Она **не** спрашивает, какую сборку — «стандартную» или «сжатую» — выбрать. Вместо этого она проверяет устройство и подбирает сборку, которая действительно поместится:
1. Считывает свободное место на `/overlay` (постоянная флеш-память) и свободную ОЗУ.
2. Для hiddify-core: если полная сборка помещается → ставит **стандартную**. Если места мало, но ОЗУ хватает → ставит **компактную** и сообщает об этом.
3. Если не помещается ни одна → останавливается с понятным сообщением, а не ставит заведомо нерабочее.
### Зачем нужна эта защита
Бинарник ядра, который **больше свободного overlay**, при записи **обрезается** и затем падает с **«bus error» (SIGBUS)** при запуске — сбивающая с толку ошибка, похожая на повреждённую загрузку. Проверка размера в установщике существует именно для этого: он никогда не предложит сборку, которая не помещается на устройстве.
### Компактная сборка
Компактная (сжатая UPX) сборка hiddify-core занимает гораздо меньше места на флеше, но **распаковывается в ОЗУ при каждом запуске** — обмен места на флеше на память. Установщик выбирает её только при достаточном объёме свободной ОЗУ и показывает примечание вроде *«Мало места — устанавливается компактная сборка (распаковывается в ОЗУ при запуске)»*. На большинстве роутеров в повседневной работе это незаметно.
---
## Память — кратко
| Свободно на `/overlay` | Что устанавливается |
|------------------------|---------------------|
| ~40 МБ+ | hiddify-core или sing-box-extended (полная сборка) |
| ~25–40 МБ | hiddify-core (компактная сборка, если хватает ОЗУ) |
| < ~25 МБ | Недостаточно — освободите место или запеките ядро в собственный образ прошивки |
> На **сжимающем** overlay (jffs2/ubifs) полной сборке нужно меньше свободного места, чем сырая цифра, потому что ФС её сжимает. Установщик учитывает это автоматически — доверяйте его проверке, а не таблице выше.
Мало флеша, но собираете свой образ? Крупные ядра свободно помещаются, если **запечь их в SquashFS**-корень на этапе сборки образа (корень SquashFS сжат и доступен только для чтения), а не ставить в записываемый overlay.
---
## Переключение ядер и обновление
- **Обновление:** нажмите **Установить** снова — будет загружен и переустановлен последний релиз.
- **Переключение ядер:** установите другое ядро; если присутствуют оба, Re:HomeProxy использует настройку **Предпочтительное ядро** (на вкладке **Настройки клиента → Настройки маршрутизации**). Генератор конфигурации и служба учитывают ту же настройку, поэтому сгенерированный конфиг всегда соответствует ядру, которое его запустит.
- **Своё / внешнее ядро:** вместо управляемой установки можно указать Re:HomeProxy на **собственный бинарник ядра** (свою сборку или версию, которой нет в Releases) — он его обнаружит и использует. Только для продвинутых сценариев.
- **Версия / статус:** раздел **Управление ядром** показывает установленное ядро и версию. Если везде `?`, значит бэкенд (rpcd) устарел — перезапустите его (`/etc/init.d/rpcd restart`) и перезагрузите страницу.
---
## Ресурсы и обновления
На вкладке **Ядро и службы** также есть раздел **Управление ресурсами** для файлов данных, от которых зависит маршрутизация — баз GeoIP / Geosite и RU-списков:
- **Проверить обновление** скачивает свежие списки и гео-данные по запросу.
- RU-списки маршрутизации к тому же **сами обновляются** по расписанию, так что в обычной работе трогать это не нужно.
- **Подписки** обновляются отдельно — по запросу со вкладки «Подписки» или автоматически по cron; см. [Подписки и импорт узлов](Subscriptions-ru).
---
## Модули ядра
Для прозрачной маршрутизации прокси нужны `kmod-nft-tproxy` и `kmod-tun`. Установщик подтягивает их; если после свежей установки маршрутизация не работает — проверьте, что они присутствуют.
См. также: [ByeDPI](ByeDPI-ru) · [Zapret](Zapret-ru) · [Поддерживаемые протоколы](Supported-Protocols-ru) · [Устранение неполадок](Troubleshooting-ru)
@@ -0,0 +1,155 @@
🇬🇧 [English](Custom-JSON-Config-en) | 🇷🇺 [Русский](Custom-JSON-Config-ru)
# Custom JSON Config
**Custom JSON** is a routing mode that lets you bypass Re:HomeProxy's UCI-based configuration entirely and supply a raw [sing-box](https://sing-box.sagernet.org) / hiddify-core JSON config directly. It is intended for advanced users who need fine-grained control that the web UI does not expose.
To enable it, go to **Re:HomeProxy → Client → Routing Settings** and set **Routing Mode** to **Custom JSON**.
---
## Config Structure
A hiddify-core config is a JSON object. The main top-level sections are:
| Section | Purpose |
|---------|---------|
| `log` | Logging level and output path |
| `dns` | DNS servers, rules, and strategy |
| `inbounds` | How traffic enters the proxy (tproxy, tun, socks, http, etc.) |
| `outbounds` | Where traffic exits (direct, proxy servers, chains) |
| `route` | Rules that map inbound traffic to outbounds |
| `experimental` | Cache database, clash API, and other experimental features |
A minimal working config needs at least one inbound, at least two outbounds (`proxy` + `direct`), and route rules to decide which traffic goes where.
**Full reference:** [sing-box Configuration](https://sing-box.sagernet.org/configuration/)
Specific sections:
- [DNS](https://sing-box.sagernet.org/configuration/dns/)
- [Inbounds](https://sing-box.sagernet.org/configuration/inbound/)
- [Outbounds](https://sing-box.sagernet.org/configuration/outbound/)
- [Route](https://sing-box.sagernet.org/configuration/route/)
---
## hiddify-core vs. sing-box
hiddify-core is a fork of sing-box and is largely config-compatible. The key differences:
- **Additional protocols** — hiddify-core may support protocols not yet upstream in sing-box, or support them earlier. This includes AnyTLS and extended options for some existing protocols. Check [Supported Protocols](Supported-Protocols-en) for what your installed build includes.
- **Hiddify-specific extensions** — some extra fields and options exist for hiddify's own features. These are documented in the [HiddifyCli guide](https://hiddify.com/app/HiddifyCli-guide/#run-config-or-subscription-link-in-hiddifycli-with-hiddifyapp-settings).
- **Version differences** — hiddify-core may be ahead of or behind a specific sing-box release. If a sing-box feature is missing, check the hiddify-core release notes.
When writing configs, start from the sing-box documentation and refer to the hiddify-core guide for anything that does not behave as expected.
---
## Example Skeleton
```json
{
"log": {
"level": "info"
},
"dns": {
"servers": [
{ "tag": "remote", "address": "tls://1.1.1.1" },
{ "tag": "local", "address": "223.5.5.5", "detour": "direct" }
],
"rules": [
{ "geosite": "cn", "server": "local" }
]
},
"inbounds": [
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332
}
],
"outbounds": [
{
"type": "vless",
"tag": "proxy",
"server": "your.server.example",
"server_port": 443
},
{ "type": "direct", "tag": "direct" },
{ "type": "block", "tag": "block" },
{ "type": "dns", "tag": "dns-out" }
],
"route": {
"rules": [
{ "protocol": "dns", "outbound": "dns-out" },
{ "geosite": "cn", "outbound": "direct" },
{ "geoip": "cn", "outbound": "direct" }
],
"final": "proxy"
}
}
```
This is illustrative only — adjust inbound type/port, outbound settings, and routing rules to match your setup.
---
## Tips
- **Validate before saving.** An invalid JSON config will silently fail to apply. Use a JSON validator before pasting — syntax errors will not be reported in the UI. After saving, check the log (see below) to confirm the config loaded correctly.
- **Logs.** If the config applies but traffic does not work, check the core log in **Re:HomeProxy → Core & Tools** or via SSH: `tail -f /var/run/homeproxy/hiddify-c.log`.
- **Inbound ports.** Re:HomeProxy's firewall rules expect specific ports. If you change inbound ports in a custom config, the nftables redirect rules will not match and traffic will not reach your inbound. The default inbound configuration used by Re:HomeProxy is:
```json
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"udp_timeout": "300s",
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
},
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"udp_timeout": "300s",
"sniff": true,
"sniff_override_destination": true
}
]
```
If you need to change these ports, update them both in your custom JSON and in `/etc/config/homeproxy` (the `proxy_port`, `redirect_port`, `tproxy_port`, and `dns_port` options) so the firewall rules stay in sync.
- **`default_mark`.** This is **required** in the `route` section to prevent tproxy routing loops. Without it, hiddify-core's own outbound traffic gets intercepted by the nftables tproxy rules and sent back to the proxy, causing a loop. The value must match `self_mark` in `/etc/config/homeproxy` (default: `100`):
```json
"route": {
"default_mark": 100,
...
}
```
If you change this value, update `self_mark` in `/etc/config/homeproxy` to match.
- **Outbound tags.** Route rules reference outbound tags by name — keep them consistent.
@@ -0,0 +1,166 @@
🇬🇧 [English](Custom-JSON-Config-en) | 🇷🇺 [Русский](Custom-JSON-Config-ru)
# Пользовательский JSON конфиг
**Пользовательский JSON** — режим маршрутизации, позволяющий полностью обойти UCI-конфигурацию Re:HomeProxy и использовать «сырой» JSON-конфиг формата [sing-box](https://sing-box.sagernet.org) / hiddify-core напрямую. Предназначен для опытных пользователей, которым нужен тонкий контроль, недоступный через веб-интерфейс.
Чтобы включить режим: **Re:HomeProxy → Клиент → Настройки маршрутизации** → установить **Режим маршрутизации** в значение **Пользовательский JSON**.
Файл конфигурации размещается по пути `/etc/homeproxy/hiddify-c.json`.
---
## Структура конфига
Конфиг hiddify-core — это JSON-объект. Основные секции верхнего уровня:
| Секция | Назначение |
|--------|-----------|
| `log` | Уровень логирования и путь к файлу лога |
| `dns` | DNS-серверы, правила и стратегия разрешения |
| `inbounds` | Как трафик попадает в прокси (tproxy, tun, socks, http и др.) |
| `outbounds` | Куда трафик выходит (прямо, через прокси-серверы, цепочки) |
| `route` | Правила сопоставления входящего трафика с исходящими |
| `experimental` | Кэш, Clash API и другие экспериментальные возможности |
Минимальный рабочий конфиг требует как минимум одного inbound, двух outbound (`proxy` + `direct`) и правил маршрутизации.
**Полная документация:** [Конфигурация sing-box](https://sing-box.sagernet.org/configuration/)
Отдельные разделы:
- [DNS](https://sing-box.sagernet.org/configuration/dns/)
- [Inbounds](https://sing-box.sagernet.org/configuration/inbound/)
- [Outbounds](https://sing-box.sagernet.org/configuration/outbound/)
- [Route](https://sing-box.sagernet.org/configuration/route/)
---
## hiddify-core и sing-box
hiddify-core — форк sing-box, в целом совместимый по формату конфига. Ключевые отличия:
- **Дополнительные протоколы** — hiddify-core поддерживает протоколы, которых нет в upstream sing-box или которые появились в нём позже. Это AnyTLS, расширенные опции для некоторых существующих протоколов. Полный список — на странице [Поддерживаемые протоколы](Supported-Protocols-ru).
- **Расширения hiddify** — некоторые дополнительные поля и опции специфичны для hiddify. Они задокументированы в [руководстве HiddifyCli](https://hiddify.com/app/HiddifyCli-guide/#run-config-or-subscription-link-in-hiddifycli-with-hiddifyapp-settings).
- **Различия версий** — hiddify-core может опережать или отставать от конкретного релиза sing-box. Если функция sing-box не работает — проверьте release notes hiddify-core.
При написании конфигов отталкивайтесь от документации sing-box, обращаясь к руководству hiddify-core там, где поведение отличается.
---
## Пример скелета конфига
```json
{
"log": {
"disabled": false,
"level": "warn",
"output": "/var/run/homeproxy/hiddify-c.log",
"timestamp": true
},
"dns": {
"servers": [
{ "tag": "remote", "address": "tls://1.1.1.1" },
{ "tag": "local", "address": "223.5.5.5", "detour": "direct" }
],
"rules": [
{ "geosite": "cn", "server": "local" }
]
},
"inbounds": [
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"sniff": true
}
],
"outbounds": [
{
"type": "vless",
"tag": "proxy",
"server": "your.server.example",
"server_port": 443
},
{ "type": "direct", "tag": "direct" },
{ "type": "block", "tag": "block" },
{ "type": "dns", "tag": "dns-out" }
],
"route": {
"default_mark": 100,
"rules": [
{ "protocol": "dns", "outbound": "dns-out" },
{ "geosite": "cn", "outbound": "direct" },
{ "geoip": "cn", "outbound": "direct" }
],
"final": "proxy"
}
}
```
Это только иллюстрация — адаптируйте тип/порт inbound, настройки outbound и правила маршрутизации под свою конфигурацию.
---
## Советы
- **Проверяйте перед сохранением.** Некорректный JSON будет молча проигнорирован. Используйте JSON-валидатор перед вставкой — ошибки синтаксиса не отображаются в интерфейсе. После сохранения проверьте лог (см. ниже), чтобы убедиться, что конфиг загрузился корректно.
- **Логи.** Если конфиг применился, но трафик не работает — проверьте лог ядра в **Re:HomeProxy → Ядро и службы** или через SSH: `tail -f /var/run/homeproxy/hiddify-c.log`.
- **Порты входящих соединений.** Правила файрвола Re:HomeProxy ожидают конкретные порты. Если вы меняете порты inbound в пользовательском JSON, правила nftables перестанут совпадать и трафик не достигнет inbound. Стандартная конфигурация inbounds, используемая Re:HomeProxy:
```json
"inbounds": [
{
"type": "direct",
"tag": "dns-in",
"listen": "::",
"listen_port": 5333
},
{
"type": "mixed",
"tag": "mixed-in",
"listen": "::",
"listen_port": 5330,
"udp_timeout": "300s",
"sniff": true,
"sniff_override_destination": true,
"set_system_proxy": false
},
{
"type": "redirect",
"tag": "redirect-in",
"listen": "::",
"listen_port": 5331,
"sniff": true,
"sniff_override_destination": true
},
{
"type": "tproxy",
"tag": "tproxy-in",
"listen": "::",
"listen_port": 5332,
"network": "udp",
"udp_timeout": "300s",
"sniff": true,
"sniff_override_destination": true
}
]
```
Если нужно изменить порты — обновите их и в пользовательском JSON, и в `/etc/config/homeproxy` (опции `mixed_port`, `redirect_port`, `tproxy_port`, `dns_port`), чтобы правила файрвола оставались согласованными.
- **`default_mark`.** **Обязательное** поле в секции `route` для предотвращения петель маршрутизации tproxy. Без него собственный исходящий трафик hiddify-core перехватывается правилами nftables и снова направляется в прокси. Значение должно совпадать с `self_mark` в `/etc/config/homeproxy` (по умолчанию: `100`):
```json
"route": {
"default_mark": 100,
...
}
```
Если вы меняете это значение — обновите `self_mark` в `/etc/config/homeproxy` соответственно.
- **Теги outbound.** Правила маршрутизации ссылаются на outbound по имени тега — следите за их согласованностью.
@@ -0,0 +1,66 @@
🇬🇧 [English](Custom-Routing-en) | 🇷🇺 [Русский](Custom-Routing-ru)
# Custom Routing
**Custom routing** is the manual middle ground between the ready-made [routing modes](Routing-and-Access-Control-en) and a hand-written [Custom JSON](Custom-JSON-Config-en) config. You build your own **routing nodes** (where traffic can exit) and **routing rules** (which traffic goes where) in the LuCI UI, without writing JSON.
Set it on **Client → Routing Settings → Routing mode → Custom routing**. Two extra tabs appear: **Routing Nodes** and **Routing Rules**.
> You don't have to switch the whole router to Custom routing: in **Russia (Proxy Banned)** mode, ticking **Advanced custom rules** 👨‍💻 reveals these same two tabs so you can layer custom rules *on top of* the RU presets. See [Routing & Access Control](Routing-and-Access-Control-en).
---
## Routing Nodes
A **routing node** is a named outbound that rules can target. Each can be:
- a specific **node**, or **URLTest** (auto-pick the fastest from a pool),
- a **chain** — via the **Outbound** field, send this node's traffic *through* another routing node,
- with its own **Domain resolver** (including the Russia 🔓 / Secure 🔒 servers in RU mode) and **Domain strategy** (IPv4-only, prefer IPv6, etc.).
This is what lets you do things like "this rule → node A, but resolve its domains with the secure DNS" or build a multi-hop chain.
---
## Routing Rules
Each **routing rule** matches some traffic and sends it to a routing node (or **Direct**). The editor groups the match fields into tabs:
| Tab | Matches on |
|-----|-----------|
| **Other fields** | **Protocol** (sniffed — BitTorrent, DNS, QUIC, TLS, HTTP, STUN, SSH, RDP, DTLS…), **IP version**, network, rule-sets |
| **Host/IP fields** | `domain` / `domain_suffix` / `domain_keyword` / `domain_regex`, destination & source **IP CIDR** |
| **Port fields** | destination / source **port** and port ranges |
| **Process fields** | **process name / path** (for traffic originating on the router itself) |
The **Mode** field shows the matching logic — within a category the entries are OR-ed, and the categories are AND-ed together:
```
(domain || domain_suffix || domain_keyword || domain_regex || ip_cidr || ip_is_private) &&
(port || port_range) &&
(source_ip_cidr || source_ip_is_private) &&
(source_port || source_port_range) &&
(other fields)
```
Rules are evaluated top to bottom (the list is sortable) and the first match wins. The fields map one-to-one to sing-box's route rule — for exactly what each accepts, see the upstream reference:
- [Route](https://sing-box.sagernet.org/configuration/route/) — how routing is structured
- [Route Rule](https://sing-box.sagernet.org/configuration/route/rule/) — every match field
- [Sniff](https://sing-box.sagernet.org/configuration/route/sniff/) — **Protocol** matching needs sniffing enabled (it is, by default)
> **Protocol matching needs sniffing.** Matching by `quic`, `tls`, `bittorrent`, etc. relies on the inbound sniffing the first packets — Re:HomeProxy enables this on its inbounds, so it works out of the box.
---
## Custom routing vs Custom JSON
| | **Custom routing** | **[Custom JSON](Custom-JSON-Config-en)** |
|---|---|---|
| How | UI: Routing Nodes + Routing Rules | Raw sing-box / hiddify-core JSON |
| Good for | Per-app/site/IP rules without writing JSON | Full control, fields the UI doesn't expose |
| Validation | UI-guided | You must validate the JSON yourself |
Start with Custom routing; drop to Custom JSON only when you need something the rule editor can't express.
See also: [Routing & Access Control](Routing-and-Access-Control-en) · [Custom JSON Config](Custom-JSON-Config-en) · [Supported Protocols](Supported-Protocols-en) · [DNS & Diagnostics](DNS-and-Diagnostics-en)
@@ -0,0 +1,66 @@
🇬🇧 [English](Custom-Routing-en) | 🇷🇺 [Русский](Custom-Routing-ru)
# Пользовательская маршрутизация
**Пользовательская маршрутизация** — это ручная золотая середина между готовыми [режимами маршрутизации](Routing-and-Access-Control-ru) и конфигом [Пользовательский JSON](Custom-JSON-Config-ru), написанным руками. Вы сами собираете **узлы маршрутизации** (откуда может выходить трафик) и **правила маршрутизации** (какой трафик куда идёт) в интерфейсе LuCI, не написав ни строчки JSON.
Включается в **Настройки клиента → Настройки маршрутизации → Режим маршрутизации → Пользовательская маршрутизация**. Появляются две вкладки: **Правила узлов** и **Расширенные правила**.
> Переключать на пользовательскую маршрутизацию весь роутер необязательно: в режиме **Россия (Обход Блокировок)** галочка **Расширенные правила** 👨‍💻 открывает те же две вкладки, чтобы добавлять свои правила *поверх* RU-пресетов. См. [Маршрутизация и контроль доступа](Routing-and-Access-Control-ru).
---
## Правила узлов
**Узел маршрутизации** — это именованный outbound, на который могут указывать правила. Каждый может быть:
- конкретным **узлом** или **URLTest** (авто-выбор самого быстрого из пула),
- **цепочкой** — через поле **outbound** направить трафик этого узла *через* другой узел маршрутизации,
- со своим **Резолвером доменов** (включая серверы Россия 🔓 / Защищённый 🔒 в режиме РФ) и **Стратегией доменов** (только IPv4, предпочитать IPv6 и т. п.).
Именно это позволяет делать вещи вроде «это правило → узел A, но домены резолвить защищённым DNS» или строить многошаговую цепочку.
---
## Расширенные правила
Каждое **правило маршрутизации** сопоставляет какой-то трафик и отправляет его на узел маршрутизации (или **Напрямую**). Поля сопоставления в редакторе сгруппированы по вкладкам:
| Вкладка | Сопоставление по |
|---------|------------------|
| **Прочие поля** | **Протокол** (по сниффингу — BitTorrent, DNS, QUIC, TLS, HTTP, STUN, SSH, RDP, DTLS…), **Версия IP**, сеть, rule-set-ы |
| **Поля хоста/IP** | `domain` / `domain_suffix` / `domain_keyword` / `domain_regex`, **IP CIDR** назначения и источника |
| **Поля портов** | **порт** и диапазоны портов назначения / источника |
| **Поля процесса** | **имя / путь процесса** (для трафика, исходящего с самого роутера) |
Поле **Режим** показывает логику сопоставления — внутри категории записи объединяются по ИЛИ, а категории — по И:
```
(domain || domain_suffix || domain_keyword || domain_regex || ip_cidr || ip_is_private) &&
(port || port_range) &&
(source_ip_cidr || source_ip_is_private) &&
(source_port || source_port_range) &&
(прочие поля)
```
Правила проверяются сверху вниз (список сортируемый), срабатывает первое совпавшее. Поля один-в-один соответствуют route-правилу sing-box — что именно принимает каждое поле, см. в официальном справочнике:
- [Route](https://sing-box.sagernet.org/configuration/route/) — как устроена маршрутизация
- [Route Rule](https://sing-box.sagernet.org/configuration/route/rule/) — все поля сопоставления
- [Sniff](https://sing-box.sagernet.org/configuration/route/sniff/) — сопоставление по **Протоколу** требует включённого сниффинга (он включён по умолчанию)
> **Сопоставление по протоколу требует сниффинга.** Матч по `quic`, `tls`, `bittorrent` и т. п. опирается на анализ первых пакетов на inbound-е — Re:HomeProxy включает это на своих inbound-ах, так что работает из коробки.
---
## Пользовательская маршрутизация vs Пользовательский JSON
| | **Пользовательская маршрутизация** | **[Пользовательский JSON](Custom-JSON-Config-ru)** |
|---|---|---|
| Как | Интерфейс: Правила узлов + Расширенные правила | «Сырой» JSON sing-box / hiddify-core |
| Для чего | Правила по приложению/сайту/IP без написания JSON | Полный контроль, поля, которых нет в интерфейсе |
| Проверка | С подсказками интерфейса | JSON нужно валидировать самому |
Начинайте с пользовательской маршрутизации; переходите на Пользовательский JSON, только когда нужно что-то, что редактор правил выразить не может.
См. также: [Маршрутизация и контроль доступа](Routing-and-Access-Control-ru) · [Пользовательский JSON конфиг](Custom-JSON-Config-ru) · [Поддерживаемые протоколы](Supported-Protocols-ru) · [DNS и диагностика](DNS-and-Diagnostics-ru)
@@ -0,0 +1,61 @@
🇬🇧 [English](DNS-and-Diagnostics-en) | 🇷🇺 [Русский](DNS-and-Diagnostics-ru)
# DNS & Diagnostics
Two related topics: how Re:HomeProxy resolves names (and why that matters for leaks), and how to verify everything works.
---
## DNS
DNS is configured on **Client → Routing**. Which fields appear depends on the routing mode.
### Russia mode (`proxy_banned_ru`) — two resolvers
Russia mode splits DNS into a **clean** resolver and a **secure** resolver so that lookups go to the right place:
| Field | Purpose | Default |
|-------|---------|---------|
| **Russia DNS server 🔓** | Resolves **Russian** domains **directly**, without the proxy. Plain UDP DNS. | Yandex `77.88.8.8` (also SkyDNS, Comss.one, Cloudflare, Google) |
| **Secure DNS server 🔒** | Resolves **blocked** domains **through the proxy** using encrypted DNS (DoH/DoT), so your ISP can't see which blocked sites you look up. | Cloudflare DoH `https://cloudflare-dns.com/dns-query` (also Quad9, AdGuard, Google; DoT variants) |
This split matters: resolving a blocked domain over plain ISP DNS would either fail or leak the lookup, so blocked domains use encrypted DNS via the proxy, while ordinary Russian domains resolve fast and direct.
> **ByeDPI note:** when a node is ByeDPI, secure DNS is automatically detoured **direct** (not through ByeDPI), because the desync corrupts DoH/DoT. This is handled for you — see [ByeDPI](ByeDPI-en).
### Other modes
- **DNS server** — the general resolver (supports UDP/TCP/DoH/DoQ/DoT). Default `8.8.8.8`. Or `WAN DNS` to read it from the interface.
- **China DNS server** — used in *Bypass mainland China* mode for Chinese domains (default Aliyun `223.5.5.5`).
### Advanced: DNS Servers & DNS Rules
The **DNS Settings**, **DNS Servers**, and **DNS Rules** tabs (under Node Settings) let you define custom resolvers and per-domain DNS routing, including the **domain strategy** (IPv4-only, prefer IPv6, etc.). Most users never need these.
### IPv6 and leaks
In Russia mode, **IPv6 support is off by default on purpose**: the RU routing lists contain **no IPv6 CIDRs**, so any IPv6 traffic would bypass *both* the proxy and the rules — a silent leak. If your WAN has IPv6 and you want everything controlled, the safe choice is to **leave IPv6 disabled** rather than enabling `ipv6_support`.
---
## Diagnostics
The **Diagnostics** page (Services → Re:HomeProxy → Diagnostics) runs live checks. Each card has its own **Check** button.
| Card | What it shows |
|------|---------------|
| **Connectivity** | Reachability of test sites. On **hiddify-core** it also shows your **Direct IP** vs **Proxy IP** (the two should differ when traffic is actually proxied); on **sing-box-extended** it shows the live **Active Node** instead, because sing-box can't report the exit IP. |
| **Core & System** | Whether the core (hiddify-core / sing-box-extended) and ByeDPI are installed, the active binary, version, whether they're running (with PID), and listening ports. Includes a **Restart Service** button. |
| **Configuration** | Whether the generated core config is **valid**, its size, and counts of inbounds / outbounds / rules. |
| **DNS Tests** | Checks that DNS resolution works through the configured resolvers. |
| **Network Intercept** | The nft/firewall interception state — UCI firewall settings and, when Zapret is on, the **Zapret NFQUEUE counters** (mark 110 → queue) so you can confirm packets are reaching nfqws2. |
| **Diagnostics Report** | **Generate** a shareable plain-text report bundling the above — paste it when asking for help. |
### Reading the results
- **Direct IP == Proxy IP** (hiddify-core) → traffic isn't being proxied (check main node / routing mode / access control). On sing-box-extended, confirm the **Active Node** row shows your chosen node instead.
- **Core not running** → use **Restart Service**; if status shows all `?`, the rpcd backend is stale (`/etc/init.d/rpcd restart`, wait ~2 s).
- **Config invalid** → a node or rule is malformed; the report names the error.
- **Zapret counters not incrementing** → the strategy/queue isn't catching traffic — see [Zapret](Zapret-en).
See also: [Getting Started](Getting-Started-en) · [Routing & Access Control](Routing-and-Access-Control-en) · [ByeDPI](ByeDPI-en) · [Zapret](Zapret-en) · [Troubleshooting](Troubleshooting-en)
@@ -0,0 +1,61 @@
🇬🇧 [English](DNS-and-Diagnostics-en) | 🇷🇺 [Русский](DNS-and-Diagnostics-ru)
# DNS и диагностика
Две связанные темы: как Re:HomeProxy резолвит имена (и почему это важно для утечек) и как проверить, что всё работает.
---
## DNS
DNS настраивается в **Настройки клиента → Настройки маршрутизации**. Какие поля видны, зависит от режима маршрутизации.
### Режим России (`proxy_banned_ru`) — два резолвера
Режим России разделяет DNS на **чистый** и **защищённый** резолверы, чтобы запросы уходили туда, куда нужно:
| Поле | Назначение | По умолчанию |
|------|------------|--------------|
| **DNS-сервер России 🔓** | Резолвит **российские** домены **напрямую**, без прокси. Обычный UDP DNS. | Yandex `77.88.8.8` (также SkyDNS, Comss.one, Cloudflare, Google) |
| **Защищённый DNS-сервер 🔒** | Резолвит **заблокированные** домены **через прокси** по шифрованному DNS (DoH/DoT), чтобы провайдер не видел, какие заблокированные сайты вы ищете. | Cloudflare DoH `https://cloudflare-dns.com/dns-query` (также Quad9, AdGuard, Google; варианты DoT) |
Это разделение важно: резолв заблокированного домена через обычный DNS провайдера либо не сработает, либо приведёт к утечке запроса, поэтому заблокированные домены идут по шифрованному DNS через прокси, а обычные российские — быстро и напрямую.
> **Про ByeDPI:** когда узел — ByeDPI, защищённый DNS автоматически уводится **напрямую** (не через ByeDPI), так как рассинхрон ломает DoH/DoT. Это делается за вас — см. [ByeDPI](ByeDPI-ru).
### Другие режимы
- **DNS-сервер** — общий резолвер (поддерживает UDP/TCP/DoH/DoQ/DoT). По умолчанию `8.8.8.8`. Или `WAN DNS`, чтобы взять его с интерфейса.
- **DNS-сервер для Китая** — используется в режиме *Китай - Direct* для китайских доменов (по умолчанию Aliyun `223.5.5.5`).
### Продвинутое: DNS-серверы и DNS-правила
Вкладки **Настройки DNS**, **DNS-серверы** и **DNS-правила** (в Настройках узлов) позволяют задать свои резолверы и пер-доменную маршрутизацию DNS, включая **стратегию доменов** (только IPv4, предпочитать IPv6 и т. п.). Большинству пользователей это не нужно.
### IPv6 и утечки
В режиме России **поддержка IPv6 по умолчанию выключена намеренно**: RU-списки маршрутизации **не содержат IPv6-подсетей**, поэтому любой IPv6-трафик обошёл бы *и* прокси, *и* правила — тихая утечка. Если на WAN есть IPv6 и вы хотите всё контролировать, безопаснее **оставить IPv6 выключенным**, а не включать `ipv6_support`.
---
## Диагностика
Страница **Диагностика** (Сервисы → Re:HomeProxy → Диагностика) запускает живые проверки. У каждой карточки своя кнопка **Проверить**.
| Карточка | Что показывает |
|----------|----------------|
| **Проверка соединения** | Доступность тестовых сайтов. На **hiddify-core** также показывает ваш **IP провайдера** против **IP прокси** (при реальном проксировании они должны различаться); на **sing-box-extended** вместо этого показывает **Активный узел**, так как sing-box не может сообщить исходящий IP. |
| **Ядро и система** | Установлены ли ядро (hiddify-core / sing-box-extended) и ByeDPI, активный бинарник, версия, запущены ли они (с PID) и слушаемые порты. Есть кнопка **Перезапустить службу**. |
| **Конфигурация** | Валиден ли сгенерированный конфиг ядра, его размер и счётчики inbound / outbound / правил. |
| **Тест DNS** | Проверяет, что резолв DNS работает через настроенные резолверы. |
| **Перехват трафика** | Состояние перехвата nft/firewall — настройки firewall в UCI и, при включённом Zapret, **счётчики Zapret NFQUEUE** (метка 110 → очередь), чтобы убедиться, что пакеты доходят до nfqws2. |
| **Отчёт диагностики** | **Сгенерировать** текстовый отчёт, объединяющий всё выше — приложите его при обращении за помощью. |
### Как читать результаты
- **IP провайдера == IP прокси** (hiddify-core) → трафик не проксируется (проверьте основной узел / режим маршрутизации / контроль доступа). На sing-box-extended убедитесь, что в строке **Активный узел** показан выбранный узел.
- **Ядро не запущено** → нажмите **Перезапустить службу**; если статус весь в `?`, бэкенд rpcd устарел (`/etc/init.d/rpcd restart`, подождите ~2 с).
- **Конфиг невалиден** → узел или правило некорректны; отчёт назовёт ошибку.
- **Счётчики Zapret не растут** → стратегия/очередь не ловит трафик — см. [Zapret](Zapret-ru).
См. также: [Первая настройка](Getting-Started-ru) · [Маршрутизация и контроль доступа](Routing-and-Access-Control-ru) · [ByeDPI](ByeDPI-ru) · [Zapret](Zapret-ru) · [Устранение неполадок](Troubleshooting-ru)
@@ -0,0 +1,96 @@
🇬🇧 [English](Getting-Started-en) | 🇷🇺 [Русский](Getting-Started-ru)
# Getting Started
This page walks you from a fresh install to a working connection. It assumes Re:HomeProxy is already installed (see the [README](../../blob/master/README.md) for package installation).
> Everything below is done in the LuCI web UI: **Services → Re:HomeProxy**.
---
## Step 1 — Install a core
The LuCI app is only the interface; a separate **core** binary does the proxying. Open **Core & Tools → Core management** and install one:
- **hiddify-core** (default) — lighter, has a compact build for small routers.
- **sing-box-extended** — needed for AmneziaWG/WARP and the widest protocol set.
The installer auto-picks a build that fits your storage. Full details: **[Core Management](Core-Management-en)**.
---
## Step 2 — Add at least one node
Open **Node Settings → Nodes**. A "node" is a proxy server you connect through. Add one by:
- **Import share links** — paste `vless://`, `vmess://`, `ss://`, `trojan://`, `vpn://` (Amnezia), Hysteria, etc.
- **Import .conf** — a WireGuard / AmneziaWG config file.
- **Subscription URL** — let the router pull and refresh a list of nodes automatically.
- **Manual** — fill the fields by hand.
All import methods and formats: **[Subscriptions & Node Import](Subscriptions-en)**.
---
## Step 3 — Select the main node
Back on the **Client** page, set **Main node** to the node you added — or choose **URLTest** to let Re:HomeProxy automatically pick and fail over to the fastest reachable node.
---
## Step 4 — Choose a routing mode
On the **Client → Routing** tab, pick **Routing mode**. The default depends on your LuCI language:
- **Russia (Proxy Banned)** — `proxy_banned_ru`, the default for Russian installs: everything goes **direct** except the destinations you add to **RU Proxy Rules** (blocked/throttled sites), which go through the proxy.
- **Global** — everything through the proxy.
- **GFWList / Bypass mainland China / Only proxy mainland China** — China-oriented presets.
- **Custom routing / Custom JSON** — full manual control.
For Russia, leave it on **Russia (Proxy Banned)** and open the **RU Proxy Rules** tab to enable the lists you need (Russia Inside, Re:Filter, YouTube, Discord…). Full reference: **[Routing & Access Control](Routing-and-Access-Control-en)**.
---
## Step 5 — Check the basic transport settings
Still on the **Routing** tab, the defaults are sensible:
- **Proxy mode** — `Redirect TCP + TProxy UDP` by default (TUN options appear if `kmod-tun` is installed).
- **IPv6 support** — **off** by default in Russia mode on purpose: the RU lists have no IPv6 CIDRs, so v6 traffic would bypass both the proxy and the rules. Leave it off unless you know you need it. See **[DNS & Diagnostics](DNS-and-Diagnostics-en)**.
---
## Step 6 — Save, apply, start
Press **Save & Apply**. Then start the service (it also auto-starts on boot):
```sh
/etc/init.d/homeproxy start
```
Watch the log at **Core & Tools**.
---
## Step 7 — Verify it works
Open the **Diagnostics** page and run the checks:
- **Connectivity** — confirms reachability of test sites. On **hiddify-core** it shows your **Direct IP** vs **Proxy IP** (they should differ when traffic is proxied); on **sing-box-extended** it shows the live **Active Node** instead (sing-box can't report the exit IP).
- **Core & System** — confirms the core is installed and running.
- **Configuration** — confirms the generated config is valid.
If something is off, see **[Troubleshooting](Troubleshooting-en)** and **[DNS & Diagnostics](DNS-and-Diagnostics-en)**.
---
## Optional — un-throttle sites without a VPN node
If a site (e.g. YouTube, Discord) is merely *throttled* rather than IP-blocked, you can offload it to a built-in DPI-bypass instead of spending VPN bandwidth:
- **[ByeDPI](ByeDPI-en)** — a SOCKS-level desync.
- **[Zapret](Zapret-en)** — a packet-level (nfqws2) desync.
Both can be selected per RU Proxy Rule (e.g. route only YouTube through ByeDPI/Zapret while the rest uses your VPN).
See also: [Core Management](Core-Management-en) · [Routing & Access Control](Routing-and-Access-Control-en) · [Subscriptions & Node Import](Subscriptions-en) · [Troubleshooting](Troubleshooting-en)
@@ -0,0 +1,96 @@
🇬🇧 [English](Getting-Started-en) | 🇷🇺 [Русский](Getting-Started-ru)
# Первая настройка
Эта страница проведёт вас от свежей установки до рабочего соединения. Предполагается, что Re:HomeProxy уже установлен (установку пакетов см. в [README](../../blob/master/README_ru.md)).
> Всё ниже делается в веб-интерфейсе LuCI: **Сервисы → Re:HomeProxy**.
---
## Шаг 1 — Установите ядро
Приложение LuCI — это только интерфейс; проксированием занимается отдельный бинарник **ядра**. Откройте **Ядро и службы → Управление ядром** и установите одно из них:
- **hiddify-core** (по умолчанию) — легче, есть компактная сборка для маленьких роутеров.
- **sing-box-extended** — нужно для AmneziaWG/WARP и самого широкого набора протоколов.
Установщик сам подберёт сборку под ваше хранилище. Подробно: **[Управление ядром](Core-Management-ru)**.
---
## Шаг 2 — Добавьте хотя бы один узел
Откройте **Настройки узлов → Узлы**. «Узел» — это прокси-сервер, через который вы подключаетесь. Добавить можно:
- **Импорт share-ссылок** — вставьте `vless://`, `vmess://`, `ss://`, `trojan://`, `vpn://` (Amnezia), Hysteria и т. п.
- **Импорт .conf** — файл конфигурации WireGuard / AmneziaWG.
- **Ссылка подписки** — роутер сам скачает и будет обновлять список узлов.
- **Вручную** — заполнить поля самостоятельно.
Все способы и форматы импорта: **[Подписки и импорт узлов](Subscriptions-ru)**.
---
## Шаг 3 — Выберите основной узел
На странице **Настройки клиента** задайте **Основной узел** = добавленный узел — или выберите **URLTest**, чтобы Re:HomeProxy автоматически выбирал самый быстрый доступный узел и переключался при сбоях.
---
## Шаг 4 — Выберите режим маршрутизации
На вкладке **Настройки клиента → Настройки маршрутизации** выберите **Режим маршрутизации**. Значение по умолчанию зависит от языка LuCI:
- **Россия (Обход Блокировок)** — `proxy_banned_ru`, по умолчанию для русских установок: всё идёт **напрямую**, кроме адресов, добавленных в **Правила прокси (РФ)** (заблокированные/троттлящиеся сайты) — они идут через прокси.
- **Глобальный** — весь трафик через прокси.
- **Китай - GFWList / Китай - Direct / Китай - Proxy** — пресеты для Китая.
- **Пользовательская маршрутизация / Пользовательский JSON** — полный ручной контроль.
Для России оставьте **Россия (Обход Блокировок)** и откройте вкладку **Правила прокси (РФ)**, чтобы включить нужные списки (Russia Inside, Re:Filter, YouTube, Discord…). Полный справочник: **[Маршрутизация и контроль доступа](Routing-and-Access-Control-ru)**.
---
## Шаг 5 — Проверьте базовые настройки транспорта
На той же вкладке **Настройки маршрутизации** значения по умолчанию разумны:
- **Режим прокси** — по умолчанию `Redirect TCP + TProxy UDP` (опции TUN появляются при установленном `kmod-tun`).
- **Поддержка IPv6** — **выключена** по умолчанию в режиме России намеренно: в RU-списках нет IPv6-подсетей, поэтому v6-трафик обошёл бы и прокси, и правила. Оставьте выключенной, если не уверены, что она нужна. См. **[DNS и диагностика](DNS-and-Diagnostics-ru)**.
---
## Шаг 6 — Сохраните, примените, запустите
Нажмите **Сохранить и применить**. Затем запустите службу (она также стартует при загрузке):
```sh
/etc/init.d/homeproxy start
```
Логи — в разделе **Ядро и службы**.
---
## Шаг 7 — Проверьте, что всё работает
Откройте страницу **Диагностика** и запустите проверки:
- **Проверка соединения** — проверяет доступность тестовых сайтов. На **hiddify-core** показывает ваш **IP провайдера** против **IP прокси** (при проксировании они должны различаться); на **sing-box-extended** вместо этого показывает **Активный узел** (sing-box не может сообщить исходящий IP).
- **Ядро и система** — подтверждает, что ядро установлено и запущено.
- **Конфигурация** — подтверждает, что сгенерированный конфиг валиден.
Если что-то не так — см. **[Устранение неполадок](Troubleshooting-ru)** и **[DNS и диагностика](DNS-and-Diagnostics-ru)**.
---
## По желанию — обойти замедление без VPN
Если сайт (например, YouTube, Discord) просто *замедляется*, а не заблокирован по IP, его можно отдать встроенному обходу DPI вместо расхода трафика VPN:
- **[ByeDPI](ByeDPI-ru)** — рассинхронизация на уровне SOCKS.
- **[Zapret](Zapret-ru)** — рассинхронизация на уровне пакетов (nfqws2).
Оба выбираются для конкретного правила Правила прокси (РФ) (например, направить через ByeDPI/Zapret только YouTube, а остальное — через VPN).
См. также: [Управление ядром](Core-Management-ru) · [Маршрутизация и контроль доступа](Routing-and-Access-Control-ru) · [Подписки и импорт узлов](Subscriptions-ru) · [Устранение неполадок](Troubleshooting-ru)
+39
View File
@@ -0,0 +1,39 @@
# Re:HomeProxy Wiki
---
## 🇬🇧 English
| Page | Description |
|------|-------------|
| [Getting Started](Getting-Started-en) | From a fresh install to a working connection, step by step |
| [Core Management](Core-Management-en) | Choosing hiddify-core vs sing-box-extended, the smart installer, storage and the compact build |
| [Supported Protocols](Supported-Protocols-en) | All supported proxy protocols, build requirements, transport options |
| [Subscriptions & Node Import](Subscriptions-en) | Share links, .conf, Amnezia vpn:// (AmneziaWG/Xray), subscriptions, base64 |
| [Routing & Access Control](Routing-and-Access-Control-en) | Routing modes, RU Proxy Rules, per-device access control |
| [Server Settings](Server-Settings-en) | Run the router as a proxy server — inbounds, types, TLS/ACME |
| [DNS & Diagnostics](DNS-and-Diagnostics-en) | Clean vs secure DNS, IPv6 leaks, and the Diagnostics page |
| [ByeDPI](ByeDPI-en) | SOCKS-level DPI-bypass — un-throttle sites without a VPN, strategy presets and the tester |
| [Zapret](Zapret-en) | Packet-level (nfqws2) DPI-bypass — per-rule desync, presets, Discord voice and the tester |
| [Custom Routing](Custom-Routing-en) | UI routing nodes + rules — match by domain/IP/port/protocol/process |
| [Custom JSON Config](Custom-JSON-Config-en) | Using the Custom JSON routing mode with raw hiddify-core config |
| [Troubleshooting](Troubleshooting-en) | Common errors and how to fix them |
---
## 🇷🇺 Русский
| Страница | Описание |
|----------|----------|
| [Первая настройка](Getting-Started-ru) | От свежей установки до рабочего соединения, по шагам |
| [Управление ядром](Core-Management-ru) | Выбор hiddify-core или sing-box-extended, умный установщик, память и компактная сборка |
| [Поддерживаемые протоколы](Supported-Protocols-ru) | Все поддерживаемые протоколы, требования к сборке, варианты транспорта |
| [Подписки и импорт узлов](Subscriptions-ru) | Share-ссылки, .conf, Amnezia vpn:// (AmneziaWG/Xray), подписки, base64 |
| [Маршрутизация и контроль доступа](Routing-and-Access-Control-ru) | Режимы маршрутизации, RU Proxy Rules, пер-девайс контроль доступа |
| [Настройки сервера](Server-Settings-ru) | Роутер как прокси-сервер — inbound-ы, типы, TLS/ACME |
| [DNS и диагностика](DNS-and-Diagnostics-ru) | Чистый и защищённый DNS, утечки IPv6 и страница диагностики |
| [ByeDPI](ByeDPI-ru) | Обход DPI на уровне SOCKS — разблокировка сайтов без VPN, пресеты стратегий и тестер |
| [Zapret](Zapret-ru) | Обход DPI на уровне пакетов (nfqws2) — рассинхронизация по правилам, пресеты, голос Discord и тестер |
| [Пользовательская маршрутизация](Custom-Routing-ru) | Узлы и правила маршрутизации в UI — матч по домену/IP/порту/протоколу/процессу |
| [Пользовательский JSON конфиг](Custom-JSON-Config-ru) | Использование режима «Пользовательский JSON» с конфигом hiddify-core |
| [Устранение неполадок](Troubleshooting-ru) | Типичные ошибки и способы их устранения |
@@ -0,0 +1,110 @@
🇬🇧 [English](Routing-and-Access-Control-en) | 🇷🇺 [Русский](Routing-and-Access-Control-ru)
# Routing & Access Control
Re:HomeProxy decides **what** traffic is proxied (routing mode + rules) and **which devices** are affected (access control). This page covers both.
---
## Routing modes
Set on **Client → Routing → Routing mode**:
| Mode | What it does |
|------|--------------|
| **Russia (Proxy Banned)** (`proxy_banned_ru`) | Default for RU. Everything is **direct** except destinations added to **RU Proxy Rules**, which go through the proxy. |
| **Global** | All traffic through the proxy. |
| **GFWList** | Proxies a built-in list of commonly blocked (GFW) domains. |
| **Bypass mainland China** | Everything proxied except mainland-China destinations (direct). |
| **Only proxy mainland China** | Only mainland-China destinations proxied. |
| **Custom routing** | Manual routing nodes + rules (advanced). |
| **Custom JSON** | Hand-written hiddify-core config — see [Custom JSON Config](Custom-JSON-Config-en). |
**Routing ports** — restrict which destination ports are proxied (e.g. *Common ports only* to keep P2P traffic direct).
**Proxy mode** — how traffic is intercepted: `Redirect TCP`, `Redirect TCP + TProxy UDP` (default), `Redirect TCP + Tun UDP`, or full `Tun TCP/UDP` (TUN modes need `kmod-tun`).
### Main node and a separate UDP node
On the **Routing Settings** tab, **Main node** sets where traffic exits (a specific node, or **URLTest** to auto-pick the fastest with its own pool, test interval and tolerance).
By default UDP follows the main node. Set **Main UDP node** to send **UDP/QUIC out a different node** than TCP — useful when your main node handles UDP poorly (or not at all), or when you want QUIC/voice on a node that's better at it. It has its own URLTest pool too. Leave it on *Same as main node* if you don't need the split.
---
## Russia (Proxy Banned) — the full UI
`proxy_banned_ru` is the default RU mode: **everything is direct except what you explicitly proxy.** When it is selected, the **Routing** tab gains the fields below and an extra **RU Proxy Rules** tab appears.
### Routing tab (in this mode)
| Field | What it does |
|-------|--------------|
| **Russia DNS server** 🔓 | Resolves Russian/normal domains **directly** (not through the proxy). Default *Yandex (77.88.8.8)*; also SkyDNS, Comss.one, and plain-UDP Cloudflare/Google. |
| **Secure DNS server** 🔒 | Resolves **blocked** domains **through the proxy** over encrypted DNS (DoH/DoT), so your ISP can't see those lookups. Default *Cloudflare DoH*; also Quad9 / AdGuard / Google (DoH and DoT). See [DNS & Diagnostics](DNS-and-Diagnostics-en). |
| **Proxy calls** 📞 | Route VoIP call ports (WhatsApp, Telegram, FaceTime…) through the proxy. Off by default. |
| **Do not proxify torrents** 🧲 | Force BitTorrent traffic (protocol + common ports) direct. Off by default. |
| **Advanced custom rules** 👨‍💻 | Reveal the **Routing Nodes** and **Routing Rules** tabs (see below). Off by default. |
| **Routing ports** | Limit which destination ports are proxied (e.g. *Common ports only* keeps P2P direct). |
| **Proxy mode** | How traffic is intercepted — see [Routing modes](#routing-modes) above. |
### RU Proxy Rules tab
This is where you choose *what* to proxy. **The default route is Direct** — only the rules you add here are proxied. Each rule = a **Source list** ⤵️ + a **Node** 🔗 to send it through, applied with automatic priority:
1. **Smaller service lists** first — YouTube, Twitter/X, TikTok, Telegram, Discord, Roblox, Meta (Facebook/Instagram), Google AI, Google Play, anime, HDRezka, international news, adult, GeoBlock, HODCA, and cloud/CDN ranges (Cloudflare, CloudFront, OVH, Hetzner, DigitalOcean).
2. **Russia Inside** (1000+ domains, by itdoginfo) — the in-Russia must-have set.
3. **Re:Filter** (60000+ domains + 25000+ IPs) — the Roskomnadzor blocklist.
The lists are community-maintained and **self-refresh** on the router; you only pick which to enable. (Adding the same source twice is flagged — only the first rule takes effect.)
**Per-rule Node** 🔗 can be:
- **Same as main node** — use your main proxy.
- **Separate URLTest** — auto-select among a chosen set of nodes, with its own **URLTest nodes**, **Test interval** (default 180 s) and **Test tolerance** (default 150 ms).
- **A specific node**.
- **ByeDPI** or **Zapret** — send that list through a DPI-bypass instead of a VPN node (e.g. YouTube via [ByeDPI](ByeDPI-en) / [Zapret](Zapret-en) to save VPN bandwidth).
### Advanced custom rules (optional)
Enabling **Advanced custom rules** 👨‍💻 layers two extra tabs on top of the RU presets:
- **Routing Nodes** — define named outbounds (a node, a URLTest group, or a chained **Outbound**), each with its own **Domain resolver** (including the Russia 🔓 / Secure 🔒 servers) and **Domain strategy**.
- **Routing Rules** — match traffic by domain / IP / port / protocol and send it to one of those routing nodes.
Full reference for both tabs (and the standalone **Custom routing** mode): **[Custom Routing](Custom-Routing-en)**.
---
## Access Control — which devices are affected
**Client → Access Control** controls *which LAN devices* the proxy applies to. There are several **independent** controls — they combine, they are not one dropdown:
### LAN IP Policy
- **Proxy mode for devices** — the global gate:
- **Disable** — no per-device restriction (all LAN devices follow the routing rules).
- **Proxy listed only** — only the listed IPs/MACs are proxied.
- **Proxy all except listed** — everyone is proxied except the listed IPs/MACs (which go direct).
- **Gaming mode** *(independent list)* — for the selected devices, **only TCP** is proxied (UDP/game traffic stays direct for lower latency).
- **Global proxy** *(independent list)* — for the selected devices, **all** traffic goes through the proxy regardless of the routing rules.
Each of these is its own IP/MAC list, so you can mix them per device.
### WAN IP Policy
Force specific **destination** IPs/CIDRs to be proxied or direct, regardless of mode.
### Proxy / Direct Domain Lists
Free-form domain lists that are always proxied or always direct — useful for one-off destinations not covered by the RU lists.
---
## Tips
- In Russia mode, start with **Russia Inside + Re:Filter** enabled; add per-service lists only if a specific site still misbehaves.
- If a site is *throttled* (slow) rather than *blocked* (unreachable), route it through [ByeDPI](ByeDPI-en) or [Zapret](Zapret-en) instead of the VPN.
- Use the [Diagnostics](DNS-and-Diagnostics-en) page to confirm traffic is actually being proxied — **Direct IP vs Proxy IP** on hiddify-core, or the **Active Node** row on sing-box-extended.
See also: [Getting Started](Getting-Started-en) · [Subscriptions & Node Import](Subscriptions-en) · [DNS & Diagnostics](DNS-and-Diagnostics-en) · [Troubleshooting](Troubleshooting-en)
@@ -0,0 +1,110 @@
🇬🇧 [English](Routing-and-Access-Control-en) | 🇷🇺 [Русский](Routing-and-Access-Control-ru)
# Маршрутизация и контроль доступа
Re:HomeProxy решает, **какой** трафик проксировать (режим маршрутизации + правила) и **каких устройств** это касается (контроль доступа). Эта страница — про оба аспекта.
---
## Режимы маршрутизации
Задаётся в **Настройки клиента → Настройки маршрутизации → Режим маршрутизации**:
| Режим | Что делает |
|-------|------------|
| **Россия (Обход Блокировок)** (`proxy_banned_ru`) | По умолчанию для РФ. Всё идёт **напрямую**, кроме адресов в **Правила прокси (РФ)** — они через прокси. |
| **Глобальный** | Весь трафик через прокси. |
| **Китай - GFWList** | Проксирует встроенный список часто блокируемых (GFW) доменов. |
| **Китай - Direct** | Всё через прокси, кроме адресов материкового Китая (напрямую). |
| **Китай - Proxy** | Через прокси только адреса материкового Китая. |
| **Пользовательская маршрутизация** | Ручные узлы и правила маршрутизации (для продвинутых). |
| **Пользовательский JSON** | Конфиг hiddify-core вручную — см. [Пользовательский JSON конфиг](Custom-JSON-Config-ru). |
**Порты маршрутизации** — ограничить, какие порты назначения проксируются (например, *Только обычные порты*, чтобы P2P-трафик шёл напрямую).
**Режим прокси** — как перехватывается трафик: `Redirect TCP`, `Redirect TCP + TProxy UDP` (по умолчанию), `Redirect TCP + Tun UDP` или полный `Tun TCP/UDP` (режимы TUN требуют `kmod-tun`).
### Основной узел и отдельный UDP-узел
На вкладке **Настройки маршрутизации** поле **Основной узел** задаёт, через какой узел уходит трафик (конкретный узел или **URLTest** для авто-выбора самого быстрого, со своим пулом, интервалом и допуском теста).
По умолчанию UDP идёт через основной узел. Поле **Основной UDP-узел** позволяет отправлять **UDP/QUIC через другой узел**, чем TCP — полезно, если основной узел плохо (или вообще не) работает с UDP, или если хочется отдать QUIC/голос узлу, который с этим справляется лучше. У него тоже свой пул URLTest. Оставьте *Как основной узел*, если разделение не нужно.
---
## Россия (Обход Блокировок) — полный интерфейс
`proxy_banned_ru` — режим РФ по умолчанию: **весь трафик идёт напрямую, кроме того, что вы явно проксируете.** При его выборе на вкладке **Маршрутизация** появляются поля ниже, а также отдельная вкладка **Правила прокси (РФ)**.
### Вкладка «Маршрутизация» (в этом режиме)
| Поле | Что делает |
|------|------------|
| **DNS-сервер России** 🔓 | Разрешает российские/обычные домены **напрямую** (не через прокси). По умолчанию *Yandex (77.88.8.8)*; также SkyDNS, Comss.one и обычный UDP Cloudflare/Google. |
| **Защищённый DNS-сервер** 🔒 | Разрешает **заблокированные** домены **через прокси** по шифрованному DNS (DoH/DoT), чтобы провайдер не видел эти запросы. По умолчанию *Cloudflare DoH*; также Quad9 / AdGuard / Google (DoH и DoT). См. [DNS и диагностика](DNS-and-Diagnostics-ru). |
| **Проксировать звонки** 📞 | Направить порты VoIP-звонков (WhatsApp, Telegram, FaceTime…) через прокси. По умолчанию выключено. |
| **Не проксировать торренты** 🧲 | Заставить трафик BitTorrent (протокол + типичные порты) идти напрямую. По умолчанию выключено. |
| **Расширенные правила** 👨‍💻 | Открывает вкладки **Правила узлов** и **Расширенные правила** (см. ниже). По умолчанию выключено. |
| **Порты маршрутизации** | Ограничить, какие порты назначения проксируются (например, *только типичные порты* оставляет P2P напрямую). |
| **Режим прокси** | Как перехватывается трафик — см. [Режимы маршрутизации](#режимы-маршрутизации) выше. |
### Вкладка «Правила прокси (РФ)»
Здесь выбирается, *что* проксировать. **Маршрут по умолчанию — Напрямую**, проксируется только то, что вы добавите. Каждое правило = **список-источник** ⤵️ + **узел** 🔗, через который его отправлять. Правила применяются с автоматическим приоритетом:
1. Сначала **небольшие сервисные списки** — YouTube, Twitter/X, TikTok, Telegram, Discord, Roblox, Meta (Facebook/Instagram), Google AI, Google Play, аниме, HDRezka, международные новости, 18+, GeoBlock, HODCA и облачные/CDN-диапазоны (Cloudflare, CloudFront, OVH, Hetzner, DigitalOcean).
2. **Russia Inside** (1000+ доменов, от itdoginfo) — обязательный набор для РФ.
3. **Re:Filter** (60000+ доменов + 25000+ IP) — список блокировок Роскомнадзора.
Сами списки поддерживаются сообществом и **автообновляются** на роутере; вы только выбираете, какие включить. (Повторное добавление того же источника отмечается — сработает только первое правило.)
**Узел** 🔗 для каждого правила может быть:
- **Как основной узел** — ваш основной прокси.
- **Отдельный URLTest** — автовыбор среди заданного набора узлов, со своими **узлами URLTest**, **интервалом тестирования** (по умолчанию 180 с) и **допуском теста** (по умолчанию 150 мс).
- **Конкретный узел**.
- **ByeDPI** или **Zapret** — отправить список через обход DPI вместо VPN-узла (например, YouTube через [ByeDPI](ByeDPI-ru) / [Zapret](Zapret-ru), чтобы экономить трафик VPN).
### Расширенные правила (опционально)
Включение **Расширенных правил** 👨‍💻 добавляет две вкладки поверх RU-пресетов:
- **Правила узлов** — задают именованные исходящие (узел, группа URLTest или цепочка через **Исходящий**), у каждого свой **Резолвер доменов** (включая серверы Россия 🔓 / Защищённый 🔒) и **Стратегия доменов**.
- **Расширенные правила** — сопоставляют трафик по домену / IP / порту / протоколу и направляют его на один из этих узлов.
Полный справочник по обеим вкладкам (и по самостоятельному режиму **Пользовательская маршрутизация**): **[Пользовательская маршрутизация](Custom-Routing-ru)**.
---
## Контроль доступа — каких устройств это касается
**Настройки клиента → Контроль доступа** определяет, *к каким устройствам LAN* применяется прокси. Здесь несколько **независимых** механизмов — они комбинируются, это не один список:
### Политика IP для LAN
- **Режим проксирования устройств** — глобальный вентиль:
- **Отключить** — без пер-девайс ограничений (все устройства следуют правилам маршрутизации).
- **Проксировать только указанные** — проксируются только перечисленные IP/MAC.
- **Проксировать всё кроме указанных** — проксируются все, кроме перечисленных IP/MAC (те идут напрямую).
- **Игровой режим** *(независимый список)* — для выбранных устройств проксируется **только TCP** (UDP/игровой трафик идёт напрямую для меньшей задержки).
- **Глобальный прокси** *(независимый список)* — для выбранных устройств **весь** трафик идёт через прокси, независимо от правил маршрутизации.
Каждый из этих механизмов — свой список IP/MAC, так что их можно комбинировать по устройствам.
### Политика WAN IP
Принудительно проксировать или направлять напрямую конкретные **IP/подсети назначения**, независимо от режима.
### Списки доменов (прокси / напрямую)
Свободные списки доменов, которые всегда проксируются или всегда идут напрямую — удобно для разовых адресов, не покрытых RU-списками.
---
## Советы
- В режиме России начните с включённых **Russia Inside + Re:Filter**; добавляйте сервисные списки, только если конкретный сайт всё ещё работает плохо.
- Если сайт *троттлится* (медленный), а не *заблокирован* (недоступен), направьте его через [ByeDPI](ByeDPI-ru) или [Zapret](Zapret-ru) вместо VPN.
- Используйте страницу [Диагностика](DNS-and-Diagnostics-ru), чтобы убедиться, что трафик реально проксируется — **IP провайдера против IP прокси** на hiddify-core или строка **Активный узел** на sing-box-extended.
См. также: [Первая настройка](Getting-Started-ru) · [Подписки и импорт узлов](Subscriptions-ru) · [DNS и диагностика](DNS-and-Diagnostics-ru) · [Устранение неполадок](Troubleshooting-ru)
@@ -0,0 +1,51 @@
🇬🇧 [English](Server-Settings-en) | 🇷🇺 [Русский](Server-Settings-ru)
# Server Settings
Most of Re:HomeProxy is a **client** — the router dials *out* to proxy nodes. **Server Settings** is the reverse: it runs **inbounds** on the router so other devices can connect *in*. Use it to turn the router into your own endpoint (e.g. a home server you reach from a phone while away), or to expose a local SOCKS/HTTP proxy to the LAN.
Open it at **Services → Re:HomeProxy → Server Settings**. It is independent of client mode — run either, both, or neither. A fresh install has **no inbounds**; you add them here.
> ⚠️ An inbound opens a listening port. Turn on **Firewall** (below) only for inbounds you want reachable from the Internet, and always protect them with a strong password and/or TLS.
---
## Global
A single **Enable** switch turns the server subsystem on or off. Each inbound below also has its own enable toggle.
## Inbounds (Server settings)
Add one row per listener. The common fields:
| Field | Meaning |
|-------|---------|
| **Label** | A unique name for the inbound |
| **Enable** | Turn this inbound on/off |
| **Firewall** | *Allow access from the Internet* — off = reachable from LAN only, on = the port is opened on WAN |
| **Type** | The inbound protocol (see below) |
| **Listen address / Listen port** | Bind address (default `::`) and a unique port |
| **Username / Password** | Where the protocol uses credentials |
Per-protocol fields (encryption method, TLS, transport, multiplex…) appear in the edit dialog **after you pick a Type**. They mirror sing-box's inbound options one-to-one — rather than repeat them here, see the **[sing-box inbound reference](https://sing-box.sagernet.org/configuration/inbound/)** for each field, and the per-type pages (e.g. [VLESS](https://sing-box.sagernet.org/configuration/inbound/vless/), [Trojan](https://sing-box.sagernet.org/configuration/inbound/trojan/), [Hysteria2](https://sing-box.sagernet.org/configuration/inbound/hysteria2/)).
### Inbound types
**Always available:** AnyTLS, HTTP, **Mixed** (HTTP + SOCKS on one port), Shadowsocks, SOCKS, Trojan, VLESS, VMess.
Core/build-dependent (the dropdown only shows them if your core supports them — check tags as in [Supported Protocols](Supported-Protocols-en)):
- **Hysteria, Hysteria2, NaïveProxy, TUIC** — require a `with_quic` build.
- **MTProxy** — sing-box-extended only.
## TLS & certificates
TLS-capable inbounds expose the usual controls (min/max version, cipher suites) plus either **your own certificate** or, on a `with_acme` build, **ACME** auto-issuance (Let's Encrypt / ZeroSSL, via HTTP-01 or DNS-01 challenge). These follow sing-box field-for-field — see **[sing-box TLS](https://sing-box.sagernet.org/configuration/shared/tls/)** and its [ACME section](https://sing-box.sagernet.org/configuration/shared/tls/#acme).
## Tips
- Quickest personal endpoint: **Mixed** + a strong password — point a client's SOCKS/HTTP proxy at the router's IP and port.
- Public, blocking-resistant endpoint: a TLS type (VLESS / Trojan / Hysteria2) with ACME **DNS-01**, so you don't have to expose port 80.
- Leave **Firewall** off for an inbound you only use from inside the LAN.
See also: [Supported Protocols](Supported-Protocols-en) · [Core Management](Core-Management-en) · [Getting Started](Getting-Started-en) · [Troubleshooting](Troubleshooting-en)
@@ -0,0 +1,51 @@
🇬🇧 [English](Server-Settings-en) | 🇷🇺 [Русский](Server-Settings-ru)
# Настройки сервера
Бо́льшая часть Re:HomeProxy — это **клиент**: роутер сам подключается *наружу* к прокси-узлам. **Настройки сервера** — наоборот: роутер поднимает **входящие соединения (inbounds)**, чтобы другие устройства подключались *к нему*. Пригодится, чтобы сделать из роутера собственную точку входа (например, домашний сервер, к которому вы подключаетесь с телефона в поездке) или открыть локальный SOCKS/HTTP-прокси в LAN.
Открывается в **Сервисы → Re:HomeProxy → Настройки сервера**. Не зависит от клиентского режима — можно использовать любой из них, оба или ни одного. На свежей установке inbound-ов **нет** — вы добавляете их здесь.
> ⚠️ Inbound открывает слушающий порт. Включайте **Брандмауэр** (ниже) только для тех inbound-ов, которые должны быть доступны из интернета, и всегда защищайте их надёжным паролем и/или TLS.
---
## Глобально
Один переключатель **Включить** включает или выключает серверную подсистему. У каждого inbound-а ниже есть свой собственный переключатель.
## Inbound-ы (Настройки сервера)
Добавьте по одной строке на каждый слушатель. Общие поля:
| Поле | Что значит |
|------|------------|
| **Метка** | Уникальное имя inbound-а |
| **Включить** | Включить/выключить этот inbound |
| **Брандмауэр** | *Разрешить доступ из интернета* — выкл = доступен только из LAN, вкл = порт открывается на WAN |
| **Тип** | Протокол inbound-а (см. ниже) |
| **Адрес прослушивания / Порт прослушивания** | Адрес привязки (по умолчанию `::`) и уникальный порт |
| **Имя пользователя / Пароль** | Где протокол использует учётные данные |
Поля конкретного протокола (метод шифрования, TLS, транспорт, мультиплекс…) появляются в окне редактирования **после выбора Типа**. Они один-в-один повторяют опции inbound-ов sing-box — поэтому здесь они не дублируются, см. **[справочник inbound-ов sing-box](https://sing-box.sagernet.org/configuration/inbound/)** по каждому полю и страницы по типам (например, [VLESS](https://sing-box.sagernet.org/configuration/inbound/vless/), [Trojan](https://sing-box.sagernet.org/configuration/inbound/trojan/), [Hysteria2](https://sing-box.sagernet.org/configuration/inbound/hysteria2/)).
### Типы inbound-ов
**Всегда доступны:** AnyTLS, HTTP, **Смешанный** (HTTP + SOCKS на одном порту), Shadowsocks, SOCKS, Trojan, VLESS, VMess.
Зависят от ядра/сборки (в списке появляются только если ядро их поддерживает — проверьте теги, как в [Поддерживаемых протоколах](Supported-Protocols-ru)):
- **Hysteria, Hysteria2, NaïveProxy, TUIC** — требуют сборку `with_quic`.
- **MTProxy** — только sing-box-extended.
## TLS и сертификаты
Inbound-ы с TLS дают обычные настройки (мин./макс. версия, наборы шифров) плюс либо **ваш собственный сертификат**, либо, на сборке `with_acme`, авто-выпуск через **ACME** (Let's Encrypt / ZeroSSL, через HTTP-01 или DNS-01). Они повторяют sing-box поле-в-поле — см. **[TLS в sing-box](https://sing-box.sagernet.org/configuration/shared/tls/)** и его [раздел ACME](https://sing-box.sagernet.org/configuration/shared/tls/#acme).
## Советы
- Самая быстрая личная точка входа: **Смешанный** + надёжный пароль — направьте SOCKS/HTTP-прокси клиента на IP и порт роутера.
- Публичная, устойчивая к блокировкам точка входа: TLS-тип (VLESS / Trojan / Hysteria2) с ACME **DNS-01**, чтобы не открывать порт 80.
- Оставляйте **Брандмауэр** выключенным для inbound-а, которым пользуетесь только из LAN.
См. также: [Поддерживаемые протоколы](Supported-Protocols-ru) · [Управление ядром](Core-Management-ru) · [Первая настройка](Getting-Started-ru) · [Устранение неполадок](Troubleshooting-ru)
@@ -0,0 +1,94 @@
🇬🇧 [English](Subscriptions-en) | 🇷🇺 [Русский](Subscriptions-ru)
# Subscriptions & Node Import
There are four ways to get nodes into Re:HomeProxy. All live on **Node Settings → Nodes** (import buttons) and **Node Settings → Subscriptions**.
---
## 1. Import share links
The **Import share links** button accepts one link per line. Supported schemes:
| Scheme | Protocol |
|--------|----------|
| `vless://` | VLESS (incl. Reality, XHTTP) |
| `vmess://` | VMess (v2rayN format) |
| `ss://` | Shadowsocks / Shadowsocks 2022 |
| `trojan://` | Trojan |
| `hysteria://`, `hysteria2://`, `hy2://` | Hysteria / Hysteria2 |
| `tuic://` | TUIC |
| `naive://` | NaïveProxy |
| `mieru://` | Mieru |
| `ssh://` | SSH |
| `wireguard://` | WireGuard |
| `vpn://` | **AmneziaVPN** share link — see below |
### AmneziaVPN `vpn://` links
Re:HomeProxy fully decodes Amnezia's `vpn://` share format (`base64url(qCompress(zlib JSON))`) right in the browser — no helper tool needed. It reads the container inside and recognises **both** Amnezia config types automatically:
- **AmneziaWG** (`amnezia-awg` / `amnezia-awg2`) → an **AmneziaWG** node, with the full obfuscation parameter set (`Jc`, `Jmin`, `Jmax`, `S1`–`S4`, `H1`–`H4`, `I1`–`I5`), MTU, keepalive and keys. *Requires the sing-box-extended core* — see [Supported Protocols](Supported-Protocols-en).
- **Xray** (`amnezia-xray`) → the Xray outbound inside is parsed into a node, including **VLESS + Reality/TLS** and the transport (`ws`, `grpc`, `xhttp`/`splithttp`, HTTPUpgrade) with their settings.
So an Amnezia link works whether it carries an AmneziaWG or an Xray profile.
### Base64
Subscription and share-link payloads are very often **base64**-encoded — this is the norm, not an edge case. Re:HomeProxy decodes base64 transparently:
- A pasted block that is one big base64 blob is decoded into its list of links.
- Individual `ss://` / `vmess://` links carry their own base64 (userinfo, VMess JSON) — handled automatically.
- Subscription URLs that return a base64 list (the classic format) are decoded on fetch.
You normally don't need to decode anything yourself — just paste the link or the blob.
Two options apply during import:
- **Allow insecure** — marks imported TLS nodes as `tls_insecure` (skip cert verification). Use only if you trust the server.
- **Packet encoding** — applied to imported VLESS/VMess nodes.
---
## 2. Import a .conf file
The **Import .conf** button reads a **WireGuard** or **AmneziaWG** `.conf` file and creates a node from it — handy for WARP/AmneziaWG configs you already have. (AmneziaWG nodes require the **sing-box-extended** core — see [Supported Protocols](Supported-Protocols-en).)
---
## 3. Subscription URLs (auto-updating)
On the **Subscriptions** tab, add one or more **Subscription URL-s**. The router fetches them and keeps the node list in sync. Re:HomeProxy understands several response formats automatically:
- **Base64 / plain share-link lists** — the classic subscription format (one encoded link per line).
- **sing-box JSON / Hiddify** — when the subscription is served as a sing-box/Hiddify JSON config. The correct format is requested via the **User-Agent**, so the same URL can return different formats to different clients.
- **Xray / V2Ray JSON** — config-array JSON (e.g. *connliberty*-style) is parsed into nodes; this path is User-Agent gated as well.
### Update settings
- **Auto update** — enable periodic refresh; pick the **Update time** (hour of day, default 02:00).
- **Update via proxy** — fetch the subscription through the proxy (useful when the subscription host itself is blocked). Off by default to avoid a startup chicken-and-egg.
- Nodes can also be refreshed on demand from the UI.
### Filtering subscription nodes
- **Filter nodes** — `Disable`, `Blacklist mode` (drop matching), or `Whitelist mode` (keep only matching).
- **Filter keywords** — the match list; **regex** is supported.
This is useful to drop dead/region-locked nodes or to keep only a curated subset (e.g. only certain countries/protocols for URLTest).
---
## Which protocols can I import?
That depends on the **core** you installed — a node type only works if the core supports it (e.g. AmneziaWG needs sing-box-extended; some QUIC/Naive protocols need specific build tags). See **[Supported Protocols](Supported-Protocols-en)** for the full matrix and how to check your build's tags.
---
## Notes
- Imported subscription nodes are grouped by their subscription; manual nodes stay separate.
- If an import reports "no valid link found", check the scheme is one of the above and that the link isn't truncated.
- For URLTest selection across many subscription nodes, combine **Filter nodes** here with a per-rule **URLTest** target in [RU Proxy Rules](Routing-and-Access-Control-en).
See also: [Getting Started](Getting-Started-en) · [Supported Protocols](Supported-Protocols-en) · [Routing & Access Control](Routing-and-Access-Control-en) · [Troubleshooting](Troubleshooting-en)
@@ -0,0 +1,94 @@
🇬🇧 [English](Subscriptions-en) | 🇷🇺 [Русский](Subscriptions-ru)
# Подписки и импорт узлов
Есть четыре способа добавить узлы в Re:HomeProxy. Все они — на **Настройки узлов → Узлы** (кнопки импорта) и **Настройки узлов → Подписки**.
---
## 1. Импорт share-ссылок
Кнопка **Импортировать ссылку-конфигурацию для подключения** (на вкладке **Узлы**) принимает по одной ссылке на строку. Поддерживаемые схемы:
| Схема | Протокол |
|-------|----------|
| `vless://` | VLESS (в т. ч. Reality, XHTTP) |
| `vmess://` | VMess (формат v2rayN) |
| `ss://` | Shadowsocks / Shadowsocks 2022 |
| `trojan://` | Trojan |
| `hysteria://`, `hysteria2://`, `hy2://` | Hysteria / Hysteria2 |
| `tuic://` | TUIC |
| `naive://` | NaïveProxy |
| `mieru://` | Mieru |
| `ssh://` | SSH |
| `wireguard://` | WireGuard |
| `vpn://` | **AmneziaVPN** share-ссылка — см. ниже |
### Ссылки AmneziaVPN `vpn://`
Re:HomeProxy полностью декодирует share-формат Amnezia `vpn://` (`base64url(qCompress(zlib JSON))`) прямо в браузере — без вспомогательных утилит. Он читает контейнер внутри и автоматически распознаёт **оба** типа конфигов Amnezia:
- **AmneziaWG** (`amnezia-awg` / `amnezia-awg2`) → узел **AmneziaWG** с полным набором параметров обфускации (`Jc`, `Jmin`, `Jmax`, `S1`–`S4`, `H1`–`H4`, `I1`–`I5`), MTU, keepalive и ключами. *Требует ядро sing-box-extended* — см. [Поддерживаемые протоколы](Supported-Protocols-ru).
- **Xray** (`amnezia-xray`) → вложенный outbound Xray разбирается в узел, включая **VLESS + Reality/TLS** и транспорт (`ws`, `grpc`, `xhttp`/`splithttp`, HTTPUpgrade) с их настройками.
Так что ссылка Amnezia работает, несёт ли она профиль AmneziaWG или Xray.
### Base64
Содержимое подписок и share-ссылок очень часто закодировано в **base64** — это норма, а не редкий случай. Re:HomeProxy декодирует base64 прозрачно:
- Вставленный блок, представляющий собой один большой base64-блоб, разворачивается в список ссылок.
- Отдельные ссылки `ss://` / `vmess://` несут собственный base64 (userinfo, JSON VMess) — обрабатывается автоматически.
- Ссылки подписок, возвращающие base64-список (классический формат), декодируются при скачивании.
Обычно ничего декодировать вручную не нужно — просто вставьте ссылку или блоб.
При импорте действуют две опции:
- **Разрешить небезопасные** — помечает импортируемые TLS-узлы как `tls_insecure` (пропуск проверки сертификата). Используйте, только если доверяете серверу.
- **Кодирование пакетов (packet encoding)** — применяется к импортируемым узлам VLESS/VMess.
---
## 2. Импорт файла .conf
Кнопка **Импорт WG (.conf) файла** читает файл конфигурации **WireGuard** или **AmneziaWG** `.conf` и создаёт из него узел — удобно для готовых конфигов WARP/AmneziaWG. (Узлы AmneziaWG требуют ядро **sing-box-extended** — см. [Поддерживаемые протоколы](Supported-Protocols-ru).)
---
## 3. Ссылки подписок (с автообновлением)
На вкладке **Подписки** добавьте одну или несколько **ссылок подписок**. Роутер скачивает их и поддерживает список узлов в актуальном состоянии. Re:HomeProxy автоматически распознаёт несколько форматов ответа:
- **Base64 / обычные списки share-ссылок** — классический формат подписки (по одной кодированной ссылке на строку).
- **sing-box JSON / Hiddify** — когда подписка отдаётся как JSON-конфиг sing-box/Hiddify. Нужный формат запрашивается через **User-Agent**, поэтому один и тот же URL может возвращать разным клиентам разные форматы.
- **Xray / V2Ray JSON** — JSON с массивом конфигов (например, в стиле *connliberty*) разбирается в узлы; этот путь также завязан на User-Agent.
### Настройки обновления
- **Автообновление** — включить периодическое обновление; выберите **Время обновления** (час суток, по умолчанию 02:00).
- **Обновлять через прокси** — скачивать подписку через прокси (полезно, если сам хост подписки заблокирован). По умолчанию выключено, чтобы избежать проблемы «курицы и яйца» при старте.
- Узлы также можно обновить вручную из интерфейса.
### Фильтрация узлов подписки
- **Фильтр узлов** — `Отключено`, `Режим чёрного списка` (отбрасывать совпадения) или `Режим белого списка` (оставлять только совпадения).
- **Ключевые слова фильтра** — список совпадений; поддерживается **регулярное выражение**.
Удобно, чтобы отбросить мёртвые/региональные узлы или оставить только нужный набор (например, только определённые страны/протоколы для URLTest).
---
## Какие протоколы можно импортировать?
Зависит от установленного **ядра** — тип узла работает, только если ядро его поддерживает (например, AmneziaWG требует sing-box-extended; некоторым протоколам QUIC/Naive нужны определённые build-теги). Полную матрицу и проверку тегов сборки см. в **[Поддерживаемые протоколы](Supported-Protocols-ru)**.
---
## Примечания
- Импортированные узлы подписки группируются по своей подписке; узлы, добавленные вручную, остаются отдельно.
- Если импорт сообщает «не найдено валидных ссылок», проверьте, что схема — одна из перечисленных выше и ссылка не обрезана.
- Для выбора через URLTest среди множества узлов подписки сочетайте **Фильтр узлов** здесь с целью **URLTest** для конкретного правила в [RU Proxy Rules](Routing-and-Access-Control-ru).
См. также: [Первая настройка](Getting-Started-ru) · [Поддерживаемые протоколы](Supported-Protocols-ru) · [Маршрутизация и контроль доступа](Routing-and-Access-Control-ru) · [Устранение неполадок](Troubleshooting-ru)
@@ -0,0 +1,208 @@
🇬🇧 [English](Supported-Protocols-en) | 🇷🇺 [Русский](Supported-Protocols-ru)
# Supported Protocols
Re:HomeProxy runs on a choice of cores: [hiddify-core](https://github.com/hiddify/hiddify-core) (default) or [sing-box-extended](https://github.com/shtorm-7/sing-box-extended) — both forks of [sing-box](https://sing-box.sagernet.org) with additional protocols and features not available upstream. The protocols shown in the node editor depend on **which core you installed** and how it was compiled on your device. You choose and install the core on the **Core management** section (Services → Re:HomeProxy → Core & Tools).
---
## Filling in the node editor
When you add a node by hand, the editor exposes the underlying **sing-box outbound** options — TLS (including **Reality**, **uTLS** fingerprints, **ECH**), the transport (**gRPC / WebSocket / HTTP / HTTPUpgrade / XHTTP**), and **multiplex**. Rather than duplicate sing-box's reference here, match each field to your server using the upstream docs:
- [Outbound reference](https://sing-box.sagernet.org/configuration/outbound/) — every outbound type and its fields
- [TLS (shared)](https://sing-box.sagernet.org/configuration/shared/tls/) — Reality, uTLS, ECH, ALPN
- [V2Ray transports](https://sing-box.sagernet.org/configuration/shared/v2ray-transport/) — gRPC / WS / HTTP / HTTPUpgrade
- [Multiplex](https://sing-box.sagernet.org/configuration/shared/multiplex/)
Most users never fill these by hand — importing a share link or subscription sets them for you (see [Subscriptions & Node Import](Subscriptions-en)).
---
## Extended-core features (not in upstream sing-box)
Both cores are sing-box forks that add features missing from upstream. Some are **hiddify-core only**; others are in **both** hiddify-core and sing-box-extended (noted per item):
### TLS Fragmentation (`tls_fragment`) *(hiddify-core only)*
Splits the TLS ClientHello handshake across multiple TCP packets so that the SNI (Server Name Indication) field — which reveals the destination domain — arrives in separate fragments. DPI systems that inspect only whole packets to identify and block domains cannot reassemble the SNI in time, so the connection passes through undetected.
**Fragment modes:**
- **SNI/Domain** — splits packets into two pieces; simple and effective in most cases
- **Random** — divides into many very small pieces for maximum obfuscation; use when SNI mode alone is insufficient
**Key parameters:**
- **Fragment size** — recommended 100–200 bytes; ideally one byte less than the domain name length
- **Fragment interval** — timing between fragments; tune per ISP if needed
- **Mixed SNI case** — randomises capitalisation of the SNI (e.g. `wWw.ExAmPlE.cOm`) to defeat case-sensitive matching
- **Padding** — appends random data to the domain field
> **Note:** Do not enable fragment and padding at the same time — they cancel each other out. Effectiveness varies by ISP and may require parameter tuning.
Can be applied to any protocol that uses TLS (VLESS, VMess, Trojan, etc.).
*Source: [How the TLS Trick works and its usage — hiddify.com](https://hiddify.com/manager/basic-concepts-and-troubleshooting/How-the-TLS-Trick-works-and-its-usage/#tls-fragment)*
### XHTTP Transport *(both cores)*
A modern HTTP-based transport for VLESS designed for CDN compatibility and multiplexing. Not in upstream sing-box, but available on **both** hiddify-core and sing-box-extended. See the VLESS section below.
### Additional Protocols *(both cores)*
MieruTCP / MieruUDP and extended NaïveProxy variants — available on **both** cores (see below).
---
## Always Available
### Direct
Bypasses the proxy — traffic goes to the destination without any tunneling. Used in route rules for local or trusted destinations.
### AnyTLS
A TLS-based multiplexing protocol developed by the hiddify team. Designed to be simple, efficient, and hard to fingerprint. Good choice when other TLS-based protocols are being blocked.
### HTTP
Plain HTTP proxy (RFC 7231 CONNECT method). Supports username/password authentication. Not encrypted — use only on trusted networks or wrapped in TLS via another layer.
### Shadowsocks
One of the most widely used anti-censorship protocols. Encrypts traffic with a pre-shared key and cipher (AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305, etc.). Simple to set up. Does not use TLS — traffic is obfuscated but does not look like HTTPS.
**Shadowsocks 2022** (`2022-blake3-aes-128-gcm`, `2022-blake3-aes-256-gcm`, `2022-blake3-chacha20-poly1305`) is also supported. It improves on the original with stronger authenticated encryption, replay protection, and better performance. If your server supports it, prefer the 2022 variants. ShadowTLS (see below) is designed to work with Shadowsocks 2022.
### ShadowTLS
A TLS camouflage wrapper designed for use with **Shadowsocks 2022** (not the original Shadowsocks). Wraps the underlying connection in a TLS handshake that impersonates a real HTTPS server, making traffic indistinguishable from legitimate TLS to passive observers. The TLS handshake is genuine — it completes with a real server — so SNI-based blocking and TLS fingerprinting are defeated. Requires a ShadowTLS-aware server alongside a Shadowsocks 2022 backend.
### Socks
SOCKS4 and SOCKS5 proxy. Supports optional username/password authentication (SOCKS5). No encryption — suitable for use within a trusted network or as a local outbound.
### SSH
Tunnels traffic over an SSH connection using port forwarding. Useful when only SSH is accessible and other ports are blocked. Requires an SSH server with a valid account. Slower than dedicated proxy protocols due to SSH overhead.
### Trojan
Disguises proxy traffic as regular HTTPS by using TLS with a valid certificate. A server hosting Trojan looks identical to an HTTPS web server to outside observers. Falls back to serving a real web page for non-Trojan connections, making it very hard to detect.
**Supported transports:**
| Transport | CDN Compatible | Notes |
|-----------|:--------------:|-------|
| TCP (raw TLS) | No | Default; lowest overhead |
| WebSocket | **Yes** | Works behind Cloudflare and other CDNs |
| gRPC | **Yes** | Works behind Cloudflare (requires gRPC support on CDN) |
| HTTP/2 | No | Multiplexed; not CDN-friendly without WebSocket/gRPC |
WebSocket + TLS is the most common setup for CDN (Cloudflare) deployment — the CDN terminates TLS and forwards WebSocket traffic to the origin server.
### VLESS
A lightweight successor to VMess without the extra encryption layer (relies on the transport for security, typically TLS). Widely used with Xray-based servers.
**Supported transports:**
| Transport | CDN Compatible | Notes |
|-----------|:--------------:|-------|
| TCP (raw TLS) | No | Standard setup |
| WebSocket | **Yes** | CDN-friendly; widely supported |
| gRPC | **Yes** | CDN-friendly |
| HTTP/2 | No | Multiplexed connections |
| HTTPUpgrade | **Yes** | Lightweight HTTP upgrade handshake |
| **XHTTP** | **Yes** | Not in upstream sing-box; on both cores — see below |
**XHTTP** is an extended transport for VLESS, available on **both** hiddify-core and sing-box-extended (but not upstream sing-box). It uses chunked HTTP transfers over a single or multiplexed connection, designed specifically for CDN compatibility and to avoid patterns detectable as non-browser traffic.
### VMess
The original V2Ray protocol. Includes its own encryption on top of the transport layer. Slightly more overhead than VLESS but very widely deployed.
**Supported transports:**
| Transport | CDN Compatible | Notes |
|-----------|:--------------:|-------|
| TCP | No | |
| WebSocket | **Yes** | Most common CDN setup |
| gRPC | **Yes** | |
| HTTP/2 | No | |
| HTTPUpgrade | **Yes** | |
VMess over WebSocket + TLS is one of the most common configurations for Cloudflare CDN deployment — the CDN hides the origin server's real IP.
---
## Requires `with_quic` Build Tag
### Hysteria
QUIC-based high-throughput proxy protocol (version 1). Uses a modified QUIC stack that tolerates packet loss better than TCP-based protocols — useful on lossy or high-latency connections. Version 1 is largely superseded by Hysteria2.
### Hysteria2
Improved and simplified version of Hysteria. Uses the Salamander obfuscation protocol and BBR congestion control. Significantly better performance than v1 and harder to detect. Recommended over Hysteria v1 for new setups.
### TUIC
QUIC-based protocol with built-in multiplexing and 0-RTT connection establishment. Low latency and efficient. Requires a TUIC server (v5 protocol).
---
## Requires `with_naive_outbound` Build Tag
### NaïveProxy
Uses the **actual Chrome network stack** to make proxy traffic indistinguishable from real Chrome browser HTTPS traffic. Extremely resistant to traffic analysis and fingerprinting because the TLS implementation, cipher choices, and behavior are identical to a real browser.
Two transport variants:
| Variant | Protocol | Notes |
|---------|----------|-------|
| **NaiveTLS** | HTTPS (TLS 1.3 over TCP) | Mimics Chrome HTTPS; most common |
| **NaiveQUIC** | HTTP/3 (QUIC) | Mimics Chrome HTTP/3; harder to block but requires UDP |
NaiveTLS is the standard choice. NaiveQUIC offers better performance where UDP is available but may be blocked in some environments that drop unknown UDP traffic.
NaïveProxy requires a compatible server (e.g., Caddy with the `forwardproxy` plugin).
---
## Requires `with_wireguard` + `with_gvisor` Build Tags
### WireGuard
Modern VPN protocol with a minimal codebase and strong cryptography (Curve25519, ChaCha20-Poly1305). Very fast and battery-efficient. Can be used as an outbound to tunnel all proxy traffic through a WireGuard VPN endpoint (e.g., a VPS or a service like Cloudflare WARP).
---
## Requires the sing-box-extended core
These node types are available when you install **sing-box-extended** instead of hiddify-core (pick your core on the **Core Management** page). hiddify-core does **not** support them.
### AmneziaWG
An obfuscated variant of WireGuard. It adds junk packets and randomised handshake headers (the `Jc`, `Jmin`, `Jmax`, `S1`, `S2`, `H1`–`H4`, `I1`–`I5` parameters) so that DPI systems which detect and block plain WireGuard no longer recognise the traffic. Set the obfuscation parameters to match your AmneziaWG server (or a Cloudflare WARP endpoint running AmneziaWG). Same fast Curve25519 / ChaCha20-Poly1305 cryptography as WireGuard, but the packets no longer look like WireGuard on the wire.
---
## Mieru — requires `with_quic`
### MieruTCP / MieruUDP
Anti-censorship protocol developed independently of sing-box, added by the extended cores (hiddify-core **and** sing-box-extended). Uses fully randomized traffic patterns with no identifiable headers or handshakes, making it very difficult to detect or classify by DPI.
- **MieruTCP** — TCP transport variant
- **MieruUDP** — UDP transport variant; higher throughput, requires UDP access
Requires a Mieru server. See the [Mieru project](https://github.com/enfein/mieru) for server setup.
---
## Coming in Future hiddify-core Versions
### DNSTT (planned)
DNS tunneling protocol — tunnels proxy traffic inside DNS queries and responses. Extremely useful in heavily restricted environments where only DNS traffic is allowed (e.g., captive portals, some corporate networks). Significantly lower throughput than other protocols due to DNS packet size limits, but works where nothing else does.
---
## How to Check What Your Build Supports
Run the version command for **whichever core you installed**:
```sh
hiddify-core version # if you run hiddify-core
sing-box version # if you run sing-box-extended
```
Look at the `Tags:` line. Protocols that require a specific tag will only appear in the node editor if that tag is present.
Example:
```
Tags: with_quic,with_wireguard,with_gvisor,with_naive_outbound,...
```
---
## Note on NaïveProxy
NaïveProxy availability is controlled by the `with_naive_outbound` build tag — it is not architecture-specific. Some builds omit it to reduce binary size. Check your installed version as shown above before assuming it is unavailable.
@@ -0,0 +1,208 @@
🇬🇧 [English](Supported-Protocols-en) | 🇷🇺 [Русский](Supported-Protocols-ru)
# Поддерживаемые протоколы
Re:HomeProxy работает на выбор ядра: [hiddify-core](https://github.com/hiddify/hiddify-core) (по умолчанию) или [sing-box-extended](https://github.com/shtorm-7/sing-box-extended) — оба форки [sing-box](https://sing-box.sagernet.org) с дополнительными протоколами и функциями, недоступными в upstream. Отображаемые в редакторе узлов протоколы зависят от того, **какое ядро вы установили** и с какими флагами оно собрано на вашем устройстве. Ядро выбирается и устанавливается в разделе **Управление ядром** (Сервисы → Re:HomeProxy → Ядро и службы).
---
## Заполнение полей в редакторе узлов
Когда вы добавляете узел вручную, редактор открывает базовые опции **outbound-а sing-box** — TLS (включая **Reality**, отпечатки **uTLS**, **ECH**), транспорт (**gRPC / WebSocket / HTTP / HTTPUpgrade / XHTTP**) и **мультиплекс**. Чтобы не дублировать справочник sing-box, сверяйте каждое поле со своим сервером по официальной документации:
- [Справочник outbound-ов](https://sing-box.sagernet.org/configuration/outbound/) — все типы outbound-ов и их поля
- [TLS (общее)](https://sing-box.sagernet.org/configuration/shared/tls/) — Reality, uTLS, ECH, ALPN
- [Транспорты V2Ray](https://sing-box.sagernet.org/configuration/shared/v2ray-transport/) — gRPC / WS / HTTP / HTTPUpgrade
- [Мультиплекс](https://sing-box.sagernet.org/configuration/shared/multiplex/)
Большинству пользователей не нужно заполнять это вручную — импорт share-ссылки или подписки делает это за вас (см. [Подписки и импорт узлов](Subscriptions-ru)).
---
## Возможности расширенных ядер (отсутствуют в upstream sing-box)
Оба ядра — форки sing-box, добавляющие функции, которых нет в upstream. Часть из них **только в hiddify-core**, часть — в **обоих** ядрах (hiddify-core и sing-box-extended), отмечено по каждому пункту:
### Фрагментация TLS (`tls_fragment`) *(только hiddify-core)*
Разбивает TLS ClientHello на несколько TCP-пакетов так, чтобы поле SNI (Server Name Indication) — раскрывающее доменное имя назначения — передавалось в отдельных фрагментах. Системы DPI, анализирующие только целые пакеты для идентификации и блокировки доменов, не успевают собрать SNI воедино, и соединение проходит незамеченным.
**Режимы фрагментации:**
- **SNI/Domain** — разбивает пакеты на два фрагмента; просто и эффективно в большинстве случаев
- **Random** — делит на множество очень мелких фрагментов для максимальной обфускации; используйте, если режима SNI недостаточно
**Ключевые параметры:**
- **Размер фрагмента** — рекомендуется 100–200 байт; в идеале на один байт меньше длины доменного имени
- **Интервал фрагментации** — задержка между фрагментами; настраивайте под конкретного провайдера
- **Mixed SNI case** — рандомизирует регистр символов SNI (например, `wWw.ExAmPlE.cOm`) для обхода блокировок, зависящих от регистра
- **Padding** — добавляет случайные данные к полю домена
> **Важно:** Не включайте фрагментацию и padding одновременно — они взаимно нейтрализуют друг друга. Эффективность зависит от провайдера и может потребовать подбора параметров.
Применяется к любому протоколу, использующему TLS (VLESS, VMess, Trojan и др.).
*Источник: [How the TLS Trick works and its usage — hiddify.com](https://hiddify.com/manager/basic-concepts-and-troubleshooting/How-the-TLS-Trick-works-and-its-usage/#tls-fragment)*
### Транспорт XHTTP *(оба ядра)*
Современный HTTP-транспорт для VLESS, разработанный для совместимости с CDN и мультиплексирования. Отсутствует в upstream sing-box, но доступен в **обоих** ядрах — hiddify-core и sing-box-extended. Подробнее — в разделе VLESS ниже.
### Дополнительные протоколы *(оба ядра)*
MieruTCP / MieruUDP и расширенные варианты NaïveProxy — доступны в **обоих** ядрах (см. ниже).
---
## Всегда доступны
### Direct
Прямое подключение без проксирования. Используется в правилах маршрутизации для локальных или доверенных адресов.
### AnyTLS
TLS-протокол с мультиплексированием, разработанный командой hiddify. Простой, эффективный и сложно поддающийся отпечатыванию. Хороший выбор, когда другие TLS-протоколы блокируются.
### HTTP
Обычный HTTP-прокси (метод CONNECT по RFC 7231). Поддерживает аутентификацию по имени пользователя и паролю. Не шифруется — используйте только в доверенных сетях или в сочетании с TLS-обёрткой.
### Shadowsocks
Один из наиболее распространённых антицензурных протоколов. Шифрует трафик с помощью общего ключа и шифра (AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 и др.). Прост в настройке. Не использует TLS — трафик обфусцирован, но не выглядит как HTTPS.
**Shadowsocks 2022** (`2022-blake3-aes-128-gcm`, `2022-blake3-aes-256-gcm`, `2022-blake3-chacha20-poly1305`) также поддерживается. Улучшенная версия с более стойким аутентифицированным шифрованием, защитой от повторного воспроизведения и лучшей производительностью. Если сервер поддерживает — используйте варианты 2022. ShadowTLS (см. ниже) рассчитан на работу именно с Shadowsocks 2022.
### ShadowTLS
TLS-обёртка, разработанная для использования с **Shadowsocks 2022** (не с оригинальным Shadowsocks). Оборачивает соединение в TLS-рукопожатие, имитирующее настоящий HTTPS-сервер — трафик неотличим от легитимного TLS для пассивного наблюдателя. TLS-рукопожатие настоящее: оно завершается с реальным сервером, поэтому блокировки по SNI и отпечатку TLS не работают. Требует сервер с поддержкой ShadowTLS и бэкенд на Shadowsocks 2022.
### Socks
Прокси SOCKS4 и SOCKS5. Поддерживает опциональную аутентификацию (SOCKS5). Без шифрования — подходит для использования внутри доверенной сети или как локальный outbound.
### SSH
Туннелирует трафик через SSH-соединение с использованием проброса портов. Полезен, когда доступен только SSH, а другие порты заблокированы. Требует SSH-сервер с учётной записью. Медленнее специализированных протоколов из-за накладных расходов SSH.
### Trojan
Маскирует прокси-трафик под обычный HTTPS, используя TLS с действующим сертификатом. Сервер с Trojan выглядит снаружи идентично HTTPS-сайту. Для не-Trojan соединений отдаёт настоящую веб-страницу, что делает его крайне сложным для обнаружения.
**Поддерживаемые транспорты:**
| Транспорт | CDN-совместим | Примечания |
|-----------|:-------------:|-----------|
| TCP (raw TLS) | Нет | По умолчанию; минимальные накладные расходы |
| WebSocket | **Да** | Работает за Cloudflare и другими CDN |
| gRPC | **Да** | Работает за Cloudflare (требует поддержки gRPC на CDN) |
| HTTP/2 | Нет | Мультиплексирование; не CDN-совместим без WS/gRPC |
WebSocket + TLS — наиболее распространённая схема для CDN (Cloudflare): CDN завершает TLS и проксирует WebSocket-трафик на origin-сервер.
### VLESS
Лёгкий преемник VMess без дополнительного слоя шифрования (безопасность обеспечивается транспортом, обычно TLS). Широко используется с серверами на основе Xray.
**Поддерживаемые транспорты:**
| Транспорт | CDN-совместим | Примечания |
|-----------|:-------------:|-----------|
| TCP (raw TLS) | Нет | Стандартная схема |
| WebSocket | **Да** | CDN-совместим; широко поддерживается |
| gRPC | **Да** | CDN-совместим |
| HTTP/2 | Нет | Мультиплексированные соединения |
| HTTPUpgrade | **Да** | Лёгкое HTTP upgrade рукопожатие |
| **XHTTP** | **Да** | Нет в upstream sing-box; есть в обоих ядрах — см. ниже |
**XHTTP** — расширенный транспорт для VLESS, доступный в **обоих** ядрах (hiddify-core и sing-box-extended), но не в upstream sing-box. Использует chunked HTTP-передачу через одиночное или мультиплексированное соединение, специально разработан для совместимости с CDN и устранения паттернов, характерных для не-браузерного трафика.
### VMess
Оригинальный протокол V2Ray. Включает собственное шифрование поверх транспортного слоя. Чуть более высокие накладные расходы по сравнению с VLESS, но очень широко распространён.
**Поддерживаемые транспорты:**
| Транспорт | CDN-совместим | Примечания |
|-----------|:-------------:|-----------|
| TCP | Нет | |
| WebSocket | **Да** | Наиболее распространённая CDN-схема |
| gRPC | **Да** | |
| HTTP/2 | Нет | |
| HTTPUpgrade | **Да** | |
VMess через WebSocket + TLS — одна из наиболее распространённых конфигураций для Cloudflare CDN, скрывающего реальный IP origin-сервера.
---
## Требуют флаг сборки `with_quic`
### Hysteria
Высокопроизводительный прокси-протокол на основе QUIC (версия 1). Использует модифицированный стек QUIC, лучше переносящий потерю пакетов по сравнению с TCP — полезен на нестабильных или высоколатентных соединениях. Версия 1 в основном вытеснена Hysteria2.
### Hysteria2
Улучшенная и упрощённая версия Hysteria. Использует обфускацию Salamander и управление перегрузкой BBR. Значительно лучше по производительности, чем v1, и сложнее обнаруживается. Рекомендуется вместо Hysteria v1 для новых установок.
### TUIC
Протокол на основе QUIC со встроенным мультиплексированием и установкой соединения 0-RTT. Низкая задержка и высокая эффективность. Требует TUIC-сервер (протокол v5).
---
## Требует флаг сборки `with_naive_outbound`
### NaïveProxy
Использует **настоящий сетевой стек Chrome**, делая трафик прокси неотличимым от реального HTTPS-трафика браузера Chrome. Чрезвычайно устойчив к анализу трафика и отпечатыванию — реализация TLS, выбор шифров и поведение идентичны реальному браузеру.
Два варианта транспорта:
| Вариант | Протокол | Примечания |
|---------|----------|-----------|
| **NaiveTLS** | HTTPS (TLS 1.3 over TCP) | Имитирует HTTPS в Chrome; наиболее распространён |
| **NaiveQUIC** | HTTP/3 (QUIC) | Имитирует HTTP/3 в Chrome; сложнее заблокировать, но требует UDP |
NaiveTLS — стандартный выбор. NaiveQUIC даёт лучшую производительность там, где UDP доступен, но может блокироваться в средах, отбрасывающих неизвестный UDP-трафик.
NaïveProxy требует совместимый сервер (например, Caddy с плагином `forwardproxy`).
---
## Требует флаги `with_wireguard` + `with_gvisor`
### WireGuard
Современный VPN-протокол с минимальной кодовой базой и стойкой криптографией (Curve25519, ChaCha20-Poly1305). Очень быстрый и энергоэффективный. Может использоваться как outbound для туннелирования всего прокси-трафика через WireGuard-эндпоинт (VPS или сервисы вроде Cloudflare WARP).
---
## Требуется ядро sing-box-extended
Эти типы узлов доступны, если вместо hiddify-core установить **sing-box-extended** (ядро выбирается на странице **Управление ядром**). hiddify-core их **не** поддерживает.
### AmneziaWG
Маскирующийся вариант WireGuard. Он добавляет мусорные пакеты и рандомизированные заголовки рукопожатия (параметры `Jc`, `Jmin`, `Jmax`, `S1`, `S2`, `H1`–`H4`, `I1`–`I5`), благодаря чему DPI-системы, которые обнаруживают и блокируют обычный WireGuard, перестают распознавать трафик. Задайте параметры маскировки в соответствии с вашим сервером AmneziaWG (или эндпоинтом Cloudflare WARP на AmneziaWG). Та же быстрая криптография Curve25519 / ChaCha20-Poly1305, что и в WireGuard, но пакеты больше не выглядят как WireGuard.
---
## Mieru — требует `with_quic`
### MieruTCP / MieruUDP
Антицензурный протокол, разработанный независимо от sing-box и добавленный расширенными ядрами (hiddify-core **и** sing-box-extended). Использует полностью случайные паттерны трафика без идентифицируемых заголовков или рукопожатий — крайне сложен для обнаружения и классификации системами DPI.
- **MieruTCP** — TCP-вариант транспорта
- **MieruUDP** — UDP-вариант; более высокая пропускная способность, требует доступ к UDP
Требует сервер Mieru. Настройка сервера — в [документации проекта Mieru](https://github.com/enfein/mieru).
---
## Ожидается в будущих версиях hiddify-core
### DNSTT (планируется)
Протокол DNS-туннелирования — передаёт прокси-трафик внутри DNS-запросов и ответов. Крайне полезен в жёстко ограниченных средах, где разрешён только DNS-трафик (например, captive portal, некоторые корпоративные сети). Значительно меньшая пропускная способность по сравнению с другими протоколами из-за ограничений размера DNS-пакетов, но работает там, где ничто другое не работает.
---
## Как проверить возможности вашей сборки
Выполните команду версии для **того ядра, которое установлено**:
```sh
hiddify-core version # если установлен hiddify-core
sing-box version # если установлен sing-box-extended
```
Посмотрите на строку `Tags:`. Протоколы, требующие определённого тега, появятся в редакторе узлов только при его наличии.
Пример вывода:
```
Tags: with_quic,with_wireguard,with_gvisor,with_naive_outbound,...
```
---
## Примечание о NaïveProxy
Доступность NaïveProxy определяется флагом сборки `with_naive_outbound` — это не зависит от архитектуры устройства. Некоторые сборки не включают его для уменьшения размера бинарника. Проверьте установленную версию, как описано выше, прежде чем считать протокол недоступным.
@@ -0,0 +1,123 @@
🇬🇧 [English](Troubleshooting-en) | 🇷🇺 [Русский](Troubleshooting-ru)
# Troubleshooting
---
## "Object not found" Error When Saving
**Symptom:** Saving any form (client settings, access control lists, etc.) shows:
```
RPC call to luci.homeproxy/[method] failed with error -32000: Object not found
```
**Cause:** rpcd has not loaded the `luci.homeproxy` module. This commonly happens right after installing or upgrading the package if the post-install script sent a reload signal while rpcd was not yet running.
**Fix:**
```sh
/etc/init.d/rpcd restart
```
Then **log out and log back in** to the web UI. An existing session does not pick up the new permissions without a fresh login.
---
## Proxy Modes Missing (No TProxy, No Tun)
**Symptom:** The Proxy Mode selector on the Client page only shows "Redirect TCP". TProxy and Tun options are absent even though the required kernel modules are installed.
**Cause:** Same as above — `luci.homeproxy` not loaded. The feature detection call (`singbox_get_features`) returns empty, so all capability flags are false and the options are hidden.
**Fix:** Same as above — restart rpcd and re-login.
---
## TypeError on the Client Page
**Symptom:** Opening `/cgi-bin/luci/admin/services/homeproxy/client` shows:
```
TypeError: Cannot read properties of undefined (reading 'content')
```
**Cause:** The domain list RPC call failed (module not loaded or first boot), and an older version of the package did not handle this gracefully.
**Fix:** Restart rpcd and re-login as above. If you are running an older build, update to the latest release which includes the fix.
---
## All Three Issues Share the Same Root Fix
If you just installed or upgraded the package and see any of the above:
```sh
/etc/init.d/rpcd restart
```
Log out of the web UI and log back in. Both steps are required.
---
## Cannot Download Package via wget (HTTPS Fails)
**Symptom:**
```
Failed to send request: Operation not permitted
Failed to allocate uclient context
```
**Cause:** OpenWRT's built-in `wget` (uclient-fetch) has no SSL library installed, so HTTPS requests fail.
**Fix:**
```sh
opkg update && opkg install libustream-openssl ca-certificates
```
If `opkg` itself cannot reach the internet, download the `.ipk` or `.apk` file on a PC from the [Releases page](https://github.com/1andrevich/homeproxy-hiddify/releases) and transfer it to the router with `scp`.
---
## No Internet Access When Re:HomeProxy Is Enabled (mwan3 Conflict)
**Symptom:** Ping and DNS work, but HTTP/HTTPS traffic fails when Re:HomeProxy is enabled. Stopping mwan3 restores connectivity.
**Cause:** mwan3 uses fwmarks and policy routing to steer traffic between WAN interfaces. When the proxy core makes outbound connections, mwan3 can intercept and misroute them.
**Fix:** Tell the OS to skip mwan3's routing logic for traffic already handled by Re:HomeProxy (marked `0x64`).
Add the following rule as a persistent firewall include:
```sh
uci add firewall include
uci set firewall.@include[-1].path='/etc/firewall.d/homeproxy-mwan3'
uci set firewall.@include[-1].type='script'
uci set firewall.@include[-1].reload='1'
uci commit firewall
mkdir -p /etc/firewall.d
cat > /etc/firewall.d/homeproxy-mwan3 << 'EOF'
#!/bin/sh
iptables -t mangle -I OUTPUT 1 -m mark --mark 0x64/0x64 -j RETURN
EOF
chmod +x /etc/firewall.d/homeproxy-mwan3
/etc/init.d/firewall reload
```
To verify the rule is active:
```sh
iptables -t mangle -L OUTPUT -n --line-numbers | head -5
```
The `0x64` mark rule should appear as line 1.
> This fix deliberately uses `iptables` because **mwan3** operates at the iptables layer. On a fw4 / nftables system it still applies through the iptables-nft compatibility shim — it does not conflict with Re:HomeProxy's nft chains.
---
## Subscription Update Button Does Nothing
**Symptom:** Clicking "Update subscriptions" shows a spinner but node count stays at zero after the page reloads.
**Fix:** Update to the latest release. Earlier builds used a background execution method that did not work correctly from the rpcd context. The current version uses the correct execution path.
If you are on the latest release and still see this, run manually over SSH to check for errors:
```sh
ucode /etc/homeproxy/scripts/update_subscriptions.uc
```
@@ -0,0 +1,123 @@
🇬🇧 [English](Troubleshooting-en) | 🇷🇺 [Русский](Troubleshooting-ru)
# Устранение неполадок
---
## Ошибка «Object not found» при сохранении
**Симптом:** При сохранении любой формы (настройки клиента, списки контроля доступа и др.) появляется:
```
RPC call to luci.homeproxy/[method] failed with error -32000: Object not found
```
**Причина:** rpcd не загрузил модуль `luci.homeproxy`. Чаще всего это происходит сразу после установки или обновления пакета, если скрипт post-install отправил сигнал перезагрузки, когда rpcd ещё не был запущен.
**Решение:**
```sh
/etc/init.d/rpcd restart
```
Затем **выйдите из веб-интерфейса и войдите снова**. Существующая сессия не подхватывает новые разрешения без повторного входа.
---
## Недоступны режимы прокси (нет TProxy, нет Tun)
**Симптом:** Селектор режима прокси на странице «Клиент» показывает только «Redirect TCP». Опции TProxy и Tun отсутствуют, хотя необходимые модули ядра установлены.
**Причина:** Та же, что выше — `luci.homeproxy` не загружен. Вызов определения возможностей (`singbox_get_features`) возвращает пустой объект, все флаги оказываются ложными и опции скрываются.
**Решение:** То же — перезапустить rpcd и войти заново.
---
## TypeError на странице «Клиент»
**Симптом:** При открытии `/cgi-bin/luci/admin/services/homeproxy/client` появляется:
```
TypeError: Cannot read properties of undefined (reading 'content')
```
**Причина:** Вызов RPC для загрузки списков доменов завершился неудачей (модуль не загружен или первая загрузка системы), а старая версия пакета не обрабатывала это корректно.
**Решение:** Перезапустите rpcd и войдите снова, как описано выше. Если вы используете старую сборку — обновитесь до последнего релиза, в котором это исправлено.
---
## Все три проблемы — одно решение
Если вы только что установили или обновили пакет и наблюдаете любую из перечисленных ошибок:
```sh
/etc/init.d/rpcd restart
```
Выйдите из веб-интерфейса и войдите снова. Нужны оба шага.
---
## Не удаётся скачать пакет через wget (HTTPS не работает)
**Симптом:**
```
Failed to send request: Operation not permitted
Failed to allocate uclient context
```
**Причина:** Встроенный `wget` в OpenWRT (uclient-fetch) не имеет установленной SSL-библиотеки и не умеет работать с HTTPS.
**Решение:**
```sh
opkg update && opkg install libustream-openssl ca-certificates
```
Если сам `opkg` не имеет доступа к интернету — скачайте файл `.ipk` или `.apk` на компьютере со [страницы релизов](https://github.com/1andrevich/homeproxy-hiddify/releases) и передайте на роутер через `scp`.
---
## Нет интернета при включённом Re:HomeProxy (конфликт с mwan3)
**Симптом:** Ping и DNS работают, но HTTP/HTTPS-трафик не проходит при включённом Re:HomeProxy. Остановка mwan3 восстанавливает связь.
**Причина:** mwan3 использует fwmark и policy routing для распределения трафика между WAN-интерфейсами. Когда ядро прокси устанавливает исходящие соединения, mwan3 может перехватить их и направить не туда.
**Решение:** Сообщить ОС пропускать логику маршрутизации mwan3 для трафика, уже обработанного Re:HomeProxy (помечен `0x64`).
Добавьте правило в виде постоянного include файрвола:
```sh
uci add firewall include
uci set firewall.@include[-1].path='/etc/firewall.d/homeproxy-mwan3'
uci set firewall.@include[-1].type='script'
uci set firewall.@include[-1].reload='1'
uci commit firewall
mkdir -p /etc/firewall.d
cat > /etc/firewall.d/homeproxy-mwan3 << 'EOF'
#!/bin/sh
iptables -t mangle -I OUTPUT 1 -m mark --mark 0x64/0x64 -j RETURN
EOF
chmod +x /etc/firewall.d/homeproxy-mwan3
/etc/init.d/firewall reload
```
Для проверки, что правило применилось:
```sh
iptables -t mangle -L OUTPUT -n --line-numbers | head -5
```
Правило с меткой `0x64` должно быть первым в списке.
> Здесь намеренно используется `iptables`, потому что **mwan3** работает на уровне iptables. На системе с fw4 / nftables правило всё равно применяется через слой совместимости iptables-nft и не конфликтует с nft-цепочками Re:HomeProxy.
---
## Кнопка обновления подписок не работает
**Симптом:** После нажатия кнопки «Обновить подписки» появляется спиннер, но количество узлов остаётся нулевым после перезагрузки страницы.
**Решение:** Обновитесь до последнего релиза. В более ранних сборках использовался метод фонового выполнения, который некорректно работал в контексте rpcd. В текущей версии используется правильный механизм.
Если вы уже используете последний релиз и проблема сохраняется — запустите вручную через SSH, чтобы увидеть возможные ошибки:
```sh
ucode /etc/homeproxy/scripts/update_subscriptions.uc
```
+116
View File
@@ -0,0 +1,116 @@
🇬🇧 [English](Zapret-en) | 🇷🇺 [Русский](Zapret-ru)
# Zapret
**Zapret 2** is a second, packet-level DPI-bypass built into Re:HomeProxy. It un-throttles and unblocks sites **without any VPN subscription** by desyncing the handshake of selected flows directly on the wire. Re:HomeProxy bundles the [bol-van/zapret2](https://github.com/bol-van/zapret2) engine (`nfqws2`) and manages it for you.
It complements [ByeDPI](ByeDPI-en): same goal, different layer. See [ByeDPI vs Zapret](#byedpi-vs-zapret) below.
---
## What it is — and what it is not
Zapret runs the **`nfqws2`** packet processor. The selected traffic is tagged in the firewall and handed to an **NFQUEUE**, where `nfqws2` rewrites the first few packets of each connection (fake packets, splitting, disorder, TCP-MD5/seq tricks, TLS-record manipulation) so your ISP's Deep Packet Inspection can no longer recognise — and therefore can no longer throttle or block — the traffic.
| | |
|---|---|
| ✅ **Un-throttles DPI-filtered sites** | The common case for YouTube, Discord and similar services slowed or blocked by DPI. |
| ✅ **No subscription, no server** | A local packet trick — no VPN node, account or remote server. |
| ✅ **Works on both cores** | Implemented as a marked direct outbound; works identically on hiddify-core and sing-box-extended. |
| ✅ **Handles TCP *and* UDP/QUIC** | Unlike ByeDPI, the NFQUEUE path also desyncs UDP flows (used for Discord voice). |
| ❌ **Does not encrypt or hide traffic** | Your ISP still sees *which* sites you visit; Zapret only changes *how* the first packets look. It is **not a VPN**. |
| ❌ **Cannot reach fully IP-blocked sites** | If a site is blocked by IP/DNS (not just throttled), only a real proxy/VPN node can reach it. |
---
## How it works
Zapret is wired as a routing **target**, not a SOCKS node:
1. The config generator adds a `direct` outbound called **`zapret-out`** stamped with a `routing_mark` (default mark `110`).
2. When a routing rule selects `zapret-out`, the core egresses that flow **directly**, but tagged with the mark.
3. An nft chain (`homeproxy_zapret_queue`) catches the marked packets and sends the first 1–12 packets of each TCP/UDP connection to an **NFQUEUE**.
4. `nfqws2` reads the queue and desyncs the handshake, then lets the connection continue normally.
Because it is a marked *direct* outbound (not a tunnel), `zapret-out` falls back to plain `direct-out` automatically when Zapret is disabled, so the generated config always stays valid. Loop-avoidance returns for the Zapret mark and `nfqws2`'s own reinjected (DESYNC_MARK) packets are added to the firewall automatically — you don't touch nft.
---
## Enabling Zapret
1. Go to **Services → Re:HomeProxy → Node Settings** and open the **Zapret** tab.
2. If `nfqws2` is not installed yet, use the **Install** button — Re:HomeProxy fetches the right [zapret2-openwrt](https://github.com/1andrevich/zapret2-openwrt) package for your architecture.
3. Tick **Enable**, pick a strategy preset (see below), and **Save & Apply**.
That's all for the engine. The `--qnum` / `--user` / `--fwmark` / `--lua-init` arguments are added automatically — you only edit the desync strategy itself.
---
## Strategies and presets
A "strategy" is the set of `nfqws2` options applied to every flow routed to Zapret. There are **no hostlists** — the routing rules already decide *what* is sent here, so the strategy only decides *how* to desync it. As with ByeDPI, **there is no universal best strategy** — what works depends on your ISP's DPI.
The **Strategy preset** picker fills the strategy field for you and is grouped into two sets:
| Group | What it is |
|-------|------------|
| **Recommended** | A small curated set, purpose-named (e.g. *Default (fake + multidisorder)*, *Multisplit*, *Fake only*). Start here. |
| **Full-test pool** | The larger technique-named pool used by the full tester. |
Presets are shipped read-only in `/etc/homeproxy/zapret_candidates.json` and are based on [bol-van/zapret2](https://github.com/bol-van/zapret2) (nfqws2/blockcheck2) with some adapted from [flowseal/zapret-discord-youtube](https://github.com/flowseal/zapret-discord-youtube) (MIT). You can also leave the preset on *custom* and type your own option string in the **Desync strategy** field.
---
## The strategy tester
Because the right strategy is ISP-specific, the Zapret tab includes a **tester** that never touches your live traffic — it spins up a **temporary NFQUEUE scoped to four test sites** and checks whether each TLS handshake completes:
- **Test current strategy** — runs whatever is in the strategy field against **YouTube**, **Telegram**, **Discord** and **Speedtest.net**, showing each site as ✓ / ✗ with the handshake time, plus an `ok/total` count.
- **Full strategy test** — sweeps the whole preset pool and reports which candidates pass, so you can apply the best one.
Pick a strategy that passes all four sites. If several do, prefer one from the **Recommended** group.
---
## Discord voice (opt-in)
Enabling **Zapret voice** routes Discord's voice-server UDP ranges (and call/voice UDP ports) to `zapret-out` **before** the normal proxy/call rules, so Discord voice is desynced via Zapret instead of being proxied. The UDP port ranges come from [flowseal/zapret-discord-youtube](https://github.com/flowseal/zapret-discord-youtube). This option is honoured only when Zapret is enabled.
---
## Routing traffic through Zapret
Once enabled, **Zapret** becomes a selectable routing target. The typical setup mirrors ByeDPI:
- **Alongside a VPN node (recommended):** in `proxy_banned_ru` mode, open **RU Proxy Rules** and set a *specific list's* target to **Zapret** — e.g. route YouTube and Discord through Zapret (no VPN bandwidth used) while everything else uses your VPN.
- **Per service:** because Zapret is selected per rule, you can mix it freely with the proxy and ByeDPI across different lists.
---
## ByeDPI vs Zapret
Both bypass DPI without a VPN; they operate at different layers, so the right one depends on your ISP.
| | **ByeDPI** | **Zapret 2** |
|---|---|---|
| Layer | Local SOCKS proxy (`ciadpi`) | Kernel NFQUEUE packet mangler (`nfqws2`) |
| Appears as | Selectable in routing rules ("ByeDPI") | Selectable in routing rules ("Zapret") |
| TCP | ✅ | ✅ |
| UDP / QUIC | Carried, but not desynced | ✅ desynced (used for Discord voice) |
| DNS | Forced direct, bypassing ByeDPI (routing it through ByeDPI corrupts DoH/DoT) | Not in the DNS path — Zapret only touches flows routed to it |
| Try when | YouTube throttling, simple per-site offload | ByeDPI doesn't beat your DPI, or you need UDP/voice desync |
There's no harm in installing both and using the tester on each — keep whichever scores all-green on your network.
---
## Troubleshooting
| Symptom | Likely cause / fix |
|---------|--------------------|
| A site still loads slowly or won't open | The strategy isn't right for your ISP — run the **Full strategy test** and apply a passing one. |
| Tester shows everything failing | `nfqws2` may not be installed/running — check the **Zapret** tab and the [Diagnostics](Troubleshooting-en) page (it reports install / running / queue status). |
| Discord voice still drops | Enable **Zapret voice**, then re-test. |
| A site is unreachable, not just slow | It may be IP-blocked, not throttled — Zapret can't help; route it through a proxy/VPN node. |
See also: [ByeDPI](ByeDPI-en) · [Core Management](Core-Management-en) · [Supported Protocols](Supported-Protocols-en) · [Troubleshooting](Troubleshooting-en)
+116
View File
@@ -0,0 +1,116 @@
🇬🇧 [English](Zapret-en) | 🇷🇺 [Русский](Zapret-ru)
# Zapret
**Zapret 2** — второй, пакетный обход DPI, встроенный в Re:HomeProxy. Он снимает троттлинг и разблокирует сайты **без какой-либо VPN-подписки**, рассинхронизируя рукопожатие выбранных потоков прямо на сетевом уровне. Re:HomeProxy включает движок [bol-van/zapret2](https://github.com/bol-van/zapret2) (`nfqws2`) и управляет им за вас.
Он дополняет [ByeDPI](ByeDPI-ru): цель та же, уровень разный. См. [ByeDPI или Zapret](#byedpi-или-zapret) ниже.
---
## Что это — и чем оно не является
Zapret запускает пакетный обработчик **`nfqws2`**. Выбранный трафик помечается в межсетевом экране и передаётся в **NFQUEUE**, где `nfqws2` переписывает первые несколько пакетов каждого соединения (фейковые пакеты, разбиение, переупорядочивание, трюки с TCP-MD5/seq, манипуляции с TLS-record), чтобы система DPI провайдера больше не распознавала трафик — а значит, не могла его замедлять или блокировать.
| | |
|---|---|
| ✅ **Снимает троттлинг сайтов под DPI** | Типичный случай — YouTube, Discord и подобные сервисы, замедленные или заблокированные DPI. |
| ✅ **Без подписки и сервера** | Локальный пакетный трюк — не нужен VPN-узел, аккаунт или удалённый сервер. |
| ✅ **Работает на обоих ядрах** | Реализован как помеченный direct-outbound; одинаково работает на hiddify-core и sing-box-extended. |
| ✅ **Обрабатывает TCP *и* UDP/QUIC** | В отличие от ByeDPI, путь через NFQUEUE рассинхронизирует и UDP-потоки (используется для голоса Discord). |
| ❌ **Не шифрует и не скрывает трафик** | Провайдер по-прежнему видит, *какие* сайты вы посещаете; Zapret меняет только то, *как выглядят* первые пакеты. Это **не VPN**. |
| ❌ **Не открывает сайты, заблокированные по IP** | Если сайт блокируется по IP/DNS (а не просто троттлится), достучаться до него может только настоящий прокси/VPN-узел. |
---
## Как это работает
Zapret подключён как **цель** маршрутизации, а не как SOCKS-узел:
1. Генератор конфига добавляет direct-outbound с именем **`zapret-out`**, помеченный `routing_mark` (метка по умолчанию `110`).
2. Когда правило маршрутизации выбирает `zapret-out`, ядро выпускает поток **напрямую**, но с этой меткой.
3. Цепочка nft (`homeproxy_zapret_queue`) ловит помеченные пакеты и отправляет первые 1–12 пакетов каждого TCP/UDP-соединения в **NFQUEUE**.
4. `nfqws2` читает очередь и рассинхронизирует рукопожатие, после чего соединение продолжается как обычно.
Поскольку это помеченный *direct*-outbound (а не туннель), при выключенном Zapret `zapret-out` автоматически откатывается к обычному `direct-out`, так что генерируемый конфиг всегда остаётся валидным. Правила-исключения для метки Zapret и для собственных переинжектированных пакетов `nfqws2` (DESYNC_MARK) добавляются в межсетевой экран автоматически — править nft вручную не нужно.
---
## Включение Zapret
1. Откройте **Сервисы → Re:HomeProxy → Настройки узлов** и вкладку **Zapret**.
2. Если `nfqws2` ещё не установлен, нажмите **Установить** — Re:HomeProxy скачает подходящий пакет [zapret2-openwrt](https://github.com/1andrevich/zapret2-openwrt) для вашей архитектуры.
3. Отметьте **Включить**, выберите пресет стратегии (см. ниже) и нажмите **Сохранить и применить**.
Этого достаточно. Аргументы `--qnum` / `--user` / `--fwmark` / `--lua-init` добавляются автоматически — вы редактируете только саму стратегию рассинхронизации.
---
## Стратегии и пресеты
«Стратегия» — это набор опций `nfqws2`, применяемых к каждому потоку, направленному в Zapret. **Списков хостов нет** — правила маршрутизации уже решают, *что* сюда отправлять, поэтому стратегия определяет только то, *как* это рассинхронизировать. Как и с ByeDPI, **универсально лучшей стратегии не существует** — что работает, зависит от DPI вашего провайдера.
Выбор **Пресет стратегии** заполняет поле стратегии за вас и сгруппирован на два набора:
| Группа | Что это |
|--------|---------|
| **Рекомендуемые** | Небольшой подобранный набор с понятными названиями (например, *Default (fake + multidisorder)*, *Multisplit*, *Fake only*). Начните отсюда. |
| **Полный пул тестирования** | Более крупный пул с техническими названиями, используемый полным тестером. |
Пресеты поставляются только для чтения в `/etc/homeproxy/zapret_candidates.json` и основаны на [bol-van/zapret2](https://github.com/bol-van/zapret2) (nfqws2/blockcheck2), часть адаптирована из [flowseal/zapret-discord-youtube](https://github.com/flowseal/zapret-discord-youtube) (MIT). Можно оставить пресет в режиме *custom* и ввести свою строку опций в поле **Стратегия рассинхронизации**.
---
## Тестер стратегий
Поскольку нужная стратегия зависит от провайдера, на вкладке Zapret есть **тестер**, который не трогает ваш живой трафик — он поднимает **временную NFQUEUE, ограниченную четырьмя тестовыми сайтами**, и проверяет, завершается ли каждое TLS-рукопожатие:
- **Проверить текущую стратегию** — запускает то, что в поле стратегии, против **YouTube**, **Telegram**, **Discord** и **Speedtest.net**, показывая каждый сайт как ✓ / ✗ со временем рукопожатия и счётчиком `ok/total`.
- **Полная проверка стратегий** — перебирает весь пул пресетов и сообщает, какие кандидаты проходят, чтобы применить лучший.
Выберите стратегию, проходящую все четыре сайта. Если подходит несколько — предпочтите вариант из группы **Рекомендуемые**.
---
## Голос Discord (по желанию)
Включение **Zapret voice** направляет UDP-диапазоны голосовых серверов Discord (и UDP-порты звонков/голоса) в `zapret-out` **до** обычных правил прокси/звонков, так что голос Discord рассинхронизируется через Zapret, а не проксируется. Диапазоны UDP-портов взяты из [flowseal/zapret-discord-youtube](https://github.com/flowseal/zapret-discord-youtube). Опция учитывается только при включённом Zapret.
---
## Маршрутизация трафика через Zapret
После включения **Zapret** становится доступной целью маршрутизации. Типичная схема повторяет ByeDPI:
- **Вместе с VPN-узлом (рекомендуется):** в режиме `proxy_banned_ru` откройте **RU Proxy Rules** и задайте цель *конкретного списка* как **Zapret** — например, направьте YouTube и Discord через Zapret (без расхода трафика VPN), а всё остальное — через VPN.
- **По сервисам:** поскольку Zapret выбирается в каждом правиле, его можно свободно смешивать с прокси и ByeDPI по разным спискам.
---
## ByeDPI или Zapret
Оба обходят DPI без VPN; работают на разных уровнях, поэтому выбор зависит от вашего провайдера.
| | **ByeDPI** | **Zapret 2** |
|---|---|---|
| Уровень | Локальный SOCKS-прокси (`ciadpi`) | Пакетный обработчик NFQUEUE в ядре (`nfqws2`) |
| Отображается как | Выбирается в правилах маршрутизации («ByeDPI») | Выбирается в правилах маршрутизации («Zapret») |
| TCP | ✅ | ✅ |
| UDP / QUIC | Переносится, но не рассинхронизируется | ✅ рассинхронизируется (используется для голоса Discord) |
| DNS | Принудительно резолвится напрямую в обход ByeDPI (через него рассинхрон ломает DoH/DoT) | Не в пути DNS — Zapret трогает только потоки, направленные на него |
| Когда пробовать | Троттлинг YouTube, простой обход по сайтам | Если ByeDPI не справляется с вашим DPI или нужен рассинхрон UDP/голоса |
Ничто не мешает установить оба и прогнать тестер на каждом — оставьте тот, что даёт «всё зелёное» в вашей сети.
---
## Устранение неполадок
| Симптом | Вероятная причина / решение |
|---------|------------------------------|
| Сайт по-прежнему медленно грузится или не открывается | Стратегия не подходит вашему провайдеру — запустите **Полную проверку стратегий** и примените проходящую. |
| Тестер показывает, что всё падает | Возможно, `nfqws2` не установлен/не запущен — проверьте вкладку **Zapret** и страницу [Диагностики](Troubleshooting-ru) (она показывает статус установки / запуска / очереди). |
| Голос Discord по-прежнему рвётся | Включите **Zapret voice** и протестируйте заново. |
| Сайт недоступен, а не просто медленный | Возможно, он заблокирован по IP, а не троттлится — Zapret тут не поможет; направьте его через прокси/VPN-узел. |
См. также: [ByeDPI](ByeDPI-ru) · [Управление ядром](Core-Management-ru) · [Поддерживаемые протоколы](Supported-Protocols-ru) · [Устранение неполадок](Troubleshooting-ru)