mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-30 04:45:13 +08:00
⛄ Sync 2026-09-15 17:04:17
This commit is contained in:
1 parent
569d3c8c73
commit
6840e76cc4
62 files changed
+16332
-19628
No files matched your search
@@ -0,0 +1,2 @@
|
||||
.DS_Store
|
||||
/homeproxy
|
||||
+15
-17
@@ -1,31 +1,29 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2022-2023 ImmortalWrt.org
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy
|
||||
PKG_VERSION:=2.0.0
|
||||
PKG_RELEASE:=13
|
||||
|
||||
LUCI_TITLE:=Sing-box proxy management platform for OpenWrt
|
||||
LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64 (sing-box 1.14)
|
||||
# Pure ucode/JS payload with no compiled code, so the package itself is arch
|
||||
# independent. The real arch constraint comes from the +sing-box dependency,
|
||||
# which the feed builds for every architecture Go supports (aarch64, arm,
|
||||
# mipsel, riscv64, x86_64, ...); where no sing-box package exists, dependency
|
||||
# resolution refuses the install. Checked against the ImmortalWrt 25.12.1
|
||||
# package index (sing-box 1.14.0-r1 published for all of the above).
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_DEPENDS:= \
|
||||
+sing-box \
|
||||
+firewall4 \
|
||||
+dnsmasq-full \
|
||||
+ip-full \
|
||||
+kmod-tun \
|
||||
+kmod-nft-tproxy \
|
||||
+ca-bundle \
|
||||
+curl \
|
||||
+jsonfilter \
|
||||
+unzip \
|
||||
+ucode-mod-digest \
|
||||
+ucode-mod-fs \
|
||||
+ucode-mod-math \
|
||||
+ucode-mod-ubus \
|
||||
+ucode-mod-uci
|
||||
+ip-full \
|
||||
+kmod-tun \
|
||||
+ucode-mod-digest
|
||||
|
||||
PKG_NAME:=luci-app-homeproxy
|
||||
PKG_VERSION:=27.911.1.14
|
||||
PKG_RELEASE:=14
|
||||
|
||||
define Package/luci-app-homeproxy/conffiles
|
||||
/etc/config/homeproxy
|
||||
|
||||
@@ -1,68 +1,102 @@
|
||||
适用于 OpenWrt 24.10 的 HomeProxy LuCI 管理界面,按 `官方内核sing-box 1.13.13` 进行二次定制适配,在官方内核基础上增加功能:
|
||||
1、远程规则集本地持久化
|
||||
2、Clash API 代理供应商
|
||||
3、Clash API 中文名称
|
||||
<div align="center">
|
||||
|
||||
## 一、安装
|
||||
上传 zip 到 OpenWrt /tmp目录后解压并执行(文件名homeproxy-2.0.1-x86_64.zip自行替换):
|
||||
```sh
|
||||
cd /tmp
|
||||
unzip homeproxy-2.0.1-x86_64.zip
|
||||
cd homeproxy-2.0.1-x86_64
|
||||
sh install.sh
|
||||
```
|
||||
# luci-app-homeproxy
|
||||
|
||||
## 二、卸载
|
||||
```sh
|
||||
/etc/init.d/homeproxy stop 2>/dev/null || true
|
||||
/etc/init.d/homeproxy disable 2>/dev/null || true
|
||||
opkg remove luci-i18n-homeproxy-zh-cn 2>/dev/null || true
|
||||
opkg remove luci-app-homeproxy 2>/dev/null || true
|
||||
rm -rf /etc/config/homeproxy
|
||||
rm -rf /etc/homeproxy
|
||||
rm -rf /usr/share/homeproxy
|
||||
rm -rf /www/luci-static/resources/view/homeproxy
|
||||
rm -f /www/luci-static/resources/homeproxy.js
|
||||
rm -f /usr/share/luci/menu.d/luci-app-homeproxy.json
|
||||
rm -f /usr/share/rpcd/acl.d/luci-app-homeproxy.json
|
||||
rm -f /usr/share/rpcd/ucode/luci.homeproxy
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache/*
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart
|
||||
```
|
||||
**The modern ImmortalWrt proxy platform for ARM64 / AMD64**
|
||||
|
||||
## 三、维护工作流
|
||||
- `Rescan-Translation.yml`:代码变更后自动重新扫描并更新翻译文件,也支持手动运行。
|
||||
- `Update-Geodata.yml`:每周自动更新内置 geodata 资源,也支持手动运行。
|
||||
基于 sing-box 1.14 内核的现代代理平台 — 简洁、高效、开箱即用。
|
||||
|
||||
## 四、常见问题
|
||||
1、面版下载失败解决办法
|
||||
第一种:面版设置-UI下载地址,下拉选择需要的UI,点击更新面版手动触发面版下载。
|
||||
第二种:下载面版ZIP包,手动上传面版ZIP,下载地址如下。
|
||||
镜像
|
||||
```sh
|
||||
https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip
|
||||
```
|
||||
或者直连
|
||||
```sh
|
||||
https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip
|
||||
```
|
||||
2、远程规则集是否下载成功判断
|
||||
连接SSH查看/etc/homeproxy/ruleset/目录下是否有规则集文件,有则表示下载成功,反之则没下载成功,检查规则集远程连接。
|
||||
---
|
||||
|
||||
3、安装新包后如果页面仍显示旧文字,建议清浏览器缓存,或执行:
|
||||
```sh
|
||||
rm -f /tmp/luci-indexcache
|
||||
rm -rf /tmp/luci-modulecache/*
|
||||
/etc/init.d/rpcd restart
|
||||
/etc/init.d/uhttpd restart
|
||||
```
|
||||
</div>
|
||||
|
||||
## 五、感谢作者
|
||||
immortalwrt
|
||||
https://github.com/immortalwrt/homeproxy
|
||||
## 项目定位
|
||||
|
||||
VIKINGYFY
|
||||
https://github.com/VIKINGYFY/homeproxy
|
||||
本项目是 [luci-app-homeproxy](https://github.com/szwjp/homeproxy) 的**分拆版本线**:以 sing-box **1.14** 内核为唯一目标,充分结合 1.14 引入的新特性进行升级,不再兼容 1.13 及更早内核。
|
||||
|
||||
| 版本线 | 内核要求 | 演进方式 |
|
||||
| --- | --- | --- |
|
||||
| **homeproxy1.14**(本项目) | sing-box ≥ 1.14 | 1.14 特性驱动,配置生成直接使用 1.14 新格式 |
|
||||
|
||||
## 已融入的 sing-box 1.14 特性
|
||||
|
||||
| 领域 | 特性 |
|
||||
| --- | --- |
|
||||
| DNS | `evaluate` / `match_response` / `race` 响应匹配,乐观缓存与 `store_dns`,统一 `dns.timeout` |
|
||||
| 规则集 | `initial_path`、多 tag、`http_clients` 下载客户端 |
|
||||
| 连接 | TUN `dns_mode` / `dns_address`,UDP NAT 参数(`udp_mapping` / `udp_filtering` / `udp_nat_max`) |
|
||||
| 证书 | `tls.acme` → `certificate_provider`(`key_type` / `profile` / `account_key`) |
|
||||
| 工程化 | 生成配置内置 `$schema`;运行要求 sing-box ≥ 1.14 |
|
||||
|
||||
## 已修复:国内域名走代理问题(r4 / r6 / r7)
|
||||
|
||||
**背景**
|
||||
|
||||
`bypass_mainland_china` 的数据路由原本只用 `geoip-cn`(按 IP 匹配国内目标)。sing-box 不会把「域名连接」的域名解析成 IP 再去匹配 IP 规则集,因此按域名访问的国内站(如字节、百度等)落到 `main-out` 走了代理,只有「裸 IP 访问国内 IP」才直连。
|
||||
|
||||
**修复方式**
|
||||
|
||||
数据路由改用 `action: resolve (prefer_ipv4)` + `geoip-cn → direct-out`——先解析域名再做 IP 匹配,由真实 IP 决定国内 / 国外(国内 IP → 直连,其余 → 代理);数据判定不再依赖 `geosite-cn` / `geosite-noncn` 域名规则(`geosite-cn` 仍用于 DNS 路由)。
|
||||
|
||||
对字节、百度等国内域名,以及苹果等全球跨国企业站点更稳更友好;默认使用公共 DNS(223.5.5.5)作为国内 DNS 以降低污染概率。
|
||||
|
||||
**可选增强 `cn_ip_fallback`**
|
||||
|
||||
启用后使用 1.14 原生 `evaluate` / `match_response`:对非 `geosite-cn` 域名,若 `main-dns` 返回国内 IP,再回退到 `china-dns` 解析。
|
||||
|
||||
> 这版块确实很难两全,改来改去也都存在缺憾,就这样不改了,照顾大多数为准。
|
||||
|
||||
## 新增:规则集自动更新(r5)
|
||||
|
||||
为 `bypass_mainland_china` 的三个远程规则集(`geoip-cn` / `geosite-cn` / `geosite-noncn`)增加 `update_interval: '24h'`,sing-box 每 24 小时自动重新拉取 `.srs`:
|
||||
|
||||
- 热替换,不中断既有连接;
|
||||
- 下载失败时保留旧规则集,不影响分流。
|
||||
|
||||
## 新增:Snell 协议与 Hysteria2 增强(r9)
|
||||
|
||||
面向 sing-box 1.14 的一轮收敛:补齐新协议、暴露传输层新参数,并修复 1.14 DNS 迁移的一个行为缺陷。
|
||||
|
||||
| 领域 | 变更 |
|
||||
| --- | --- |
|
||||
| **Snell(1.14 新协议)** | 客户端出站 + 自建服务端 inbound 全支持(v4 / v6、psk、多用户 `userkey`、HTTP 混淆、v6 流量整形);订阅导入 `snell://` |
|
||||
| **Hysteria2** | 新增 `disable_chrome_parrot`(Ed25519 证书服务器兼容开关)、`bbr_profile`、`hop_interval_max`(跳变间隔随机化)、`gecko` 混淆与 min/max 包长;服务端同步支持 gecko |
|
||||
| **Hysteria v1** | 移除上游已废弃的 QUIC 流控调参(上游 1.16 将删除),回归 sing-box 默认 |
|
||||
| **DNS** | 修正 1.14 自动迁移:legacy `ip_cidr` / `ip_is_private` 包装 `evaluate` 时继承原规则查询条件,不再对全部查询预解析 |
|
||||
| **连接与交互** | TLS `handshake_timeout`;TUN `dns_mode` 缺省(hijack)与平台 DNS 劫持叠加的界面指引;规则集 merged / OR 匹配语义与 `query_type` 兼容性提示 |
|
||||
|
||||
> 设计取舍:Chrome QUIC 指纹(1.14 默认开启)仅在服务器证书为 Ed25519 等握手失败时才需关闭;Snell 采用 sing-box 官方实现(v5 无线协议视同 v4,不支持 v5 QUIC 代理模式)。
|
||||
|
||||
## 新增:规则分流与 TLS 能力补全(r10)
|
||||
|
||||
补齐 sing-box 1.14 在路由规则与规则集加载侧的剩余能力,并加固一处规则集配置的校验提示。
|
||||
|
||||
| 领域 | 变更 |
|
||||
| --- | --- |
|
||||
| **route `resolve`** | 新增 `disable_optimistic_cache`、`timeout`两个控件,乐观缓存增至四个选项 |
|
||||
| **route / route-options** | 新增 `tls_spoof` / `tls_spoof_method`(注入伪造 ClientHello 干扰按 SNI 过滤的中间盒,需特权),LuCI 提供 SNI 与方式选择(默认 wrong-sequence) |
|
||||
| **规则集加载** | 多 tag 时若 `path`/`url`/`initial_path` 缺少 `{tag}` 占位,生成器给出 `warn` 提示,配合 LuCI 表单校验与 sing-box 的硬拒绝(`missing {tag} placeholder`),三层防护避免误配 |
|
||||
| **i18n** | 补齐新增文案的 zh_Hans 译文 |
|
||||
|
||||
## 工程化加固(r11)
|
||||
|
||||
一轮以可维护性与可验证性为目标的加固:重构不改变行为,测试锁住结果,经实机验证。
|
||||
|
||||
| 轮次 | 做了什么 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| 重构和修复错误 | 重构:前端 TLS/传输表单收敛、generator 共享 TLS/transport 构建、`parse_uri` 拆为 13 个协议函数<br>测试:协议单测 153 条、generator 回归、LuCI 表单快照<br>修复:`executeCommand` 清理、dnsmasq 路径告警、fw4 清单单源、启动日志、PEM 校验、`wGET` 失败原因<br>工程:提高中文翻译率 CI、架构核实 | <br>parse_uri 对比零差异、generator JSON 逐字节一致、表单快照逐字段一致<br>顺带修复 3 个既有缺陷:证书上传、fw4 清理回滚、dnsmasq 路径 |
|
||||
|
||||
## 运行要求
|
||||
|
||||
- ImmortalWrt / OpenWrt ≥ 24.10+(apk 或 opkg 均可安装)
|
||||
- sing-box ≥ 1.14.0(ImmortalWrt 25.12 源对应 sing-box 1.14.0-r1)
|
||||
- 低于 1.14 时服务会拒绝启动并记录明确日志
|
||||
|
||||
|
||||
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](LICENSE) 版权归 ImmortalWrt.org 与各贡献者
|
||||
|
||||
</div>
|
||||
@@ -12,157 +12,21 @@
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
function pad2(value) {
|
||||
value = (value == null) ? '' : String(value).trim();
|
||||
return value.length === 1 ? '0' + value : value;
|
||||
}
|
||||
/* Shared wording for the TLS / transport form blocks. Keeping the strings here
|
||||
makes node.js and server.js consume a single definition instead of two
|
||||
copies that have to be edited in lockstep. */
|
||||
const TRANSPORT_NONE_HINT = _('No TCP transport, plain HTTP is merged into the HTTP transport.');
|
||||
const TRANSPORT_HTTP_HINT = _('TLS is not enforced. If TLS is not configured, plain HTTP 1.1 is used.');
|
||||
const TRANSPORT_QUIC_HINT = _('No additional encryption support: It\'s basically duplicate encryption.');
|
||||
|
||||
function normalizeTimePart(value, min, max) {
|
||||
value = parseInt(String(value || '').trim(), 10);
|
||||
if (isNaN(value))
|
||||
value = min;
|
||||
value = Math.max(min, Math.min(max, value));
|
||||
return pad2(value);
|
||||
}
|
||||
const HTTP_IDLE_HEALTH_CHECK_HINT = _('Specifies the period of time (in seconds) after which a health check will be performed using a ping frame if no frames have been received on the connection.<br/>' +
|
||||
'Please note that a ping response is considered a received frame, so if there is no other traffic on the connection, the health check will be executed every interval.');
|
||||
const HTTP_IDLE_GOAWAY_HINT = _('Specifies the time (in seconds) until idle clients should be closed with a GOAWAY frame. PING frames are not considered as activity.');
|
||||
const HTTP_IDLE_KEEPALIVE_HINT = _('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.');
|
||||
|
||||
function parseCron(value) {
|
||||
const matched = String(value || '').trim().match(/^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+([0-7*])$/);
|
||||
if (!matched)
|
||||
return null;
|
||||
|
||||
const minute = +matched[1],
|
||||
hour = +matched[2];
|
||||
if (minute < 0 || minute > 59 || hour < 0 || hour > 23)
|
||||
return null;
|
||||
|
||||
return {
|
||||
minute: normalizeTimePart(matched[1], 0, 59),
|
||||
hour: normalizeTimePart(matched[2], 0, 23),
|
||||
day: matched[3] === '*' ? '*' : (matched[3] === '7' ? '0' : matched[3])
|
||||
};
|
||||
}
|
||||
|
||||
function buildCron(day, hour, minute) {
|
||||
return [
|
||||
normalizeTimePart(minute, 0, 59),
|
||||
normalizeTimePart(hour, 0, 23),
|
||||
'*',
|
||||
'*',
|
||||
String(day || '*')
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function alignCronEditorRow(wrap) {
|
||||
let field = wrap?.parentNode;
|
||||
|
||||
while (field && !field.classList?.contains('cbi-value-field'))
|
||||
field = field.parentNode;
|
||||
|
||||
let row = field?.parentNode;
|
||||
while (row && !row.classList?.contains('cbi-value'))
|
||||
row = row.parentNode;
|
||||
|
||||
if (!row || !field)
|
||||
return false;
|
||||
|
||||
if (wrap._dailyRow) {
|
||||
wrap._dailyRow.style.display = row.style.display;
|
||||
return true;
|
||||
}
|
||||
|
||||
let dailyRow = E('div', { 'class': 'cbi-value homeproxy-cron-daily-row' }, [
|
||||
E('div', { 'class': 'cbi-value-title' }, _('Update time (daily)')),
|
||||
E('div', { 'class': 'cbi-value-field' }, [ wrap._dailyField ])
|
||||
]);
|
||||
|
||||
wrap._dailyRow = dailyRow;
|
||||
row.parentNode.insertBefore(dailyRow, row.nextSibling);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function watchCronEditorRow(wrap) {
|
||||
const align = () => alignCronEditorRow(wrap);
|
||||
[ 0, 50, 200, 500, 1000, 2000 ].forEach(delay => window.setTimeout(align, delay));
|
||||
|
||||
if (typeof MutationObserver !== 'undefined' && document?.body) {
|
||||
const observer = new MutationObserver(() => {
|
||||
if (align())
|
||||
observer.disconnect();
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
window.setTimeout(() => observer.disconnect(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function renderCronEditor(input) {
|
||||
if (!input)
|
||||
return null;
|
||||
|
||||
const parsed = parseCron(input.value);
|
||||
input.type = 'text';
|
||||
input.readOnly = true;
|
||||
input.tabIndex = -1;
|
||||
input.style.position = 'absolute';
|
||||
input.style.opacity = '0';
|
||||
input.style.pointerEvents = 'none';
|
||||
input.style.width = '1px';
|
||||
input.style.height = '1px';
|
||||
input.style.minWidth = '1px';
|
||||
input.style.maxWidth = '1px';
|
||||
input.style.padding = '0';
|
||||
input.style.border = '0';
|
||||
|
||||
let day = E('select', { 'class': 'cbi-input-select', 'style': 'width: 12em !important; min-width: 12em !important; max-width: 12em !important; box-sizing: border-box;' }),
|
||||
hour = E('select', { 'class': 'cbi-input-select', 'style': 'width: 4.25em !important; min-width: 4.25em !important; max-width: 4.25em !important; box-sizing: border-box;' }),
|
||||
minute = E('select', { 'class': 'cbi-input-select', 'style': 'width: 4.25em !important; min-width: 4.25em !important; max-width: 4.25em !important; box-sizing: border-box;' }),
|
||||
dailyField = E('div', { 'style': 'display: flex; align-items: center; gap: .25em; width: 9.5em;' }, [
|
||||
hour,
|
||||
E('span', ':'),
|
||||
minute
|
||||
]),
|
||||
wrap = E('div', { 'class': 'homeproxy-cron-editor' }, [ day ]);
|
||||
|
||||
wrap._dailyField = dailyField;
|
||||
|
||||
for (let i = 0; i < 24; i++)
|
||||
hour.appendChild(E('option', { value: pad2(i) }, pad2(i)));
|
||||
|
||||
for (let i = 0; i < 60; i++)
|
||||
minute.appendChild(E('option', { value: pad2(i) }, pad2(i)));
|
||||
|
||||
[
|
||||
['*', _('Every day')],
|
||||
['1', _('Every Monday')],
|
||||
['2', _('Every Tuesday')],
|
||||
['3', _('Every Wednesday')],
|
||||
['4', _('Every Thursday')],
|
||||
['5', _('Every Friday')],
|
||||
['6', _('Every Saturday')],
|
||||
['0', _('Every Sunday')]
|
||||
].forEach(([value, label]) => day.appendChild(E('option', { value }, label)));
|
||||
|
||||
day.value = parsed ? parsed.day : '*';
|
||||
hour.value = parsed ? parsed.hour : '00';
|
||||
minute.value = parsed ? parsed.minute : '00';
|
||||
input.value = buildCron(day.value, hour.value, minute.value);
|
||||
input.setAttribute('value', input.value);
|
||||
|
||||
const sync = () => {
|
||||
input.value = buildCron(day.value, hour.value, minute.value);
|
||||
input.setAttribute('value', input.value);
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
};
|
||||
|
||||
day.addEventListener('change', sync);
|
||||
hour.addEventListener('change', sync);
|
||||
minute.addEventListener('change', sync);
|
||||
wrap.appendChild(input);
|
||||
window.setTimeout(() => watchCronEditorRow(wrap), 0);
|
||||
return wrap;
|
||||
}
|
||||
const HTTP_PING_HEALTH_CHECK_HINT = _('Specifies the timeout duration (in seconds) after sending a PING frame, within which a response must be received.<br/>' +
|
||||
'If a response to the PING frame is not received within the specified timeout duration, the connection will be closed.');
|
||||
const HTTP_PING_KEEPALIVE_HINT = _('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.');
|
||||
|
||||
return baseclass.extend({
|
||||
dns_strategy: {
|
||||
@@ -238,28 +102,225 @@ return baseclass.extend({
|
||||
}
|
||||
}),
|
||||
|
||||
CBIMultiValue: form.MultiValue.extend({
|
||||
__name__: 'CBI.HomeProxyMultiValue',
|
||||
/* Build the transport option group shared by the node (client) and server
|
||||
forms. `options.side` selects the client/server wording and the handful of
|
||||
fields that only exist on one side; option names, defaults and dependency
|
||||
sets are otherwise identical. */
|
||||
renderTransportOptions(section, options) {
|
||||
const features = options.features || {},
|
||||
side = options.side || 'client';
|
||||
let o;
|
||||
|
||||
renderWidget(section_id, _option_index, cfgvalue) {
|
||||
let value = (cfgvalue != null) ? cfgvalue : this.default,
|
||||
choices = this.transformChoices() || {},
|
||||
widget = new ui.Dropdown(L.toArray(value), choices, {
|
||||
id: this.cbid(section_id),
|
||||
sort: this.keylist,
|
||||
multiple: true,
|
||||
optional: this.optional || this.rmempty,
|
||||
select_placeholder: this.placeholder,
|
||||
create: this.create,
|
||||
display_items: this.display_size ?? this.size ?? 3,
|
||||
dropdown_items: this.dropdown_size ?? this.size ?? -1,
|
||||
validate: this.getValidator(section_id),
|
||||
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
|
||||
});
|
||||
o = section.option(form.ListValue, 'transport', _('Transport'), TRANSPORT_NONE_HINT);
|
||||
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 = TRANSPORT_HTTP_HINT;
|
||||
else if (value === 'quic')
|
||||
desc.innerHTML = TRANSPORT_QUIC_HINT;
|
||||
else
|
||||
desc.innerHTML = TRANSPORT_NONE_HINT;
|
||||
|
||||
return widget.render();
|
||||
let tls = this.map.findElement('id', 'cbid.homeproxy.%s.tls'.format(section_id)).firstElementChild;
|
||||
if ((value === 'http' && tls.checked) || (value === 'grpc' && !features.with_grpc)) {
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
(side === 'server') ? HTTP_IDLE_GOAWAY_HINT : HTTP_IDLE_HEALTH_CHECK_HINT;
|
||||
|
||||
if (side !== 'server')
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_ping_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
HTTP_PING_HEALTH_CHECK_HINT;
|
||||
} else if (value === 'grpc' && features.with_grpc) {
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_idle_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
HTTP_IDLE_KEEPALIVE_HINT;
|
||||
|
||||
if (side !== 'server')
|
||||
this.map.findElement('id', 'cbid.homeproxy.%s.http_ping_timeout'.format(section_id)).nextElementSibling.innerHTML =
|
||||
HTTP_PING_KEEPALIVE_HINT;
|
||||
}
|
||||
}
|
||||
}),
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.option(form.Value, 'grpc_servicename', _('gRPC service name'));
|
||||
o.depends('transport', 'grpc');
|
||||
o.modalonly = true;
|
||||
|
||||
if (side !== 'server' && features.with_grpc) {
|
||||
o = section.option(form.Flag, 'grpc_permit_without_stream', _('gRPC permit without stream'),
|
||||
_('If enabled, the client transport sends keepalive pings even with no active connections.'));
|
||||
o.depends('transport', 'grpc');
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
o = section.option(form.DynamicList, 'http_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('transport', 'http');
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.option(form.Value, 'httpupgrade_host', _('Host'));
|
||||
o.datatype = 'hostname';
|
||||
o.depends('transport', 'httpupgrade');
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.option(form.Value, 'http_path', _('Path'));
|
||||
o.depends('transport', 'http');
|
||||
o.depends('transport', 'httpupgrade');
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.option(form.Value, 'http_method', _('Method'));
|
||||
if (side !== 'server') {
|
||||
o.value('GET', _('GET'));
|
||||
o.value('PUT', _('PUT'));
|
||||
}
|
||||
o.depends('transport', 'http');
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.option(form.Value, 'http_idle_timeout', _('Idle timeout'),
|
||||
(side === 'server') ? HTTP_IDLE_GOAWAY_HINT : HTTP_IDLE_HEALTH_CHECK_HINT);
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('transport', 'grpc');
|
||||
o.depends({'transport': 'http', 'tls': '1'});
|
||||
o.modalonly = true;
|
||||
|
||||
if (side !== 'server' || features.with_grpc) {
|
||||
o = section.option(form.Value, 'http_ping_timeout', _('Ping timeout'),
|
||||
(side === 'server') ? HTTP_PING_KEEPALIVE_HINT : HTTP_PING_HEALTH_CHECK_HINT);
|
||||
o.datatype = 'uinteger';
|
||||
o.depends('transport', 'grpc');
|
||||
if (side !== 'server')
|
||||
o.depends({'transport': 'http', 'tls': '1'});
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
o = section.option(form.Value, 'ws_host', _('Host'));
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.option(form.Value, 'ws_path', _('Path'));
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.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 = section.option(form.Value, 'websocket_early_data_header', _('Early data header name'),
|
||||
(side === 'server') ? (_('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>.')) : undefined);
|
||||
o.value('Sec-WebSocket-Protocol');
|
||||
o.depends('transport', 'ws');
|
||||
o.modalonly = true;
|
||||
|
||||
if (side !== 'server') {
|
||||
o = section.option(form.ListValue, 'packet_encoding', _('Packet encoding'));
|
||||
o.value('', _('none'));
|
||||
o.value('packetaddr', _('packet addr (v2ray-core v5+)'));
|
||||
o.value('xudp', _('Xudp (Xray-core)'));
|
||||
o.depends('type', 'vless');
|
||||
o.depends('type', 'vmess');
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
return o;
|
||||
},
|
||||
|
||||
/* Build the TLS option group shared by the node (client) and server forms.
|
||||
`options.type_depends` lists the node types the TLS flag applies to,
|
||||
`options.tls_forced_types` the types that force TLS on, and
|
||||
`options.oninsecurechange` the client-side confirm handler. */
|
||||
renderTlsOptions(section, options) {
|
||||
const side = options.side || 'client';
|
||||
let o;
|
||||
|
||||
o = section.option(form.Flag, 'tls', _('TLS'));
|
||||
for (let t of (options.type_depends || []))
|
||||
o.depends('type', t);
|
||||
if (side === 'server')
|
||||
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 ((options.tls_forced_types || []).includes(type)) {
|
||||
tls.checked = true;
|
||||
tls.disabled = true;
|
||||
} else {
|
||||
tls.disabled = null;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.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 = section.option(form.DynamicList, 'tls_alpn', _('TLS ALPN'),
|
||||
_('List of supported application level protocols, in order of preference.'));
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
if (side !== 'server') {
|
||||
o = section.option(form.Flag, 'tls_insecure', _('Allow insecure'),
|
||||
_('Allow insecure connection at TLS client.') +
|
||||
'<br/>' +
|
||||
_('This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!'));
|
||||
o.depends('tls', '1');
|
||||
o.onchange = options.oninsecurechange;
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
o = section.option(form.ListValue, 'tls_min_version', _('Minimum TLS version'),
|
||||
_('The minimum TLS version that is acceptable.'));
|
||||
o.value('', _('default'));
|
||||
for (let i of this.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.option(form.ListValue, 'tls_max_version', _('Maximum TLS version'),
|
||||
_('The maximum TLS version that is acceptable.'));
|
||||
o.value('', _('default'));
|
||||
for (let i of this.tls_versions)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = section.option(this.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 this.tls_cipher_suites)
|
||||
o.value(i);
|
||||
o.depends('tls', '1');
|
||||
o.optional = true;
|
||||
o.modalonly = true;
|
||||
|
||||
if (side !== 'server') {
|
||||
o = section.option(form.Value, 'tls_handshake_timeout', _('Handshake timeout (1.14)'),
|
||||
_('TLS handshake timeout in seconds. 15s is used by default.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.placeholder = '15';
|
||||
o.depends('tls', '1');
|
||||
o.modalonly = true;
|
||||
}
|
||||
|
||||
return o;
|
||||
},
|
||||
|
||||
calcStringMD5(e) {
|
||||
/* Thanks to https://stackoverflow.com/a/41602636 */
|
||||
@@ -360,49 +421,6 @@ return baseclass.extend({
|
||||
return L.resolveDefault(callGetSingBoxFeatures(), {});
|
||||
},
|
||||
|
||||
renderCronSelector(/* ... */) {
|
||||
if (!this._homeproxyCronOriginalFormvalue) {
|
||||
this._homeproxyCronOriginalFormvalue = this.formvalue;
|
||||
this.formvalue = function(section_id) {
|
||||
let cbid = this.cbid(section_id),
|
||||
ids = [ cbid, 'widget.' + cbid ],
|
||||
roots = [ document.getElementById('modal_overlay'), document ],
|
||||
input = null;
|
||||
|
||||
for (let root of roots) {
|
||||
if (!root)
|
||||
continue;
|
||||
|
||||
for (let el of root.querySelectorAll('input')) {
|
||||
if (ids.includes(el.id) || ids.includes(el.name)) {
|
||||
input = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (input)
|
||||
break;
|
||||
}
|
||||
|
||||
let value = input?.value ||
|
||||
this._homeproxyCronOriginalFormvalue?.call(this, section_id) ||
|
||||
this.default ||
|
||||
'0 0 * * *';
|
||||
|
||||
return parseCron(value) ? value : (this.default || '0 0 * * *');
|
||||
};
|
||||
}
|
||||
|
||||
let node = form.Value.prototype.renderWidget.apply(this, arguments),
|
||||
editor = renderCronEditor(node.querySelector('input'));
|
||||
|
||||
return editor || node;
|
||||
},
|
||||
|
||||
renderCronSelectorRow(/* ... */) {
|
||||
return form.Value.prototype.render.apply(this, arguments);
|
||||
},
|
||||
|
||||
generateRand(type, length) {
|
||||
let byteArr;
|
||||
if (['base64', 'hex'].includes(type))
|
||||
@@ -426,9 +444,6 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
loadDefaultLabel(uciconfig, ucisection) {
|
||||
if (!ucisection)
|
||||
return '';
|
||||
|
||||
let label = uci.get(uciconfig, ucisection, 'label');
|
||||
if (label) {
|
||||
return label;
|
||||
@@ -439,127 +454,32 @@ return baseclass.extend({
|
||||
},
|
||||
|
||||
loadModalTitle(title, addtitle, uciconfig, ucisection) {
|
||||
if (!ucisection)
|
||||
return addtitle;
|
||||
|
||||
let label = uci.get(uciconfig, ucisection, 'label');
|
||||
return label ? title + ' » ' + label : addtitle;
|
||||
},
|
||||
|
||||
normalizeSectionId(label, prefix) {
|
||||
let section_id = (label || '').trim().replace(/[^A-Za-z0-9_]/g, '_').replace(/^_+|_+$/g, '');
|
||||
return (prefix || 'cfg') + '_' + (section_id || 'section');
|
||||
},
|
||||
|
||||
renderSectionAdd(section, extra_class) {
|
||||
let el = form.GridSection.prototype.renderSectionAdd.apply(section, [ extra_class ]),
|
||||
nameEl = el.querySelector('.cbi-section-create-name'),
|
||||
button = el.querySelector('.cbi-section-create > .cbi-button-add'),
|
||||
uciconfig = section.uciconfig || section.map.config,
|
||||
sectiontype = section.sectiontype,
|
||||
labelEl = E('input', {
|
||||
'type': 'text',
|
||||
'class': nameEl.className
|
||||
}),
|
||||
hintEl = E('div', { 'class': 'cbi-value-description' });
|
||||
nameEl = el.querySelector('.cbi-section-create-name');
|
||||
ui.addValidator(nameEl, 'uciname', true, (v) => {
|
||||
let button = el.querySelector('.cbi-section-create > .cbi-button-add');
|
||||
let uciconfig = section.uciconfig || section.map.config;
|
||||
|
||||
nameEl.style.display = 'none';
|
||||
nameEl.parentNode.insertBefore(labelEl, nameEl);
|
||||
nameEl.parentNode.appendChild(hintEl);
|
||||
button.disabled = true;
|
||||
|
||||
const syncSectionName = () => {
|
||||
let label = (labelEl.value || '').trim();
|
||||
hintEl.textContent = '';
|
||||
|
||||
if (!label) {
|
||||
nameEl.value = '';
|
||||
if (!v) {
|
||||
button.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
let duplicate = false;
|
||||
uci.sections(uciconfig, sectiontype, (res) => {
|
||||
if ((res.label || res['.name']) === label)
|
||||
duplicate = true;
|
||||
});
|
||||
|
||||
if (duplicate) {
|
||||
nameEl.value = '';
|
||||
return true;
|
||||
} else if (uci.get(uciconfig, v)) {
|
||||
button.disabled = true;
|
||||
hintEl.textContent = _('Expecting: %s').format(_('unique value'));
|
||||
return;
|
||||
return _('Expecting: %s').format(_('unique UCI identifier'));
|
||||
} else {
|
||||
button.disabled = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
let normalized = (label || '').trim().replace(/[^A-Za-z0-9_]/g, '_').replace(/^_+|_+$/g, '') || 'section',
|
||||
section_id = (sectiontype || 'cfg') + '_' + normalized,
|
||||
suffix = 1;
|
||||
while (uci.get(uciconfig, section_id))
|
||||
section_id = (sectiontype || 'cfg') + '_' + normalized + '_' + suffix++;
|
||||
|
||||
nameEl.value = section_id;
|
||||
nameEl.dataset.sectionId = section_id;
|
||||
nameEl.dataset.sectionLabel = label;
|
||||
button.disabled = null;
|
||||
};
|
||||
|
||||
labelEl.addEventListener('input', syncSectionName);
|
||||
labelEl.addEventListener('blur', syncSectionName);
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
syncSectionName();
|
||||
|
||||
let label = nameEl.dataset.sectionLabel,
|
||||
section_id = nameEl.dataset.sectionId;
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (label && section_id && uci.get(uciconfig, section_id))
|
||||
uci.set(uciconfig, section_id, 'label', label);
|
||||
}, 0);
|
||||
});
|
||||
}, 'blur', 'keyup');
|
||||
|
||||
return el;
|
||||
},
|
||||
|
||||
installCloseButtonText() {
|
||||
if (window.__homeproxyCloseButtonText)
|
||||
return;
|
||||
|
||||
window.__homeproxyCloseButtonText = true;
|
||||
|
||||
let fix = (root) => {
|
||||
for (let el of (root || document).querySelectorAll('button, .btn, input[type="button"], input[type="submit"]')) {
|
||||
let text = (el.textContent || el.value || '').trim();
|
||||
if (text !== 'Dismiss' && text !== '\u5ffd\u7565')
|
||||
continue;
|
||||
|
||||
if ('value' in el && !el.textContent.trim())
|
||||
el.value = _('Close');
|
||||
else
|
||||
el.textContent = _('Close');
|
||||
}
|
||||
};
|
||||
|
||||
let observe = () => {
|
||||
if (!document.body)
|
||||
return;
|
||||
|
||||
fix(document);
|
||||
new MutationObserver((mutations) => {
|
||||
for (let mutation of mutations)
|
||||
for (let node of mutation.addedNodes)
|
||||
if (node.nodeType === 1)
|
||||
fix(node);
|
||||
fix(document);
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
};
|
||||
|
||||
if (document.body)
|
||||
observe();
|
||||
else
|
||||
document.addEventListener('DOMContentLoaded', observe, { once: true });
|
||||
},
|
||||
|
||||
uploadCertificate(_option, type, filename, ev) {
|
||||
const callWriteCertificate = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
@@ -580,44 +500,6 @@ return baseclass.extend({
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message)) });
|
||||
},
|
||||
|
||||
uploadPanel(_option, ev) {
|
||||
const callInstallPanel = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'clash_api_install_panel',
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const normalizeMessage = (message) => {
|
||||
switch (message) {
|
||||
case 'panel_backup_failed':
|
||||
return _('Backup panel failed.');
|
||||
case 'panel_restore_failed':
|
||||
return _('Restore panel failed.');
|
||||
case 'empty_panel_zip':
|
||||
return _('Panel ZIP file is empty.');
|
||||
case 'unzip_unavailable':
|
||||
return _('The unzip command is unavailable.');
|
||||
case 'invalid_panel_zip':
|
||||
return _('Invalid panel ZIP file.');
|
||||
case 'panel_install_failed':
|
||||
return _('Install panel failed.');
|
||||
default:
|
||||
return message || _('unknown error');
|
||||
}
|
||||
};
|
||||
|
||||
return ui.uploadFile('/tmp/homeproxy_panel.tmp', ev.target)
|
||||
.then(L.bind((_btn, res) => {
|
||||
return L.resolveDefault(callInstallPanel(), {}).then((ret) => {
|
||||
if (ret.result === true)
|
||||
ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(_('panel ZIP package'), res.size)), 'info');
|
||||
else
|
||||
ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(_('panel ZIP package'), normalizeMessage(ret.error))), 'danger');
|
||||
});
|
||||
}, this, ev.target))
|
||||
.catch((e) => { ui.addNotification(null, E('p', e.message || e), 'danger') });
|
||||
},
|
||||
|
||||
validateBase64Key(length, section_id, value) {
|
||||
/* Thanks to luci-proto-wireguard */
|
||||
if (section_id && value)
|
||||
@@ -644,7 +526,8 @@ return baseclass.extend({
|
||||
else if (!value[2])
|
||||
value[2] = 65535;
|
||||
|
||||
if (value[1] < value[2] && value[2] <= 65535)
|
||||
/* numeric comparison: string '<' does lexicographic ordering */
|
||||
if (parseInt(value[1], 10) < parseInt(value[2], 10) && value[2] <= 65535)
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -658,7 +541,7 @@ return baseclass.extend({
|
||||
if (section_id) {
|
||||
if (!value)
|
||||
return _('Expecting: %s').format(_('non-empty value'));
|
||||
if (ucioption === 'node' && (value === 'urltest' || value === 'selector'))
|
||||
if (ucioption === 'node' && value === 'urltest')
|
||||
return true;
|
||||
|
||||
let duplicate = false;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -126,8 +126,6 @@ return view.extend({
|
||||
let m, s, o;
|
||||
let features = data[1];
|
||||
|
||||
hp.installCloseButtonText();
|
||||
|
||||
m = new form.Map('homeproxy', _('HomeProxy Server'),
|
||||
_('The modern ImmortalWrt proxy platform for ARM64/AMD64.'));
|
||||
|
||||
@@ -184,13 +182,13 @@ return view.extend({
|
||||
}
|
||||
o.value('mixed', _('Mixed'));
|
||||
o.value('shadowsocks', _('Shadowsocks'));
|
||||
o.value('snell', _('Snell (1.14)'));
|
||||
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.default = 'mixed';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'address', _('Listen address'));
|
||||
@@ -216,12 +214,13 @@ return view.extend({
|
||||
o.depends({'type': /^(http|mixed|naive|socks)$/, 'username': /[\s\S]/});
|
||||
o.depends('type', 'hysteria2');
|
||||
o.depends('type', 'shadowsocks');
|
||||
o.depends('type', 'snell');
|
||||
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' ];
|
||||
let required_type = [ 'anytls', 'http', 'mixed', 'naive', 'shadowsocks', 'snell', 'socks', 'trojan' ];
|
||||
|
||||
if (required_type.includes(type)) {
|
||||
if (type === 'shadowsocks') {
|
||||
@@ -291,6 +290,7 @@ return view.extend({
|
||||
o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
|
||||
o.value('', _('Disable'));
|
||||
o.value('salamander', _('Salamander'));
|
||||
o.value('gecko', _('Gecko (1.14)'));
|
||||
o.depends('type', 'hysteria2');
|
||||
o.modalonly = true;
|
||||
|
||||
@@ -300,30 +300,18 @@ return view.extend({
|
||||
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 = s.option(form.Value, 'hysteria_obfs_min_packet_size', _('Min obfs packet size (1.14)'),
|
||||
_('Minimum on-wire packet size in bytes. Gecko only.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '67108864';
|
||||
o.depends('type', 'hysteria');
|
||||
o.placeholder = '512';
|
||||
o.depends({'type': 'hysteria2', 'hysteria_obfs_type': 'gecko'});
|
||||
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 = s.option(form.Value, 'hysteria_obfs_max_packet_size', _('Max obfs packet size (1.14)'),
|
||||
_('Maximum on-wire packet size in bytes. Gecko only.'));
|
||||
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.placeholder = '1200';
|
||||
o.depends({'type': 'hysteria2', 'hysteria_obfs_type': 'gecko'});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'),
|
||||
@@ -337,6 +325,31 @@ return view.extend({
|
||||
o.modalonly = true;
|
||||
/* Hysteria (2) config end */
|
||||
|
||||
/* Snell config start */
|
||||
o = s.option(form.ListValue, 'snell_version', _('Snell version'),
|
||||
_('The pre-shared key (password) above must be 12-255 bytes for v6.'));
|
||||
o.value('5', _('v5'));
|
||||
o.value('6', _('v6'));
|
||||
o.default = '5';
|
||||
o.depends('type', 'snell');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'snell_obfs_mode', _('Obfuscation mode'),
|
||||
_('HTTP obfuscation. v5 only.'));
|
||||
o.value('', _('none'));
|
||||
o.value('http', _('http'));
|
||||
o.depends({'type': 'snell', 'snell_version': '5'});
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'snell_mode', _('Traffic shaping mode'),
|
||||
_('v6 only.'));
|
||||
o.value('', _('default'));
|
||||
o.value('unshaped', _('unshaped'));
|
||||
o.value('unsafe-raw', _('unsafe-raw'));
|
||||
o.depends({'type': 'snell', 'snell_version': '6'});
|
||||
o.modalonly = true;
|
||||
/* Snell config end */
|
||||
|
||||
/* Shadowsocks config */
|
||||
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
||||
for (let i of hp.shadowsocks_encrypt_methods)
|
||||
@@ -399,104 +412,7 @@ return view.extend({
|
||||
/* 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 */
|
||||
|
||||
hp.renderTransportOptions(s, { features: features, side: 'server' });
|
||||
/* Transport config end */
|
||||
|
||||
/* Mux config start */
|
||||
@@ -532,67 +448,11 @@ return view.extend({
|
||||
/* 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;
|
||||
hp.renderTlsOptions(s, {
|
||||
side: 'server',
|
||||
type_depends: [ 'anytls', 'http', 'hysteria', 'hysteria2', 'naive', 'trojan', 'tuic', 'vless', 'vmess' ],
|
||||
tls_forced_types: [ 'hysteria', 'hysteria2', 'tuic' ]
|
||||
});
|
||||
|
||||
if (features.with_acme) {
|
||||
o = s.option(form.Flag, 'tls_acme', _('Enable ACME'),
|
||||
@@ -627,6 +487,29 @@ return view.extend({
|
||||
}
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_key_type', _('ACME key type'),
|
||||
_('Private key type for new certificates (1.14). Empty uses the default.'));
|
||||
o.value('ed25519');
|
||||
o.value('p256');
|
||||
o.value('p384');
|
||||
o.value('rsa2048');
|
||||
o.value('rsa4096');
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.Value, 'tls_acme_profile', _('ACME profile'),
|
||||
_('ACME profile for certificate issuance (1.14). Example: shortlived.'));
|
||||
o.placeholder = 'shortlived';
|
||||
o.depends('tls_acme', '1');
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.TextValue, 'tls_acme_account_key', _('ACME account key'),
|
||||
_('PEM-encoded private key of an existing ACME account (1.14).'));
|
||||
o.monospace = true;
|
||||
o.rows = 3;
|
||||
o.depends('tls_acme', '1');
|
||||
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'));
|
||||
@@ -640,10 +523,10 @@ return view.extend({
|
||||
o.modalonly = true;
|
||||
|
||||
o = s.option(form.ListValue, 'tls_dns01_provider', _('DNS provider'));
|
||||
o.value('alidns', _('AliYun'));
|
||||
o.value('cfdns', _('CloudFlare'));
|
||||
o.value('alidns', _('Alibaba Cloud DNS'));
|
||||
o.value('cloudflare', _('Cloudflare'));
|
||||
o.depends('tls_dns01_challenge', '1');
|
||||
o.default = 'cfdns';
|
||||
o.default = 'cloudflare';
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
@@ -666,7 +549,7 @@ return view.extend({
|
||||
|
||||
o = s.option(form.Value, 'tls_dns01_cf_api_token', _('API token'));
|
||||
o.password = true;
|
||||
o.depends('tls_dns01_provider', 'cfdns');
|
||||
o.depends('tls_dns01_provider', 'cloudflare');
|
||||
o.rmempty = false;
|
||||
o.modalonly = true;
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
'require homeproxy as hp';
|
||||
|
||||
/* Thanks to luci-app-aria2 */
|
||||
const css = ' \
|
||||
#log_textarea { \
|
||||
@@ -29,53 +27,6 @@ const css = ' \
|
||||
} \
|
||||
.description { \
|
||||
background-color: #33ccff; \
|
||||
} \
|
||||
.homeproxy-connect-grid { \
|
||||
display: grid; \
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));\
|
||||
gap: 12px; \
|
||||
} \
|
||||
.homeproxy-connect-card { \
|
||||
display: grid; \
|
||||
grid-template-columns: 44px 1fr; \
|
||||
gap: 10px; \
|
||||
align-items: center; \
|
||||
min-height: 64px; \
|
||||
padding: 8px 10px; \
|
||||
border-radius: 6px; \
|
||||
background: #fff; \
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, .08);\
|
||||
} \
|
||||
.homeproxy-connect-icon { \
|
||||
display: flex; \
|
||||
align-items: center; \
|
||||
justify-content: center; \
|
||||
width: 38px; \
|
||||
height: 38px; \
|
||||
border-radius: 50%; \
|
||||
color: #fff; \
|
||||
font-weight: 700; \
|
||||
font-size: 15px; \
|
||||
} \
|
||||
.homeproxy-connect-info { \
|
||||
min-height: 42px; \
|
||||
padding: 7px 12px; \
|
||||
border-radius: 8px; \
|
||||
background: #e9edf2; \
|
||||
} \
|
||||
.homeproxy-connect-title { \
|
||||
color: #7d8aa0; \
|
||||
font-weight: 700; \
|
||||
font-size: 13px; \
|
||||
line-height: 1.2; \
|
||||
} \
|
||||
.homeproxy-connect-result { \
|
||||
margin-top: 2px; \
|
||||
color: #ff4d35; \
|
||||
font-weight: 700; \
|
||||
font-size: 13px; \
|
||||
cursor: pointer; \
|
||||
line-height: 1.2; \
|
||||
}';
|
||||
|
||||
const hp_dir = '/var/run/homeproxy';
|
||||
@@ -93,13 +44,13 @@ function getConnStat(o, site) {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
return L.resolveDefault(callConnStat(site), {}).then((ret) => {
|
||||
let ele = o.default.firstElementChild.nextElementSibling;
|
||||
let ele = o.default.firstElementChild.nextElementSibling;
|
||||
if (ret.result) {
|
||||
ele.style.setProperty('color', 'green');
|
||||
ele.innerHTML = _('passed');
|
||||
ele.innerHTML = _('passed');
|
||||
} else {
|
||||
ele.style.setProperty('color', 'red');
|
||||
ele.innerHTML = _('failed');
|
||||
ele.innerHTML = _('failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
@@ -109,53 +60,6 @@ function getConnStat(o, site) {
|
||||
]);
|
||||
}
|
||||
|
||||
function renderConnCards() {
|
||||
const callConnStat = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
method: 'connection_check',
|
||||
params: ['site'],
|
||||
expect: { '': {} }
|
||||
});
|
||||
|
||||
const items = [
|
||||
{ site: 'baidu', title: _('Baidu Connection'), icon: 'B', color: '#ff7a00' },
|
||||
{ site: 'google', title: _('Google Connection'), icon: 'G', color: '#4285f4' },
|
||||
{ site: 'github', title: _('GitHub Connection'), icon: 'GH', color: '#7b3bb3' },
|
||||
{ site: 'youtube', title: _('YouTube Connection'), icon: 'YT', color: '#ff0033' }
|
||||
];
|
||||
|
||||
return E('div', { 'class': 'homeproxy-connect-grid' }, items.map((item) => {
|
||||
let result = E('div', {
|
||||
'class': 'homeproxy-connect-result',
|
||||
'click': ui.createHandlerFn(this, () => {
|
||||
result.textContent = _('Checking...');
|
||||
result.style.setProperty('color', '#f0ad4e');
|
||||
|
||||
return L.resolveDefault(callConnStat(item.site), {}).then((ret) => {
|
||||
if (ret.result && ret.latency != null) {
|
||||
result.style.setProperty('color', 'green');
|
||||
result.textContent = _('%s ms').format(ret.latency);
|
||||
} else {
|
||||
result.style.setProperty('color', '#ff4d35');
|
||||
result.textContent = _('failed');
|
||||
}
|
||||
});
|
||||
})
|
||||
}, [ _('Click to check') ]);
|
||||
|
||||
return E('div', { 'class': 'homeproxy-connect-card' }, [
|
||||
E('div', {
|
||||
'class': 'homeproxy-connect-icon',
|
||||
'style': 'background:%s'.format(item.color)
|
||||
}, [ item.icon ]),
|
||||
E('div', { 'class': 'homeproxy-connect-info' }, [
|
||||
E('div', { 'class': 'homeproxy-connect-title' }, [ item.title ]),
|
||||
result
|
||||
])
|
||||
]);
|
||||
}));
|
||||
}
|
||||
|
||||
function getResVersion(o, type) {
|
||||
const callResVersion = rpc.declare({
|
||||
object: 'luci.homeproxy',
|
||||
@@ -323,16 +227,16 @@ return view.extend({
|
||||
render() {
|
||||
let m, s, o;
|
||||
|
||||
hp.installCloseButtonText();
|
||||
|
||||
m = new form.Map('homeproxy');
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Connection check'));
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.DummyValue, '_connection_check');
|
||||
o.rawhtml = true;
|
||||
o.render = renderConnCards;
|
||||
o = s.option(form.DummyValue, '_check_baidu', _('BaiDu'));
|
||||
o.cfgvalue = L.bind(getConnStat, this, o, 'baidu');
|
||||
|
||||
o = s.option(form.DummyValue, '_check_google', _('Google'));
|
||||
o.cfgvalue = L.bind(getConnStat, this, o, 'google');
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'homeproxy', _('Resources management'));
|
||||
s.anonymous = true;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+1698
-1479
File diff suppressed because it is too large.
Load diff
@@ -8,7 +8,6 @@ config homeproxy 'infra'
|
||||
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'
|
||||
@@ -25,68 +24,61 @@ config homeproxy 'migration'
|
||||
config homeproxy 'config'
|
||||
option main_node 'nil'
|
||||
option main_udp_node 'same'
|
||||
option dns_server 'https://dns.quad9.net/dns-query'
|
||||
option china_dns_server 'https://dns.alidns.com/dns-query'
|
||||
option dns_server '8.8.8.8'
|
||||
option china_dns_server '223.5.5.5'
|
||||
option routing_mode 'bypass_mainland_china'
|
||||
option routing_port 'common'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '1'
|
||||
option github_token ''
|
||||
option log_level 'warn'
|
||||
option cn_ip_fallback '0'
|
||||
option tun_dns_mode ''
|
||||
option udp_mapping ''
|
||||
option udp_filtering ''
|
||||
option udp_nat_max ''
|
||||
|
||||
config homeproxy 'control'
|
||||
option lan_proxy_mode 'disabled'
|
||||
list wan_proxy_ipv4_ips '91.105.192.0/23'
|
||||
list wan_proxy_ipv4_ips '91.108.4.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.8.0/21'
|
||||
list wan_proxy_ipv4_ips '91.108.16.0/21'
|
||||
list wan_proxy_ipv4_ips '91.108.8.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.16.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.12.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.20.0/22'
|
||||
list wan_proxy_ipv4_ips '91.108.56.0/22'
|
||||
list wan_proxy_ipv4_ips '95.161.64.0/20'
|
||||
list wan_proxy_ipv4_ips '149.154.160.0/20'
|
||||
list wan_proxy_ipv4_ips '185.76.151.0/24'
|
||||
list wan_proxy_ipv4_ips '203.208.50.66/32'
|
||||
list wan_proxy_ipv6_ips '2001:67c:4e8::/48'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23c::/47'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23c::/48'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23d::/48'
|
||||
list wan_proxy_ipv6_ips '2001:b28:f23f::/48'
|
||||
list wan_proxy_ipv6_ips '2a0a:f280::/32'
|
||||
|
||||
config homeproxy 'clash_api'
|
||||
option external_ui '/etc/homeproxy/run/ui'
|
||||
option external_ui_download_url 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip'
|
||||
option external_ui_download_detour 'direct-out'
|
||||
option external_controller '0.0.0.0:9095'
|
||||
option secret 'homeproxy'
|
||||
option default_mode 'rule'
|
||||
|
||||
config homeproxy 'ntp'
|
||||
option enabled '1'
|
||||
option server 'ntp.aliyun.com'
|
||||
option server_port '123'
|
||||
option interval '30m'
|
||||
|
||||
config homeproxy 'cache'
|
||||
option enabled '1'
|
||||
option path '/etc/homeproxy/cache.db'
|
||||
option store_rdrc '1'
|
||||
|
||||
config homeproxy 'routing'
|
||||
option sniff_override '1'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
option find_neighbor '0'
|
||||
|
||||
config homeproxy 'dns'
|
||||
option dns_strategy 'prefer_ipv4'
|
||||
option default_server 'local-dns'
|
||||
option default_strategy 'prefer_ipv4'
|
||||
option default_server 'default-dns'
|
||||
option disable_cache '0'
|
||||
option disable_cache_expire '0'
|
||||
option optimistic_cache '0'
|
||||
option optimistic_timeout ''
|
||||
option dns_timeout ''
|
||||
option cache_file_store_dns '0'
|
||||
|
||||
config homeproxy 'subscription'
|
||||
option auto_update '0'
|
||||
option allow_insecure '0'
|
||||
option latency_test_mode 'icmp'
|
||||
option packet_encoding 'xudp'
|
||||
option update_via_proxy '0'
|
||||
option auto_update_time '0 0 * * *'
|
||||
option filter_nodes 'disabled'
|
||||
option filter_nodes 'blacklist'
|
||||
list filter_keywords '重置|到期|过期|剩余|套餐'
|
||||
list filter_keywords 'Expiration|Remaining'
|
||||
|
||||
config homeproxy 'server'
|
||||
option enabled '0'
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
202609130005
|
||||
20260704060757
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
202609130005
|
||||
20260704060757
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1 +1 @@
|
||||
202609130005
|
||||
202607052256
|
||||
@@ -4,8 +4,6 @@
|
||||
1000giri.net
|
||||
10beasts.net
|
||||
10musume.com
|
||||
123av.com
|
||||
123av.me
|
||||
12bet.com
|
||||
12vpn.com
|
||||
12vpn.net
|
||||
@@ -15,7 +13,6 @@
|
||||
173ng.com
|
||||
18board.com
|
||||
18comic.org
|
||||
18comic.vip
|
||||
18mh.net
|
||||
1984bbs.com
|
||||
1991way.com
|
||||
@@ -130,7 +127,6 @@ agoogleaday.com
|
||||
agro.hk
|
||||
ai.dev
|
||||
ai.studio
|
||||
aihub.top
|
||||
aiosearch.com
|
||||
aiph.net
|
||||
airconsole.com
|
||||
@@ -215,15 +211,18 @@ aoxvpn.com
|
||||
apartmentratings.com
|
||||
apartments.com
|
||||
apat1989.org
|
||||
api-gl.lbkrs.com
|
||||
api-secure.recaptcha.net
|
||||
api-verify.recaptcha.net
|
||||
api.ai
|
||||
api.futunn.com
|
||||
api.lbkrs.com
|
||||
api.moomoobull.com
|
||||
api.palworldgame.com
|
||||
api.pureapk.com
|
||||
api.recaptcha.net
|
||||
api.shuaiapi.com
|
||||
api.steampowered.com
|
||||
api5.futunn.com
|
||||
apiary.io
|
||||
apigee.com
|
||||
apk.support
|
||||
@@ -295,6 +294,7 @@ atlanta168.com
|
||||
atnext.com
|
||||
auctions.yahoo.co.jp
|
||||
audacy.com
|
||||
auntology.fandom.com
|
||||
autodraw.com
|
||||
av.movie
|
||||
av01.tv
|
||||
@@ -342,7 +342,6 @@ baramangaonline.com
|
||||
barnabu.co.uk
|
||||
bartender.dowjones.com
|
||||
barton.de
|
||||
basemaps.arcgis.com
|
||||
bastillepost.com
|
||||
bayvoice.net
|
||||
bb-chat.tv
|
||||
@@ -561,7 +560,6 @@ break.com
|
||||
breakgfw.com
|
||||
breakingtweets.com
|
||||
breakwall.net
|
||||
briarproject.org
|
||||
brill.com
|
||||
brizzly.com
|
||||
broadpressinc.com
|
||||
@@ -667,6 +665,7 @@ cdjp.org
|
||||
cdn-images.mailchimp.com
|
||||
cdn-telegram.org
|
||||
cdn.arstechnica.net
|
||||
cdn.futustatic.com
|
||||
cdn.jwplayer.com
|
||||
cdn.printfriendly.com
|
||||
cdn.seatguru.com
|
||||
@@ -687,7 +686,6 @@ centauro.com.br
|
||||
centerforhumanreprod.com
|
||||
centralnation.com
|
||||
centurys.net
|
||||
cerebras.ai
|
||||
certificate-transparency.org
|
||||
certificate.revocationcheck.com
|
||||
cfr.org
|
||||
@@ -703,7 +701,6 @@ character.ai
|
||||
chat.com
|
||||
chat.lmsys.org
|
||||
chatgpt.com
|
||||
chatgpt.site
|
||||
chatpdf.com
|
||||
chaturbate.com
|
||||
checkgfw.com
|
||||
@@ -769,12 +766,10 @@ chrome.com
|
||||
chromecast.com
|
||||
chromeexperiments.com
|
||||
chromestatus.com
|
||||
cht.com.tw
|
||||
ci-en.jp
|
||||
cia.gov
|
||||
cici.com
|
||||
ciciai.com
|
||||
circle19.org
|
||||
cirosantilli.com
|
||||
citizencn.com
|
||||
citizenlab.ca
|
||||
@@ -787,7 +782,6 @@ civilmedia.tw
|
||||
civitai.com
|
||||
cixiaoya.club
|
||||
ck101.com
|
||||
clashyun.com
|
||||
classicalguitarblog.net
|
||||
claude.ai
|
||||
claude.com
|
||||
@@ -816,6 +810,7 @@ cn.fmnnow.com
|
||||
cn.sandscotaicentral.com
|
||||
cn.shafaqna.com
|
||||
cn.theaustralian.com.au
|
||||
cn.uptodown.com
|
||||
cna.com.tw
|
||||
cnbbnews.wordpress.com
|
||||
cnbeta.com.tw
|
||||
@@ -896,7 +891,6 @@ css.pixnet.in
|
||||
csuchen.de
|
||||
csw.org.uk
|
||||
ct.org.tw
|
||||
ctee.com.tw
|
||||
ctinets.com
|
||||
ctinews.com
|
||||
ctinsider.com
|
||||
@@ -997,7 +991,6 @@ disneyplus.com
|
||||
disp.cc
|
||||
disqus.com
|
||||
dit-inc.us
|
||||
divdyn.com
|
||||
diyin.org
|
||||
dizhuzhishang.com
|
||||
dl-laby.jp
|
||||
@@ -1044,6 +1037,7 @@ dougscripts.com
|
||||
doujincafe.com
|
||||
dowjones.io
|
||||
download.dappcdn.com
|
||||
download.futunn.com
|
||||
dpool.top
|
||||
dpp.org.tw
|
||||
dpr.info
|
||||
@@ -1146,7 +1140,6 @@ eromanga-kingdom.com
|
||||
eromangadouzin.com
|
||||
eromon.net
|
||||
esg.t91y.com
|
||||
esim.redteago.com
|
||||
eslite.com
|
||||
esurance.com
|
||||
etherdelta.com
|
||||
@@ -1169,7 +1162,6 @@ exmormon.org
|
||||
expatshield.com
|
||||
expecthim.com
|
||||
expekt.com
|
||||
experience.arcgis.com
|
||||
exploader.net
|
||||
expressnews.com
|
||||
expressvpn.com
|
||||
@@ -1209,7 +1201,6 @@ falundafaindia.org
|
||||
falunhr.org
|
||||
famunion.com
|
||||
fanbox.cc
|
||||
fandom.com
|
||||
fangeqiang.com
|
||||
fanglizhi.info
|
||||
fangong.org
|
||||
@@ -1448,7 +1439,6 @@ geti2p.net
|
||||
getlantern.org
|
||||
getmalus.com
|
||||
getmdl.io
|
||||
getmonero.org
|
||||
getoutline.org
|
||||
getsession.org
|
||||
getsync.com
|
||||
@@ -1838,7 +1828,6 @@ help.byspotify.com
|
||||
helpeachpeople.com
|
||||
helpster.de
|
||||
hembed.com
|
||||
hentai.tv
|
||||
heqinglian.net
|
||||
heritage.org
|
||||
herokuapp.com
|
||||
@@ -1864,7 +1853,6 @@ himalayan-foundation.org
|
||||
himemix.com
|
||||
hindustantimes.com
|
||||
hinet.net
|
||||
historylines.net
|
||||
hitbtc.com
|
||||
hitomi.la
|
||||
hive.blog
|
||||
@@ -1881,7 +1869,6 @@ hkcnews.com
|
||||
hkcoc.com
|
||||
hkcoc.weather.com.hk
|
||||
hkdc.us
|
||||
hkej.com
|
||||
hket.com
|
||||
hkfaa.com
|
||||
hkgalden.com
|
||||
@@ -1905,6 +1892,7 @@ holyspiritspeaks.org
|
||||
home.saxo
|
||||
homedepot.com
|
||||
homeservershow.com
|
||||
hongkong.fandom.com
|
||||
hongkongfp.com
|
||||
hongzhi.li
|
||||
honven.xyz
|
||||
@@ -2183,7 +2171,6 @@ kanshifang.com
|
||||
kantie.org
|
||||
kaotic.com
|
||||
karayou.com
|
||||
kards.com
|
||||
kathmandupost.com
|
||||
kawaiikawaii.jp
|
||||
kawase.com
|
||||
@@ -2208,7 +2195,6 @@ kinmen.org.tw
|
||||
kinmen.travel
|
||||
kinokuniya.com
|
||||
kiwi.kz
|
||||
kiwix.org
|
||||
kk-whys.co.jp
|
||||
kkbox.com
|
||||
kknews.cc
|
||||
@@ -2269,7 +2255,6 @@ lenwhite.com
|
||||
leonardo.ai
|
||||
lesoir.be
|
||||
letscorp.net
|
||||
level-plus.net
|
||||
lexica.art
|
||||
lhakar.org
|
||||
li.taipei
|
||||
@@ -2304,7 +2289,6 @@ linkedin.com
|
||||
linktr.ee
|
||||
linux.do
|
||||
linux.org.hk
|
||||
linux.sb
|
||||
liquiditytp.com
|
||||
liquidvpn.com
|
||||
listennotes.com
|
||||
@@ -2317,7 +2301,6 @@ liveleak.com
|
||||
livemint.com
|
||||
livestream.com
|
||||
livevideo.com
|
||||
livingatlas.arcgis.com
|
||||
livingstream.com
|
||||
lmarena.ai
|
||||
localbitcoins.com
|
||||
@@ -2359,6 +2342,7 @@ lvv2.com
|
||||
lyfhk.net
|
||||
lzjscript.com
|
||||
lzmtnews.org
|
||||
m.hkgalden.com
|
||||
m.me
|
||||
m.moegirl.org
|
||||
macgamestore.com
|
||||
@@ -2404,7 +2388,6 @@ mattwilcox.net
|
||||
maven.neoforged.net
|
||||
mcadforums.com
|
||||
mcaf.ee
|
||||
mceindex.com
|
||||
mcusercontent.com
|
||||
md-t.org
|
||||
me.me
|
||||
@@ -2535,6 +2518,7 @@ mosavi.io
|
||||
motherless.com
|
||||
movements.org
|
||||
moviefap.com
|
||||
mozilla.ai
|
||||
mpettis.com
|
||||
mpfinance.com
|
||||
mpinews.com
|
||||
@@ -2649,7 +2633,6 @@ nga.mil
|
||||
nhentai.net
|
||||
nic.cz.cc
|
||||
nic.gov
|
||||
nicoseiga.jp
|
||||
nicovideo.jp
|
||||
nightswatch.top
|
||||
nikke-en.com
|
||||
@@ -2690,7 +2673,6 @@ ntd.tv
|
||||
ntdtv.com
|
||||
ntdtv.com.tw
|
||||
ntdtv.cz
|
||||
nthlink.com
|
||||
nuexpo.com
|
||||
nurgo-software.com
|
||||
nusatrip.com
|
||||
@@ -2735,7 +2717,6 @@ oklink.com
|
||||
okpool.me
|
||||
okx.com
|
||||
old.honeynet.org
|
||||
oldmapsonline.org
|
||||
olehdtv.com
|
||||
olelive.com
|
||||
olevod.com
|
||||
@@ -2767,8 +2748,12 @@ ooni.org
|
||||
open.firstory.me
|
||||
openai.com
|
||||
openapi-quote.longbridge.cn
|
||||
openapi-quote.longbridge.com
|
||||
openapi-trade.longbridge.cn
|
||||
openapi-trade.longbridge.com
|
||||
openapi.futunn.com
|
||||
openapi.longbridge.cn
|
||||
openapi.longbridge.com
|
||||
openart.ai
|
||||
opencritic.com
|
||||
opendemocracy.net
|
||||
@@ -3002,6 +2987,7 @@ puffinbrowser.com
|
||||
pullfolio.co
|
||||
pure18.com
|
||||
pureconcepts.net
|
||||
puredns.org
|
||||
pureinsight.org
|
||||
purepdf.com
|
||||
purevpn.com
|
||||
@@ -3024,8 +3010,9 @@ qmp4.com
|
||||
qoos.com
|
||||
qq.co.za
|
||||
qstatus.com
|
||||
qtcard.futunn.com
|
||||
qtcardfthk.futufin.com
|
||||
qtrac.eu
|
||||
quakemachinex.com
|
||||
questvisual.com
|
||||
quitccp.org
|
||||
quiz.directory
|
||||
@@ -3045,6 +3032,7 @@ radio.co
|
||||
radio.garden
|
||||
radioaustralia.net.au
|
||||
radiohilight.net
|
||||
radiojar.com
|
||||
radioline.co
|
||||
radiovaticana.org
|
||||
radiovncr.com
|
||||
@@ -3116,7 +3104,6 @@ rlwlw.com
|
||||
rmbl.ws
|
||||
roblox.com
|
||||
robustnessiskey.com
|
||||
roc-taiwan.org
|
||||
rocket-inc.net
|
||||
rocket.chat
|
||||
rocksdb.org
|
||||
@@ -3126,7 +3113,6 @@ rolia.net
|
||||
rolsociety.org
|
||||
ronjoneswriter.com
|
||||
rou.video
|
||||
roushuwo.com
|
||||
rsdlmonitor.com
|
||||
rsf-chinese.org
|
||||
rsf.org
|
||||
@@ -3152,7 +3138,6 @@ rxhj.net
|
||||
s-cute.com
|
||||
s.yimg.com
|
||||
s3-ap-northeast-1.amazonaws.com
|
||||
s3.dualstack.us-west-2.amazonaws.com
|
||||
sacks.com
|
||||
sacom.hk
|
||||
sadistic-v.com
|
||||
@@ -3160,6 +3145,7 @@ sadpanda.us
|
||||
safechat.com
|
||||
safeguarddefenders.com
|
||||
safervpn.com
|
||||
sagernet.org
|
||||
saintyculture.com
|
||||
sakuralive.com
|
||||
salvation.org.hk
|
||||
@@ -3172,7 +3158,6 @@ savetibet.de
|
||||
savetibet.org
|
||||
savetibetstore.org
|
||||
saveuighur.org
|
||||
sb.sb
|
||||
sbme.me
|
||||
sbti.unun.dev
|
||||
schema.org
|
||||
@@ -3429,7 +3414,7 @@ supchina.com
|
||||
superpages.com
|
||||
supervpn.net
|
||||
superzooi.com
|
||||
supjav.com
|
||||
support.futunn.com
|
||||
supremecourt.gov
|
||||
suprememastertv.com
|
||||
surfeasy.com
|
||||
@@ -3450,7 +3435,6 @@ synapse.org
|
||||
syncback.com
|
||||
synergyse.com
|
||||
syosetu.com
|
||||
sysub.com
|
||||
t-g.com
|
||||
t.co
|
||||
t.me
|
||||
@@ -3459,12 +3443,10 @@ taa-usa.org
|
||||
taaze.tw
|
||||
taedp.org.tw
|
||||
tagwalk.com
|
||||
taiav.com
|
||||
tails.net
|
||||
taipeisociety.org
|
||||
taipeitimes.com
|
||||
taisounds.com
|
||||
taiwanembassy.org
|
||||
taiwanhot.net
|
||||
taiwanjustice.net
|
||||
taiwanncf.org.tw
|
||||
@@ -3666,7 +3648,6 @@ to-porno.com
|
||||
togetter.com
|
||||
token.im
|
||||
tokenlon.im
|
||||
tokenplus.app
|
||||
tokyo-porn-tube.com
|
||||
tokyocn.com
|
||||
tomp3.cc
|
||||
@@ -3693,6 +3674,8 @@ totalvpn.com
|
||||
tou.tv
|
||||
tpi.org.tw
|
||||
tracfone.com
|
||||
trade.futunn.com
|
||||
trade.longportapp.com
|
||||
tradeup.com
|
||||
tradingview.com
|
||||
translate.goog
|
||||
@@ -3706,7 +3689,6 @@ trtworld.com
|
||||
truebuddha-md.org
|
||||
trustwallet.com
|
||||
truthsocial.com
|
||||
tryai.dev
|
||||
tryheart.jp
|
||||
tsdr.uspto.gov
|
||||
tt1069.com
|
||||
@@ -3888,7 +3870,6 @@ uyghurstudies.org
|
||||
uyghurtribunal.com
|
||||
uygur.fc2web.com
|
||||
uymaarip.com
|
||||
v.recipes
|
||||
v2.help
|
||||
v2.hysteria.network
|
||||
v2ex.com
|
||||
@@ -4099,7 +4080,6 @@ wiktionary.org
|
||||
willw.net
|
||||
wilsoncenter.org
|
||||
windscribe.com
|
||||
windy.com
|
||||
wingamestore.com
|
||||
wingy.site
|
||||
wionews.com
|
||||
@@ -4142,11 +4122,9 @@ woyaolian.org
|
||||
wp.com
|
||||
wplace.live
|
||||
wpoforum.com
|
||||
write.as
|
||||
writesonic.com
|
||||
wsj.com
|
||||
wsj.net
|
||||
wtfpass.com
|
||||
wufafangwen.com
|
||||
wufi.org.tw
|
||||
wujieliulan.com
|
||||
@@ -4157,6 +4135,7 @@ www.ajsands.com
|
||||
www.antd.org
|
||||
www.aolnews.com
|
||||
www.bing.com
|
||||
www.clashverge.dev
|
||||
www.cmoinc.org
|
||||
www.dmm.com
|
||||
www.eastturkistan.net
|
||||
@@ -4192,7 +4171,6 @@ x.co
|
||||
x.com
|
||||
x.company
|
||||
x3guide.com
|
||||
x86.fr
|
||||
xbabe.com
|
||||
xbookcn.com
|
||||
xbtce.com
|
||||
@@ -4335,7 +4313,6 @@ zdnet.com.tw
|
||||
zello.com
|
||||
zenmate.com
|
||||
zenmate.com.ru
|
||||
zenmux.ai
|
||||
zerohedge.com
|
||||
zeronet.io
|
||||
zh-hans.cfsh99.com
|
||||
|
||||
@@ -1 +1 @@
|
||||
202609130005
|
||||
202607052256
|
||||
@@ -14,6 +14,6 @@ while true; do
|
||||
sleep 180
|
||||
for i in "$main_log_file" "$singc_log_file" "$sings_log_file"; do
|
||||
[ -s "$i" ] || continue
|
||||
[ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i"
|
||||
[ "$(( $(wc -c < "$i") / 1024 >= log_max_size))" -eq "0" ] || : > "$i"
|
||||
done
|
||||
done
|
||||
@@ -1,9 +1,20 @@
|
||||
#!/usr/bin/utpl -S
|
||||
|
||||
{%-
|
||||
/*
|
||||
* Declares every homeproxy_* fw4 chain/set. The inventory of those objects is
|
||||
* tracked in scripts/fw4_names.sh, which init.d/homeproxy uses to clean them up
|
||||
* on stop; add/remove entries there together with any change below.
|
||||
* tests/ucode/test_fw4_names.sh compares both lists, and
|
||||
* tests/ucode/test_firewall_template.sh renders this template and checks the
|
||||
* objects actually survive into the generated nftables script.
|
||||
*
|
||||
* Keep this comment inside the ucode block: text placed before `{%-` has its
|
||||
* trailing newline trimmed, which would glue it onto the first nft statement.
|
||||
*/
|
||||
import { readfile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty } from '/etc/homeproxy/scripts/homeproxy.uc';
|
||||
import { isEmpty, isValidCIDR } from '/etc/homeproxy/scripts/homeproxy.uc';
|
||||
|
||||
const fw4 = require('fw4');
|
||||
|
||||
@@ -25,7 +36,24 @@ function resolve_ipv6(str) {
|
||||
if (ipv6.bits > -1)
|
||||
return `${ipv6.addr}/${ipv6.bits}`;
|
||||
else
|
||||
return `& ${ipv6.mask} == ${ipv6.addr}`;
|
||||
return ipv6.addr;
|
||||
}
|
||||
|
||||
/* Normalize a list of IPv6 entries into an nftables anonymous set
|
||||
(or null when empty), so IPv6 ACLs match in a single set lookup
|
||||
instead of one rule per address. */
|
||||
function ipv6_to_nftarr(list) {
|
||||
if (isEmpty(list))
|
||||
return null;
|
||||
|
||||
let out = [];
|
||||
for (let ip in list) {
|
||||
const r = resolve_ipv6(ip);
|
||||
if (!isEmpty(r))
|
||||
push(out, r);
|
||||
}
|
||||
|
||||
return isEmpty(out) ? null : `{ ${join(', ', out)} }`;
|
||||
}
|
||||
|
||||
function resolve_mark(str) {
|
||||
@@ -182,7 +210,9 @@ set homeproxy_mainland_addr_v4 {
|
||||
auto-merge
|
||||
elements = {
|
||||
{% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)): %}
|
||||
{% if (isValidCIDR(cnip4, 4)): %}
|
||||
{{ cnip4 }},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
@@ -193,7 +223,9 @@ set homeproxy_mainland_addr_v6 {
|
||||
auto-merge
|
||||
elements = {
|
||||
{% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)): %}
|
||||
{% if (isValidCIDR(cnip6, 6)): %}
|
||||
{{ cnip6 }},
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
@@ -286,9 +318,9 @@ chain homeproxy_redirect_lanac {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_proxy_ipv6_ips) }} counter goto homeproxy_redirect
|
||||
{% endif /* 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 */ %}
|
||||
@@ -296,9 +328,9 @@ chain homeproxy_redirect_lanac {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_direct_ipv6_ips) }} counter return
|
||||
{% endif /* 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 */ %}
|
||||
@@ -326,9 +358,9 @@ chain homeproxy_redirect {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_global_proxy_ipv6_ips) }} counter goto homeproxy_redirect_proxy_port
|
||||
{% endif /* 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 */ %}
|
||||
@@ -359,9 +391,9 @@ chain homeproxy_redirect {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_gaming_mode_ipv6_ips) }} counter goto homeproxy_redirect_proxy
|
||||
{% endif /* 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 */ %}
|
||||
@@ -409,9 +441,9 @@ chain homeproxy_mangle_lanac {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_proxy_ipv6_ips) }} counter goto homeproxy_mangle_prerouting
|
||||
{% endif /* 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 */ %}
|
||||
@@ -419,9 +451,9 @@ chain homeproxy_mangle_lanac {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_direct_ipv6_ips) }} counter return
|
||||
{% endif /* 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 */ %}
|
||||
@@ -447,9 +479,9 @@ chain homeproxy_mangle_prerouting {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_global_proxy_ipv6_ips) }} counter goto homeproxy_mangle_tproxy_port
|
||||
{% endif /* 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 */ %}
|
||||
@@ -465,14 +497,14 @@ chain homeproxy_mangle_prerouting {
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
|
||||
{% endif /* routing_mode */ %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
@@ -484,9 +516,9 @@ chain homeproxy_mangle_prerouting {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_gaming_mode_ipv6_ips) }} counter goto homeproxy_mangle_tproxy
|
||||
{% endif /* 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 */ %}
|
||||
@@ -555,9 +587,9 @@ chain homeproxy_mangle_lanac {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_proxy_ipv6_ips) }} counter goto homeproxy_mangle_tun
|
||||
{% endif /* 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 */ %}
|
||||
@@ -565,9 +597,9 @@ chain homeproxy_mangle_lanac {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_direct_ipv6_ips) }} counter return
|
||||
{% endif /* 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 */ %}
|
||||
@@ -606,9 +638,9 @@ chain homeproxy_mangle_tun {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_global_proxy_ipv6_ips) }} counter goto homeproxy_mangle_tun_mark
|
||||
{% endif /* 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 */ %}
|
||||
@@ -626,14 +658,14 @@ chain homeproxy_mangle_tun {
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
|
||||
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
||||
ip daddr @homeproxy_mainland_addr_v4 counter return
|
||||
{% if (ipv6_support === '1'): %}
|
||||
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
||||
{% endif /* ipv6_support */ %}
|
||||
{% if (routing_mode !== 'custom'): %}
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Fuck you QUIC"
|
||||
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (not proxyable)"
|
||||
{% endif /* routing_mode */ %}
|
||||
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
||||
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
||||
@@ -645,9 +677,9 @@ chain homeproxy_mangle_tun {
|
||||
{% 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_ipv6_ips)): %}
|
||||
ip6 saddr {{ ipv6_to_nftarr(control_info.lan_gaming_mode_ipv6_ips) }} counter meta mark set {{ tun_mark }}
|
||||
{% endif /* 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 */ %}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import { writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty, RUN_DIR } from 'homeproxy';
|
||||
import { isEmpty, RUN_DIR, validation } from 'homeproxy';
|
||||
|
||||
const cfgname = 'homeproxy';
|
||||
const uci = cursor();
|
||||
@@ -39,7 +39,17 @@ if (server_enabled === '1') {
|
||||
if (s.enabled !== '1' || s.firewall !== '1')
|
||||
return;
|
||||
|
||||
if (!validation('port', s.port)) {
|
||||
print(`WARN: skipping server ${s['.name']}: invalid port "${s.port}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
let proto = s.network || '{ tcp, udp }';
|
||||
if (!(proto in ['tcp', 'udp', '{ tcp, udp }'])) {
|
||||
print(`WARN: skipping server ${s['.name']}: invalid network "${proto}".`);
|
||||
return;
|
||||
}
|
||||
|
||||
push(input, `meta l4proto ${proto} th dport ${s.port} counter accept comment "!${cfgname}: accept server ${s['.name']}"`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2025 ImmortalWrt.org
|
||||
#
|
||||
# Single source of truth for every fw4 chain and set owned by homeproxy.
|
||||
#
|
||||
# * root/etc/homeproxy/scripts/firewall_post.ut declares these objects and
|
||||
# decides which of them actually exist for the current proxy mode.
|
||||
# * root/etc/init.d/homeproxy flushes and deletes them on stop.
|
||||
#
|
||||
# When you add or remove a chain/set in firewall_post.ut, update this list in
|
||||
# the same commit -- tests/ucode/test_fw4_names.sh compares both sides and
|
||||
# fails when they drift.
|
||||
#
|
||||
# Each object is cleaned up independently: the cleanup loop deletes one at a
|
||||
# time so that a chain/set which does not exist in the current mode cannot
|
||||
# abort the whole batch. A single `nft -f` batch is atomic and would roll back
|
||||
# every preceding command in that case.
|
||||
|
||||
HP_FW4_CHAINS="
|
||||
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_lanac
|
||||
homeproxy_mangle_mark
|
||||
homeproxy_mangle_tun
|
||||
homeproxy_mangle_tun_mark
|
||||
"
|
||||
|
||||
HP_FW4_SETS="
|
||||
homeproxy_local_addr_v4
|
||||
homeproxy_local_addr_v6
|
||||
homeproxy_gfw_list_v4
|
||||
homeproxy_gfw_list_v6
|
||||
homeproxy_mainland_addr_v4
|
||||
homeproxy_mainland_addr_v6
|
||||
homeproxy_wan_proxy_addr_v4
|
||||
homeproxy_wan_proxy_addr_v6
|
||||
homeproxy_wan_direct_addr_v4
|
||||
homeproxy_wan_direct_addr_v6
|
||||
homeproxy_routing_port
|
||||
"
|
||||
File diff suppressed because it is too large.
Load diff
@@ -11,8 +11,8 @@ import { writefile } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
|
||||
import {
|
||||
isEmpty, strToBool, strToInt, strToTime,
|
||||
removeBlankAttrs, HP_DIR, RUN_DIR
|
||||
strToBool, strToInt, strToTime,
|
||||
removeBlankAttrs, buildTLSObject, buildTransportObject, HP_DIR, RUN_DIR
|
||||
} from 'homeproxy';
|
||||
|
||||
/* UCI config start */
|
||||
@@ -42,6 +42,26 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
if (cfg.enabled !== '1')
|
||||
return;
|
||||
|
||||
/* Snell (1.14): single-psk inbound; shape differs from the generic users[] block below */
|
||||
if (cfg.type === 'snell') {
|
||||
push(config.inbounds, {
|
||||
type: 'snell',
|
||||
tag: 'cfg-' + cfg['.name'] + '-in',
|
||||
|
||||
listen: cfg.address || '::',
|
||||
listen_port: strToInt(cfg.port),
|
||||
bind_interface: cfg.bind_interface,
|
||||
reuse_addr: strToBool(cfg.reuse_addr),
|
||||
tcp_fast_open: strToBool(cfg.tcp_fast_open),
|
||||
tcp_multi_path: strToBool(cfg.tcp_multi_path),
|
||||
version: strToInt(cfg.snell_version) || 5,
|
||||
psk: cfg.password,
|
||||
obfs_mode: cfg.snell_obfs_mode,
|
||||
mode: cfg.snell_mode
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
push(config.inbounds, {
|
||||
type: cfg.type,
|
||||
tag: 'cfg-' + cfg['.name'] + '-in',
|
||||
@@ -59,17 +79,15 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
/* AnyTLS */
|
||||
padding_scheme: cfg.anytls_padding_scheme,
|
||||
|
||||
/* Hysteria */
|
||||
/* Hysteria (2) */
|
||||
up_mbps: strToInt(cfg.hysteria_up_mbps),
|
||||
down_mbps: strToInt(cfg.hysteria_down_mbps),
|
||||
obfs: cfg.hysteria_obfs_type ? {
|
||||
type: cfg.hysteria_obfs_type,
|
||||
password: cfg.hysteria_obfs_password
|
||||
password: cfg.hysteria_obfs_password,
|
||||
min_packet_size: strToInt(cfg.hysteria_obfs_min_packet_size),
|
||||
max_packet_size: strToInt(cfg.hysteria_obfs_max_packet_size)
|
||||
} : cfg.hysteria_obfs_password,
|
||||
recv_window_conn: strToInt(cfg.hysteria_recv_window_conn),
|
||||
recv_window_client: strToInt(cfg.hysteria_recv_window_client),
|
||||
max_conn_client: strToInt(cfg.hysteria_max_conn_client),
|
||||
disable_mtu_discovery: strToBool(cfg.hysteria_disable_mtu_discovery),
|
||||
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
|
||||
masquerade: cfg.hysteria_masquerade,
|
||||
|
||||
@@ -113,73 +131,22 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
} : null
|
||||
} : null,
|
||||
|
||||
tls: (cfg.tls === '1') ? {
|
||||
enabled: true,
|
||||
server_name: cfg.tls_sni,
|
||||
alpn: cfg.tls_alpn,
|
||||
min_version: cfg.tls_min_version,
|
||||
max_version: cfg.tls_max_version,
|
||||
cipher_suites: cfg.tls_cipher_suites,
|
||||
certificate_path: cfg.tls_cert_path,
|
||||
key_path: cfg.tls_key_path,
|
||||
acme: (cfg.tls_acme === '1') ? {
|
||||
domain: cfg.tls_acme_domain,
|
||||
data_directory: HP_DIR + '/certs',
|
||||
default_server_name: cfg.tls_acme_dsn,
|
||||
email: cfg.tls_acme_email,
|
||||
provider: cfg.tls_acme_provider,
|
||||
disable_http_challenge: strToBool(cfg.tls_acme_dhc),
|
||||
disable_tls_alpn_challenge: (cfg.tls_acme_dtac),
|
||||
alternative_http_port: strToInt(cfg.tls_acme_ahp),
|
||||
alternative_tls_port: strToInt(cfg.tls_acme_atp),
|
||||
external_account: (cfg.tls_acme_external_account === '1') ? {
|
||||
key_id: cfg.tls_acme_ea_keyid,
|
||||
mac_key: cfg.tls_acme_ea_mackey
|
||||
} : null,
|
||||
dns01_challenge: (cfg.tls_dns01_challenge === '1') ? {
|
||||
provider: cfg.tls_dns01_provider,
|
||||
access_key_id: cfg.tls_dns01_ali_akid,
|
||||
access_key_secret: cfg.tls_dns01_ali_aksec,
|
||||
region_id: cfg.tls_dns01_ali_rid,
|
||||
api_token: cfg.tls_dns01_cf_api_token
|
||||
} : null
|
||||
} : null,
|
||||
ech: (cfg.tls_ech_key) ? {
|
||||
enabled: true,
|
||||
key: split(cfg.tls_ech_key, '\n'),
|
||||
// config: split(cfg.tls_ech_config, '\n')
|
||||
} : null,
|
||||
reality: (cfg.tls_reality === '1') ? {
|
||||
enabled: true,
|
||||
private_key: cfg.tls_reality_private_key,
|
||||
short_id: cfg.tls_reality_short_id,
|
||||
max_time_difference: strToTime(cfg.tls_reality_max_time_difference),
|
||||
handshake: {
|
||||
server: cfg.tls_reality_server_addr,
|
||||
server_port: strToInt(cfg.tls_reality_server_port)
|
||||
}
|
||||
} : null
|
||||
} : null,
|
||||
tls: buildTLSObject(cfg, true),
|
||||
|
||||
transport: !isEmpty(cfg.transport) ? {
|
||||
type: cfg.transport,
|
||||
host: cfg.http_host || cfg.httpupgrade_host,
|
||||
path: cfg.http_path || cfg.ws_path,
|
||||
headers: cfg.ws_host ? {
|
||||
Host: cfg.ws_host
|
||||
} : null,
|
||||
method: cfg.http_method,
|
||||
max_early_data: strToInt(cfg.websocket_early_data),
|
||||
early_data_header_name: cfg.websocket_early_data_header,
|
||||
service_name: cfg.grpc_servicename,
|
||||
idle_timeout: strToTime(cfg.http_idle_timeout),
|
||||
ping_timeout: strToTime(cfg.http_ping_timeout)
|
||||
} : null
|
||||
transport: buildTransportObject(cfg, true)
|
||||
});
|
||||
});
|
||||
|
||||
if (length(config.inbounds) === 0)
|
||||
exit(1);
|
||||
|
||||
config['$schema'] = 'https://sing-box.sagernet.org/schema.json';
|
||||
|
||||
system('mkdir -p ' + RUN_DIR);
|
||||
writefile(RUN_DIR + '/sing-box-s.json', sprintf('%.J\n', removeBlankAttrs(config)));
|
||||
const server_tmp = RUN_DIR + '/sing-box-s.json.tmp';
|
||||
writefile(server_tmp, sprintf('%.J\n', removeBlankAttrs(config)));
|
||||
if (system('/usr/bin/sing-box check --config ' + server_tmp) !== 0) {
|
||||
system('rm -f ' + server_tmp);
|
||||
exit(1);
|
||||
}
|
||||
system('mv -f ' + server_tmp + ' ' + RUN_DIR + '/sing-box-s.json');
|
||||
Executable → Regular
+278
-41
@@ -26,30 +26,55 @@ export function isBinary(str) {
|
||||
return false;
|
||||
};
|
||||
|
||||
/* Close a file descriptor, swallowing secondary errors (e.g. an already
|
||||
closed fd). ucode has no `finally` clause, so both the normal and the
|
||||
exceptional path of executeCommand() have to close explicitly. */
|
||||
function closeFD(fd) {
|
||||
if (fd) {
|
||||
try {
|
||||
fd.close();
|
||||
} catch (e) {
|
||||
/* already closed */
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function executeCommand(...args) {
|
||||
let outfd = mkstemp();
|
||||
let errfd = mkstemp();
|
||||
let outfd = null, errfd = null;
|
||||
|
||||
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
|
||||
try {
|
||||
outfd = mkstemp();
|
||||
errfd = mkstemp();
|
||||
|
||||
outfd.seek(0);
|
||||
errfd.seek(0);
|
||||
const exitcode = system(`${join(' ', args)} >&${outfd.fileno()} 2>&${errfd.fileno()}`);
|
||||
|
||||
const stdout = outfd.read(1024 * 512) ?? '';
|
||||
const stderr = errfd.read(1024 * 512) ?? '';
|
||||
outfd.seek(0);
|
||||
errfd.seek(0);
|
||||
|
||||
outfd.close();
|
||||
errfd.close();
|
||||
const stdout = outfd.read(1024 * 512) ?? '';
|
||||
const stderr = errfd.read(1024 * 512) ?? '';
|
||||
|
||||
const binary = isBinary(stdout);
|
||||
const binary = isBinary(stdout);
|
||||
|
||||
return {
|
||||
command: join(' ', args),
|
||||
stdout: binary ? null : stdout,
|
||||
stderr,
|
||||
exitcode,
|
||||
binary
|
||||
};
|
||||
/* mkstemp() returns delete-on-close files, so closing is enough */
|
||||
closeFD(outfd);
|
||||
closeFD(errfd);
|
||||
|
||||
return {
|
||||
command: join(' ', args),
|
||||
stdout: binary ? null : stdout,
|
||||
stderr,
|
||||
exitcode,
|
||||
binary
|
||||
};
|
||||
} catch (e) {
|
||||
/* Never leak the temporary descriptors on a failing run. ucode has
|
||||
no `finally` clause, so the exception is re-raised by hand. */
|
||||
closeFD(outfd);
|
||||
closeFD(errfd);
|
||||
|
||||
die(e);
|
||||
}
|
||||
};
|
||||
|
||||
export function getTime(epoch) {
|
||||
@@ -66,34 +91,35 @@ export function getTime(epoch) {
|
||||
|
||||
};
|
||||
|
||||
export function wGETResponse(url, ua) {
|
||||
/*
|
||||
* Fetch a URL and report both the body and, on failure, the reason. The
|
||||
* reason is wget's own stderr (whitespace collapsed, length-capped) so the
|
||||
* caller can tell a DNS failure from a timeout or a TLS handshake error.
|
||||
*/
|
||||
export function wGETVerbose(url, ua) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return null;
|
||||
return { content: null, error: 'invalid URL' };
|
||||
|
||||
if (!ua)
|
||||
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
|
||||
|
||||
const output = executeCommand(`/usr/bin/wget -O- -S --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
|
||||
return {
|
||||
body: trim(output.stdout),
|
||||
headers: output.stderr || '',
|
||||
exitcode: output.exitcode
|
||||
};
|
||||
/* -nv (not -q) so wget still reports *why* a fetch failed on stderr */
|
||||
const output = executeCommand(`/usr/bin/wget -nv -O- --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
|
||||
if (output.exitcode !== 0) {
|
||||
let reason = trim(output.stderr || '');
|
||||
reason = reason ? replace(reason, /\s+/g, ' ') : 'no error output';
|
||||
|
||||
if (length(reason) > 200)
|
||||
reason = substr(reason, 0, 200) + '...';
|
||||
|
||||
return { content: null, error: `wget exited with status ${output.exitcode}: ${reason}` };
|
||||
}
|
||||
|
||||
return { content: trim(output.stdout), error: null };
|
||||
};
|
||||
|
||||
export function wGET(url, ua) {
|
||||
return wGETResponse(url, ua)?.body;
|
||||
};
|
||||
|
||||
export function wGETHeaders(url, ua) {
|
||||
if (!url || type(url) !== 'string')
|
||||
return null;
|
||||
|
||||
if (!ua)
|
||||
ua = 'clash.meta';
|
||||
|
||||
const output = executeCommand(`/usr/bin/wget -O /dev/null -S --user-agent ${shellQuote(ua)} --timeout=10 ${shellQuote(url)}`) || {};
|
||||
return output.stderr || '';
|
||||
return wGETVerbose(url, ua).content;
|
||||
};
|
||||
/* Utilities end */
|
||||
|
||||
@@ -103,7 +129,7 @@ export function isEmpty(res) {
|
||||
};
|
||||
|
||||
export function strToBool(str) {
|
||||
return (str === '1') || null;
|
||||
return str === '1' ? true : (str === '0' ? false : null);
|
||||
};
|
||||
|
||||
export function strToInt(str) {
|
||||
@@ -111,7 +137,11 @@ export function strToInt(str) {
|
||||
};
|
||||
|
||||
export function strToTime(str) {
|
||||
return !isEmpty(str) ? (str + 's') : null;
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
/* Preserve values that already carry a time unit (e.g. "30s", "1m") */
|
||||
return match(str, /[a-zA-Z]$/) ? str : (str + 's');
|
||||
};
|
||||
|
||||
export function removeBlankAttrs(res) {
|
||||
@@ -152,6 +182,77 @@ export function validation(datatype, data) {
|
||||
const ret = system(`/sbin/validate_data ${shellQuote(datatype)} ${shellQuote(data)} 2>/dev/null`);
|
||||
return (ret === 0);
|
||||
};
|
||||
|
||||
/* Validate IP/CIDR format to prevent nftables template injection from resource files */
|
||||
export function isValidCIDR(addr, family) {
|
||||
if (isEmpty(addr))
|
||||
return false;
|
||||
|
||||
/* Strip leading/trailing whitespace */
|
||||
addr = trim(addr);
|
||||
if (!addr)
|
||||
return false;
|
||||
|
||||
/* Split address and optional prefix */
|
||||
const parts = split(addr, '/');
|
||||
const ip = parts[0];
|
||||
const prefix = parts[1];
|
||||
|
||||
/* Validate IP part */
|
||||
if (family === 4) {
|
||||
if (!match(ip, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
|
||||
return false;
|
||||
/* Validate octet ranges */
|
||||
const octets = split(ip, '.');
|
||||
for (let o in octets)
|
||||
if (int(o) > 255)
|
||||
return false;
|
||||
/* Validate prefix if present */
|
||||
if (prefix && (int(prefix) < 0 || int(prefix) > 32))
|
||||
return false;
|
||||
} else if (family === 6) {
|
||||
/* Only hex digits and colons */
|
||||
if (!match(ip, /^[0-9a-fA-F:]+$/))
|
||||
return false;
|
||||
|
||||
/* At most one "::" */
|
||||
if (match(ip, /::.*::/))
|
||||
return false;
|
||||
|
||||
/* Validate group structure */
|
||||
const dcolon = index(ip, '::');
|
||||
if (dcolon === -1) {
|
||||
/* Uncompressed form: exactly 8 groups of 1-4 hex digits */
|
||||
const groups = split(ip, ':');
|
||||
if (length(groups) !== 8)
|
||||
return false;
|
||||
for (let g in groups)
|
||||
if (!match(g, /^[0-9a-fA-F]{1,4}$/))
|
||||
return false;
|
||||
} else {
|
||||
/* Compressed form: "::" expands to at least one zero group */
|
||||
let count = 0;
|
||||
for (let part in [substr(ip, 0, dcolon), substr(ip, dcolon + 2)]) {
|
||||
if (part === '')
|
||||
continue;
|
||||
for (let g in split(part, ':')) {
|
||||
if (!match(g, /^[0-9a-fA-F]{1,4}$/))
|
||||
return false;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 7)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prefix && (int(prefix) < 0 || int(prefix) > 128))
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
/* String helper end */
|
||||
|
||||
/* String parser start */
|
||||
@@ -160,8 +261,8 @@ export function decodeBase64Str(str) {
|
||||
return null;
|
||||
|
||||
str = trim(str);
|
||||
str = replace(str, '_', '/');
|
||||
str = replace(str, '-', '+');
|
||||
str = replace(str, /_/g, '/');
|
||||
str = replace(str, /-/g, '+');
|
||||
|
||||
const padding = length(str) % 4;
|
||||
if (padding)
|
||||
@@ -247,3 +348,139 @@ export function parseURL(url) {
|
||||
return objurl;
|
||||
};
|
||||
/* String parser end */
|
||||
|
||||
/* Config generator helper start */
|
||||
/*
|
||||
* Shared sing-box TLS object builder for the client outbound and the server
|
||||
* inbound. The property order below is significant: the generated JSON keeps
|
||||
* the insertion order of this object after removeBlankAttrs() drops the blank
|
||||
* attributes, and both generators used to emit the fields in exactly this
|
||||
* order. Keep client-only fields (insecure/handshake_timeout/utls) and
|
||||
* server-only fields (key_path/certificate_provider) at their current
|
||||
* positions when editing.
|
||||
*/
|
||||
export function buildTLSObject(cfg, is_server) {
|
||||
if (cfg.tls !== '1')
|
||||
return null;
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
server_name: cfg.tls_sni,
|
||||
insecure: is_server ? null : strToBool(cfg.tls_insecure),
|
||||
alpn: cfg.tls_alpn,
|
||||
min_version: cfg.tls_min_version,
|
||||
max_version: cfg.tls_max_version,
|
||||
handshake_timeout: is_server ? null : strToTime(cfg.tls_handshake_timeout),
|
||||
cipher_suites: cfg.tls_cipher_suites,
|
||||
certificate_path: cfg.tls_cert_path,
|
||||
key_path: is_server ? cfg.tls_key_path : null,
|
||||
certificate_provider: (is_server && cfg.tls_acme === '1') ? {
|
||||
type: 'acme',
|
||||
domain: (type(cfg.tls_acme_domain) === 'array') ? cfg.tls_acme_domain
|
||||
: (isEmpty(cfg.tls_acme_domain) ? [] : [cfg.tls_acme_domain]),
|
||||
data_directory: HP_DIR + '/certs',
|
||||
default_server_name: cfg.tls_acme_dsn,
|
||||
email: cfg.tls_acme_email,
|
||||
provider: cfg.tls_acme_provider,
|
||||
account_key: cfg.tls_acme_account_key,
|
||||
key_type: cfg.tls_acme_key_type,
|
||||
profile: cfg.tls_acme_profile,
|
||||
disable_http_challenge: strToBool(cfg.tls_acme_dhc),
|
||||
disable_tls_alpn_challenge: strToBool(cfg.tls_acme_dtac),
|
||||
alternative_http_port: strToInt(cfg.tls_acme_ahp),
|
||||
alternative_tls_port: strToInt(cfg.tls_acme_atp),
|
||||
external_account: (cfg.tls_acme_external_account === '1') ? {
|
||||
key_id: cfg.tls_acme_ea_keyid,
|
||||
mac_key: cfg.tls_acme_ea_mackey
|
||||
} : null,
|
||||
dns01_challenge: (cfg.tls_dns01_challenge === '1') ? {
|
||||
provider: cfg.tls_dns01_provider,
|
||||
access_key_id: cfg.tls_dns01_ali_akid,
|
||||
access_key_secret: cfg.tls_dns01_ali_aksec,
|
||||
region_id: cfg.tls_dns01_ali_rid,
|
||||
api_token: cfg.tls_dns01_cf_api_token
|
||||
} : null
|
||||
} : null,
|
||||
ech: is_server ? (cfg.tls_ech_key ? {
|
||||
enabled: true,
|
||||
key: split(cfg.tls_ech_key, '\n')
|
||||
/* config: split(cfg.tls_ech_config, '\n') */
|
||||
} : null) : ((cfg.tls_ech === '1') ? {
|
||||
enabled: true,
|
||||
config: cfg.tls_ech_config,
|
||||
config_path: cfg.tls_ech_config_path
|
||||
} : null),
|
||||
utls: (is_server || isEmpty(cfg.tls_utls)) ? null : {
|
||||
enabled: true,
|
||||
fingerprint: cfg.tls_utls
|
||||
},
|
||||
reality: (cfg.tls_reality !== '1') ? null : (is_server ? {
|
||||
enabled: true,
|
||||
private_key: cfg.tls_reality_private_key,
|
||||
short_id: cfg.tls_reality_short_id,
|
||||
max_time_difference: strToTime(cfg.tls_reality_max_time_difference),
|
||||
handshake: {
|
||||
server: cfg.tls_reality_server_addr,
|
||||
server_port: strToInt(cfg.tls_reality_server_port)
|
||||
}
|
||||
} : {
|
||||
enabled: true,
|
||||
public_key: cfg.tls_reality_public_key,
|
||||
short_id: cfg.tls_reality_short_id
|
||||
})
|
||||
};
|
||||
};
|
||||
|
||||
/* Shared sing-box transport object builder; the client transport additionally
|
||||
supports the gRPC keepalive hint, the server one does not. */
|
||||
export function buildTransportObject(cfg, is_server) {
|
||||
if (isEmpty(cfg.transport))
|
||||
return null;
|
||||
|
||||
return {
|
||||
type: cfg.transport,
|
||||
host: cfg.http_host || cfg.httpupgrade_host,
|
||||
path: cfg.http_path || cfg.ws_path,
|
||||
headers: cfg.ws_host ? {
|
||||
Host: cfg.ws_host
|
||||
} : null,
|
||||
method: cfg.http_method,
|
||||
max_early_data: strToInt(cfg.websocket_early_data),
|
||||
early_data_header_name: cfg.websocket_early_data_header,
|
||||
service_name: cfg.grpc_servicename,
|
||||
idle_timeout: strToTime(cfg.http_idle_timeout),
|
||||
ping_timeout: strToTime(cfg.http_ping_timeout),
|
||||
permit_without_stream: is_server ? null : strToBool(cfg.grpc_permit_without_stream)
|
||||
};
|
||||
};
|
||||
/* Config generator helper end */
|
||||
|
||||
/* PEM validation start */
|
||||
/*
|
||||
* Check that `content` is a PEM certificate (is_private_key = false) or an
|
||||
* RSA/EC private key (is_private_key = true): matching BEGIN/END boundaries
|
||||
* with a base64 body in between. Kanged from luci-proto-openconnect; used by
|
||||
* the rpcd certificate upload and available for future certificate features.
|
||||
*/
|
||||
export function isValidPEM(content, is_private_key) {
|
||||
if (isEmpty(content))
|
||||
return false;
|
||||
|
||||
const beg = is_private_key ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
|
||||
end = is_private_key ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
|
||||
lines = split(trim(content), /[\r\n]/);
|
||||
let start = false, i;
|
||||
|
||||
for (i = 0; i < length(lines); i++) {
|
||||
if (match(lines[i], beg))
|
||||
start = true;
|
||||
else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!start || i < length(lines) - 1 || !match(lines[i], end))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
/* PEM validation end */
|
||||
@@ -7,34 +7,14 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { popen } from 'fs';
|
||||
import { cursor } from 'uci';
|
||||
import { isEmpty, parseURL } from 'homeproxy';
|
||||
import { isEmpty, parseURL, validation } from 'homeproxy';
|
||||
|
||||
const uci = cursor();
|
||||
|
||||
const uciconfig = 'homeproxy';
|
||||
uci.load(uciconfig);
|
||||
|
||||
function random_secret() {
|
||||
const fd = popen('tr -dc "A-Za-z0-9" < /dev/urandom 2>/dev/null | head -c 16');
|
||||
let random = '';
|
||||
if (fd) {
|
||||
random = trim(fd.read('all') || '');
|
||||
fd.close();
|
||||
}
|
||||
|
||||
if (isEmpty(random)) {
|
||||
const fallback = popen('awk \'BEGIN{srand(); printf "%06d", int(rand() * 1000000)}\'');
|
||||
if (fallback) {
|
||||
random = trim(fallback.read('all') || '');
|
||||
fallback.close();
|
||||
}
|
||||
}
|
||||
|
||||
return random;
|
||||
}
|
||||
|
||||
const uciinfra = 'infra',
|
||||
ucimigration = 'migration',
|
||||
ucimain = 'config',
|
||||
@@ -45,16 +25,8 @@ const uciinfra = 'infra',
|
||||
ucirouting = 'routing',
|
||||
uciroutingnode = 'routing_node',
|
||||
uciroutingrule = 'routing_rule',
|
||||
uciclashapi = 'clash_api',
|
||||
ucintp = 'ntp',
|
||||
ucicache = 'cache',
|
||||
uciserver = 'server';
|
||||
|
||||
function is_update_cron(value) {
|
||||
let matched = match(value || '', /^(\d{1,2})\s+(\d{1,2})\s+\*\s+\*\s+([0-7*])$/);
|
||||
return !isEmpty(matched) && int(matched[1]) <= 59 && int(matched[2]) <= 23;
|
||||
}
|
||||
|
||||
/* chinadns-ng has been removed */
|
||||
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
|
||||
uci.delete(uciconfig, uciinfra, 'china_dns_port');
|
||||
@@ -64,7 +36,9 @@ const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
|
||||
if (type(china_dns_server) === 'array') {
|
||||
uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]);
|
||||
} else {
|
||||
if (match(china_dns_server, /,/))
|
||||
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]);
|
||||
}
|
||||
|
||||
@@ -83,93 +57,6 @@ if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
|
||||
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
|
||||
uci.delete(uciconfig, uciinfra, 'tun_gso');
|
||||
|
||||
/* endpoint_independent_nat was removed in sing-box 1.13 */
|
||||
if (!isEmpty(uci.get(uciconfig, ucirouting, 'endpoint_independent_nat')))
|
||||
uci.delete(uciconfig, ucirouting, 'endpoint_independent_nat');
|
||||
|
||||
/* direct outbound proxy_protocol was removed in sing-box 1.13 */
|
||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
if (cfg.type === 'direct' && !isEmpty(cfg.proxy_protocol))
|
||||
uci.delete(uciconfig, cfg['.name'], 'proxy_protocol');
|
||||
});
|
||||
|
||||
const legacy_panel_url = 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip';
|
||||
const legacy_panel_proxy_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip';
|
||||
const old_panel_url = 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
|
||||
const old_panel_proxy_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
|
||||
const default_panel_url = 'https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip';
|
||||
|
||||
/* clash api panel options were introduced */
|
||||
if (!uci.get(uciconfig, uciclashapi))
|
||||
uci.set(uciconfig, uciclashapi, uciconfig);
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_ui')))
|
||||
uci.set(uciconfig, uciclashapi, 'external_ui', '/etc/homeproxy/run/ui');
|
||||
|
||||
const panel_url = uci.get(uciconfig, uciclashapi, 'external_ui_download_url');
|
||||
if (isEmpty(panel_url) || panel_url === legacy_panel_url || panel_url === legacy_panel_proxy_url || panel_url === old_panel_proxy_url)
|
||||
uci.set(uciconfig, uciclashapi, 'external_ui_download_url', default_panel_url);
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_ui_download_detour')))
|
||||
uci.set(uciconfig, uciclashapi, 'external_ui_download_detour', 'direct-out');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'external_controller')))
|
||||
uci.set(uciconfig, uciclashapi, 'external_controller', '0.0.0.0:9095');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'secret')) || uci.get(uciconfig, uciclashapi, 'secret') === 'homeproxy') {
|
||||
let random = random_secret();
|
||||
if (!isEmpty(random))
|
||||
uci.set(uciconfig, uciclashapi, 'secret', random);
|
||||
}
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, uciclashapi, 'default_mode')))
|
||||
uci.set(uciconfig, uciclashapi, 'default_mode', 'rule');
|
||||
|
||||
/* ntp options were moved into a dedicated section */
|
||||
if (!uci.get(uciconfig, ucintp))
|
||||
uci.set(uciconfig, ucintp, uciconfig);
|
||||
|
||||
const legacy_ntp_server = uci.get(uciconfig, uciinfra, 'ntp_server');
|
||||
if (isEmpty(uci.get(uciconfig, ucintp, 'enabled')))
|
||||
uci.set(uciconfig, ucintp, 'enabled', '1');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucintp, 'server')))
|
||||
uci.set(uciconfig, ucintp, 'server', !isEmpty(legacy_ntp_server) ? legacy_ntp_server : 'ntp.aliyun.com');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucintp, 'server_port')))
|
||||
uci.set(uciconfig, ucintp, 'server_port', '123');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucintp, 'interval')))
|
||||
uci.set(uciconfig, ucintp, 'interval', '30m');
|
||||
|
||||
/* cache file options were moved into a dedicated section */
|
||||
if (!uci.get(uciconfig, ucicache))
|
||||
uci.set(uciconfig, ucicache, uciconfig);
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucicache, 'enabled')))
|
||||
uci.set(uciconfig, ucicache, 'enabled', '1');
|
||||
|
||||
const old_cache_path = '/var/run/homeproxy/cache.db';
|
||||
const default_cache_path = '/etc/homeproxy/cache.db';
|
||||
const cache_path = uci.get(uciconfig, ucicache, 'path');
|
||||
if (isEmpty(cache_path) || cache_path === old_cache_path)
|
||||
uci.set(uciconfig, ucicache, 'path', default_cache_path);
|
||||
|
||||
if (!isEmpty(uci.get(uciconfig, ucicache, 'store_fakeip')))
|
||||
uci.delete(uciconfig, ucicache, 'store_fakeip');
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucicache, 'store_rdrc')))
|
||||
uci.set(uciconfig, ucicache, 'store_rdrc', isEmpty(uci.get(uciconfig, ucidns, 'cache_file_store_rdrc')) ? '1' : uci.get(uciconfig, ucidns, 'cache_file_store_rdrc'));
|
||||
|
||||
if (isEmpty(uci.get(uciconfig, ucicache, 'rdrc_timeout')) && !isEmpty(uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout')))
|
||||
uci.set(uciconfig, ucicache, 'rdrc_timeout', uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout'));
|
||||
|
||||
if (!isEmpty(uci.get(uciconfig, ucidns, 'cache_file_store_rdrc')))
|
||||
uci.delete(uciconfig, ucidns, 'cache_file_store_rdrc');
|
||||
|
||||
if (!isEmpty(uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout')))
|
||||
uci.delete(uciconfig, ucidns, 'cache_file_rdrc_timeout');
|
||||
|
||||
/* create migration section */
|
||||
if (!uci.get(uciconfig, ucimigration))
|
||||
uci.set(uciconfig, ucimigration, uciconfig);
|
||||
@@ -177,7 +64,7 @@ if (!uci.get(uciconfig, ucimigration))
|
||||
/* 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"');
|
||||
system('sed -i "/update_crond.sh/d" "/etc/crontabs/root" 2>/dev/null');
|
||||
uci.set(uciconfig, ucimigration, 'crontab', '1');
|
||||
}
|
||||
|
||||
@@ -188,10 +75,6 @@ if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
|
||||
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
|
||||
uci.set(uciconfig, uciserver, 'log_level', 'warn');
|
||||
|
||||
if (!isEmpty(uci.get(uciconfig, 'subscription', 'auto_update_time')) &&
|
||||
!is_update_cron(uci.get(uciconfig, 'subscription', 'auto_update_time')))
|
||||
uci.set(uciconfig, 'subscription', 'auto_update_time', '0 0 * * *');
|
||||
|
||||
/* empty value defaults to all ports now */
|
||||
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
|
||||
uci.delete(uciconfig, ucimain, 'routing_port');
|
||||
@@ -211,6 +94,8 @@ if (default_dns_server === 'block-dns') {
|
||||
uci.set(uciconfig, '_migration_dns_final_block', 'action', 'reject');
|
||||
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
|
||||
}
|
||||
else if (default_dns_server === 'local-dns')
|
||||
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
|
||||
|
||||
const dns_server_migration = {};
|
||||
/* DNS servers options */
|
||||
@@ -288,7 +173,7 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
uci.set(uciconfig, ucirouting, 'default_outbound_dns', cfg.server);
|
||||
break;
|
||||
default:
|
||||
uci.set(uciconfig, cfg.outbound, 'domain_resolver', cfg.server);
|
||||
uci.set(uciconfig, outbound, 'domain_resolver', cfg.server);
|
||||
break;
|
||||
}
|
||||
});
|
||||
@@ -338,6 +223,29 @@ uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||
/* wireguard_gso was deprecated in sb 1.11 */
|
||||
if (!isEmpty(cfg.wireguard_gso))
|
||||
uci.delete(uciconfig, cfg['.name'], 'wireguard_gso');
|
||||
|
||||
/* hysteria_revc_window was a typo, renamed to hysteria_recv_window */
|
||||
if (!isEmpty(cfg.hysteria_revc_window))
|
||||
uci.rename(uciconfig, cfg['.name'], 'hysteria_revc_window', 'hysteria_recv_window');
|
||||
});
|
||||
|
||||
/* sing-box 1.14: remove deprecated DNS semantics */
|
||||
if (!isEmpty(uci.get(uciconfig, ucidns, 'independent_cache')))
|
||||
uci.delete(uciconfig, ucidns, 'independent_cache');
|
||||
|
||||
const legacy_store_rdrc = uci.get(uciconfig, ucidns, 'cache_file_store_rdrc');
|
||||
if (!isEmpty(legacy_store_rdrc)) {
|
||||
uci.set(uciconfig, ucidns, 'cache_file_store_dns', legacy_store_rdrc);
|
||||
uci.delete(uciconfig, ucidns, 'cache_file_store_rdrc');
|
||||
}
|
||||
if (!isEmpty(uci.get(uciconfig, ucidns, 'cache_file_rdrc_timeout')))
|
||||
uci.delete(uciconfig, ucidns, 'cache_file_rdrc_timeout');
|
||||
|
||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||
if (!isEmpty(cfg.domain_strategy))
|
||||
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
|
||||
if (!isEmpty(cfg.rule_set_ip_cidr_accept_empty))
|
||||
uci.delete(uciconfig, cfg['.name'], 'rule_set_ip_cidr_accept_empty');
|
||||
});
|
||||
|
||||
/* routing rules options */
|
||||
@@ -356,47 +264,6 @@ uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||
}
|
||||
});
|
||||
|
||||
/* routing node options */
|
||||
uci.foreach(uciconfig, uciroutingnode, (cfg) => {
|
||||
if (cfg.node === 'urltest' && !isEmpty(cfg.urltest_nodes)) {
|
||||
let custom_nodes = [],
|
||||
subscription_nodes = [];
|
||||
|
||||
for (let node in (type(cfg.urltest_nodes) === 'array' ? cfg.urltest_nodes : [ cfg.urltest_nodes ])) {
|
||||
let node_cfg = uci.get_all(uciconfig, node);
|
||||
if (isEmpty(node_cfg))
|
||||
continue;
|
||||
|
||||
if (!isEmpty(node_cfg.grouphash))
|
||||
push(subscription_nodes, node);
|
||||
else
|
||||
push(custom_nodes, node);
|
||||
}
|
||||
|
||||
if (isEmpty(cfg.selected_nodes) && !isEmpty(custom_nodes))
|
||||
uci.set(uciconfig, cfg['.name'], 'selected_nodes', custom_nodes);
|
||||
|
||||
if (isEmpty(cfg.subscription_nodes) && !isEmpty(subscription_nodes))
|
||||
uci.set(uciconfig, cfg['.name'], 'subscription_nodes', subscription_nodes);
|
||||
|
||||
uci.delete(uciconfig, cfg['.name'], 'urltest_nodes');
|
||||
}
|
||||
});
|
||||
|
||||
/* rule set options */
|
||||
uci.foreach(uciconfig, uciruleset, (cfg) => {
|
||||
if (isEmpty(cfg.tag))
|
||||
uci.set(uciconfig, cfg['.name'], 'tag', 'cfg-' + cfg['.name'] + '-rule');
|
||||
|
||||
if (cfg.type === 'remote') {
|
||||
if (isEmpty(cfg.auto_update))
|
||||
uci.set(uciconfig, cfg['.name'], 'auto_update', '1');
|
||||
|
||||
if (!is_update_cron(cfg.update_interval))
|
||||
uci.set(uciconfig, cfg['.name'], 'update_interval', '0 0 * * *');
|
||||
}
|
||||
});
|
||||
|
||||
/* server options */
|
||||
/* auto_firewall was moved into server options */
|
||||
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
|
||||
@@ -404,13 +271,6 @@ if (!isEmpty(auto_firewall))
|
||||
uci.delete(uciconfig, uciserver, 'auto_firewall');
|
||||
|
||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||
if (!isEmpty(cfg.hysteria_revc_window_client) && isEmpty(cfg.hysteria_recv_window_client)) {
|
||||
uci.set(uciconfig, cfg['.name'], 'hysteria_recv_window_client', cfg.hysteria_revc_window_client);
|
||||
}
|
||||
|
||||
if (!isEmpty(cfg.hysteria_revc_window_client))
|
||||
uci.delete(uciconfig, cfg['.name'], 'hysteria_revc_window_client');
|
||||
|
||||
/* auto_firewall was moved into server options */
|
||||
if (auto_firewall === '1')
|
||||
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2023-2025 ImmortalWrt.org
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { urldecode, urlencode } from 'luci.http';
|
||||
|
||||
import { decodeBase64Str, parseURL, isEmpty, validation } from 'homeproxy';
|
||||
|
||||
/*
|
||||
* Share-link parsers. Every protocol has its own parse_<scheme>_uri() so the
|
||||
* branches can be read, changed and unit-tested in isolation; parse_uri() only
|
||||
* dispatches on the scheme and applies the common address/port validation and
|
||||
* label fallback afterwards.
|
||||
*
|
||||
* `features` is the sing-box feature map (with_quic/with_utls/...), `log` is a
|
||||
* logging callback, both injected so the parsers stay free of ubus/uci.
|
||||
*/
|
||||
|
||||
/* https://shadowsocks.org/guide/sip008.html */
|
||||
export function parse_sip008_uri(uri) {
|
||||
return {
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
|
||||
export function parse_anytls_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
const params = url.searchParams || {};
|
||||
|
||||
return {
|
||||
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'
|
||||
};
|
||||
};
|
||||
|
||||
export function parse_http_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
|
||||
return {
|
||||
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'
|
||||
};
|
||||
};
|
||||
|
||||
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
|
||||
export function parse_hysteria_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
const params = url.searchParams || {};
|
||||
|
||||
if (!features.with_quic || (params.protocol && params.protocol !== 'udp')) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
if (!features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
|
||||
export function parse_hysteria2_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
const params = url.searchParams || {};
|
||||
|
||||
if (!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;
|
||||
}
|
||||
|
||||
return {
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
/* Surge Snell share link: snell://host:port?psk=..&obfs=http&obfs-host=..#name */
|
||||
export function parse_snell_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
const params = url.searchParams || {};
|
||||
|
||||
return {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'snell',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
password: url.username ? urldecode(url.username)
|
||||
: (params.psk ? urldecode(params.psk) : null),
|
||||
snell_version: params.version || '4',
|
||||
snell_userkey: params.userkey,
|
||||
snell_obfs_mode: (params.obfs === 'http') ? 'http' : null,
|
||||
snell_obfs_host: params['obfs-host'],
|
||||
snell_reuse: (params.reuse === '1') ? '1' : '0'
|
||||
};
|
||||
};
|
||||
|
||||
export function parse_socks_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
|
||||
return {
|
||||
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'
|
||||
};
|
||||
};
|
||||
|
||||
export function parse_ss_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
/* "Lovely" Shadowrocket format */
|
||||
const ss_suri = split(uri[1], '#');
|
||||
let ss_slabel = '';
|
||||
if (length(ss_suri) <= 2) {
|
||||
if (length(ss_suri) === 2)
|
||||
ss_slabel = '#' + urlencode(ss_suri[1]);
|
||||
if (decodeBase64Str(ss_suri[0]))
|
||||
uri[1] = decodeBase64Str(ss_suri[0]) + ss_slabel;
|
||||
}
|
||||
|
||||
/* Legacy format is not supported, it should be never appeared in modern subscriptions */
|
||||
/* https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
|
||||
|
||||
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
|
||||
const 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];
|
||||
}
|
||||
|
||||
return {
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
|
||||
export function parse_trojan_uri(uri) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
const params = url.searchParams || {};
|
||||
|
||||
const config = {
|
||||
label: url.hash ? urldecode(url.hash) : null,
|
||||
type: 'trojan',
|
||||
address: url.hostname,
|
||||
port: url.port,
|
||||
password: urldecode(url.username),
|
||||
transport: (params.type !== 'tcp') ? params.type : null,
|
||||
tls: '1',
|
||||
tls_sni: params.sni
|
||||
};
|
||||
switch (params.type) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = params.serviceName;
|
||||
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;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
/* https://github.com/daeuniverse/dae/discussions/182 */
|
||||
export function parse_tuic_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
const params = url.searchParams || {};
|
||||
|
||||
if (!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;
|
||||
}
|
||||
|
||||
return {
|
||||
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,
|
||||
tls: '1',
|
||||
tls_sni: params.sni,
|
||||
tls_alpn: params.alpn ? split(urldecode(params.alpn), ',') : null,
|
||||
};
|
||||
};
|
||||
|
||||
/* https://github.com/XTLS/Xray-core/discussions/716 */
|
||||
export function parse_vless_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
const url = parseURL('http://' + uri[1]) || {};
|
||||
const params = url.searchParams || {};
|
||||
|
||||
/* Unsupported protocol */
|
||||
if (params.type === 'kcp') {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
return null;
|
||||
} else if (params.type === 'quic' && ((params.quicSecurity && params.quicSecurity !== 'none') || !features.with_quic)) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||
if (!features.with_quic)
|
||||
log(sprintf('Please rebuild sing-box with %s support!', 'QUIC'));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const 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: 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;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
|
||||
export function parse_vmess_uri(uri, features, log) {
|
||||
uri = split(trim(uri), '://');
|
||||
|
||||
/* "Lovely" shadowrocket format */
|
||||
if (match(uri, /&/)) {
|
||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||
return null;
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = json(decodeBase64Str(uri[1])) || {};
|
||||
} catch(e) {
|
||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (payload.v != '2') {
|
||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||
return null;
|
||||
/* Unsupported protocol */
|
||||
} else if (payload.net === 'kcp') {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
||||
return null;
|
||||
} else if (payload.net === 'quic' && ((payload.type && payload.type !== 'none') || payload.path || !features.with_quic)) {
|
||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
||||
if (!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 (payload.aid && int(payload.aid) !== 0) {
|
||||
* log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
||||
* return null;
|
||||
* }
|
||||
*/
|
||||
|
||||
const config = {
|
||||
label: payload.ps ? urldecode(payload.ps) : null,
|
||||
type: 'vmess',
|
||||
address: payload.add,
|
||||
port: payload.port,
|
||||
uuid: payload.id,
|
||||
vmess_alterid: payload.aid,
|
||||
vmess_encrypt: payload.scy || 'auto',
|
||||
vmess_global_padding: '1',
|
||||
transport: (payload.net !== 'tcp') ? payload.net : null,
|
||||
tls: (payload.tls === 'tls') ? '1' : '0',
|
||||
tls_sni: payload.sni || payload.host,
|
||||
tls_alpn: payload.alpn ? split(payload.alpn, ',') : null,
|
||||
tls_utls: features.with_utls ? payload.fp : null
|
||||
};
|
||||
switch (payload.net) {
|
||||
case 'grpc':
|
||||
config.grpc_servicename = payload.path;
|
||||
break;
|
||||
case 'h2':
|
||||
case 'tcp':
|
||||
if (payload.net === 'h2' || payload.type === 'http') {
|
||||
config.transport = 'http';
|
||||
config.http_host = payload.host ? split(payload.host, ',') : null;
|
||||
config.http_path = payload.path;
|
||||
}
|
||||
break;
|
||||
case 'httpupgrade':
|
||||
config.httpupgrade_host = payload.host;
|
||||
config.http_path = payload.path;
|
||||
break;
|
||||
case 'ws':
|
||||
config.ws_host = payload.host;
|
||||
config.ws_path = payload.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;
|
||||
}
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
export function parse_uri(uri, features, log) {
|
||||
if (!features) features = {};
|
||||
if (!log) log = function() {};
|
||||
|
||||
let config;
|
||||
|
||||
if (type(uri) === 'object') {
|
||||
if (uri.nodetype === 'sip008')
|
||||
config = parse_sip008_uri(uri);
|
||||
} else if (type(uri) === 'string') {
|
||||
switch (split(trim(uri), '://')[0]) {
|
||||
case 'anytls':
|
||||
config = parse_anytls_uri(uri, features, log);
|
||||
break;
|
||||
case 'http':
|
||||
case 'https':
|
||||
config = parse_http_uri(uri, features, log);
|
||||
break;
|
||||
case 'hysteria':
|
||||
config = parse_hysteria_uri(uri, features, log);
|
||||
break;
|
||||
case 'hysteria2':
|
||||
case 'hy2':
|
||||
config = parse_hysteria2_uri(uri, features, log);
|
||||
break;
|
||||
case 'snell':
|
||||
config = parse_snell_uri(uri, features, log);
|
||||
break;
|
||||
case 'socks':
|
||||
case 'socks4':
|
||||
case 'socks4a':
|
||||
case 'socks5':
|
||||
case 'socks5h':
|
||||
config = parse_socks_uri(uri, features, log);
|
||||
break;
|
||||
case 'ss':
|
||||
config = parse_ss_uri(uri, features, log);
|
||||
break;
|
||||
case 'trojan':
|
||||
config = parse_trojan_uri(uri, features, log);
|
||||
break;
|
||||
case 'tuic':
|
||||
config = parse_tuic_uri(uri, features, log);
|
||||
break;
|
||||
case 'vless':
|
||||
config = parse_vless_uri(uri, features, log);
|
||||
break;
|
||||
case 'vmess':
|
||||
config = parse_vmess_uri(uri, features, log);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isEmpty(config)) {
|
||||
if (config.address)
|
||||
config.address = replace(config.address, /\[|\]/g, '');
|
||||
|
||||
if (!validation('host', config.address) || !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;
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
set -eu
|
||||
|
||||
NAME="homeproxy"
|
||||
RUN_DIR="/var/run/$NAME"
|
||||
REQUEST_FILE="$RUN_DIR/provider-update-id"
|
||||
LOCK_FILE="$RUN_DIR/provider-update-v2.lock"
|
||||
LOG_FILE="$RUN_DIR/homeproxy.log"
|
||||
PROVIDER_ID="${1:-}"
|
||||
|
||||
log() {
|
||||
printf '%s [SUBSCRIBE] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG_FILE"
|
||||
}
|
||||
|
||||
case "$PROVIDER_ID" in
|
||||
*[!0-9a-f]*|'') exit 1 ;;
|
||||
esac
|
||||
[ "${#PROVIDER_ID}" -eq 32 ] || exit 1
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
if ! lock -n "$LOCK_FILE" 2>/dev/null; then
|
||||
log "Another provider update is running; waiting for it to finish..."
|
||||
lock "$LOCK_FILE"
|
||||
fi
|
||||
|
||||
umask 077
|
||||
printf '%s\n' "$PROVIDER_ID" > "$REQUEST_FILE"
|
||||
cleanup() {
|
||||
rm -f "$REQUEST_FILE"
|
||||
lock -u "$LOCK_FILE" 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
# Give the Clash API response time to reach the panel before HomeProxy restarts.
|
||||
log "Updating provider $PROVIDER_ID from Clash API..."
|
||||
sleep 1
|
||||
if /etc/homeproxy/scripts/update_subscriptions.uc; then
|
||||
log "Provider $PROVIDER_ID update completed."
|
||||
else
|
||||
log "Provider $PROVIDER_ID update failed."
|
||||
exit 4
|
||||
fi
|
||||
@@ -21,65 +21,89 @@ to_upper() {
|
||||
}
|
||||
|
||||
check_list_update() {
|
||||
local LIST_FILE="$1"
|
||||
local REPO_NAME="$2"
|
||||
local REPO_BRANCH="$3"
|
||||
local REPO_FILE="$4"
|
||||
local LOCK_FILE="$RUN_DIR/update_resources-$LIST_FILE.lock"
|
||||
local GITHUB_TOKEN="$(uci -q get homeproxy.config.github_token)"
|
||||
local listtype="$1"
|
||||
local listrepo="$2"
|
||||
local listref="$3"
|
||||
local listname="$4"
|
||||
local lock="$RUN_DIR/update_resources-$listtype.lock"
|
||||
local github_token="$(uci -q get homeproxy.config.github_token)"
|
||||
local wget="wget --timeout=10 -q"
|
||||
|
||||
if ! lock -n "$LOCK_FILE" 2>"/dev/null"; then
|
||||
log "[$(to_upper "$LIST_FILE")] A task is already running."
|
||||
exec 200>"$lock"
|
||||
if ! flock -n 200 &> "/dev/null"; then
|
||||
log "[$(to_upper "$listtype")] A task is already running."
|
||||
return 2
|
||||
fi
|
||||
|
||||
local NEW_VER=$(curl -sL ${GITHUB_TOKEN:+-H "Authorization: Bearer $GITHUB_TOKEN"} "https://api.github.com/repos/$REPO_NAME/releases/latest" | jsonfilter -e "@.tag_name")
|
||||
if [ -z "$NEW_VER" ]; then
|
||||
log "[$(to_upper "$LIST_FILE")] Failed to get the latest version, please retry later."
|
||||
|
||||
return 1
|
||||
local github_header_file=""
|
||||
if [ -n "$github_token" ]; then
|
||||
github_header_file="$RUN_DIR/.gh_header_${listtype}"
|
||||
( umask 077; printf 'Authorization: Bearer %s\n' "$github_token" > "$github_header_file" )
|
||||
trap "[ -n \"$github_header_file\" ] && rm -f \"$github_header_file\"" EXIT INT TERM
|
||||
fi
|
||||
|
||||
local OLD_VER=$(cat "$RESOURCES_DIR/$LIST_FILE.ver" 2>/dev/null || echo "NOT FOUND")
|
||||
if [ "$OLD_VER" = "$NEW_VER" ]; then
|
||||
log "[$(to_upper "$LIST_FILE")] Current version: $NEW_VER."
|
||||
log "[$(to_upper "$LIST_FILE")] You're already at the latest version."
|
||||
local list_info="$($wget ${github_header_file:+--header-file=$github_header_file} -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1")"
|
||||
local wget_exit=$?
|
||||
|
||||
[ -n "$github_header_file" ] && rm -f "$github_header_file"
|
||||
trap - EXIT INT TERM
|
||||
|
||||
if [ $wget_exit -ne 0 ]; then
|
||||
log "[$(to_upper "$listtype")] Failed to fetch version info (wget exit $wget_exit)."
|
||||
return 1
|
||||
fi
|
||||
local list_sha="$(echo -e "$list_info" | jsonfilter -qe "@[0].sha")"
|
||||
local list_date="$(echo -e "$list_info" | jsonfilter -qe "@[0].commit.committer.date" | cut -d 'T' -f1)"
|
||||
if [ -z "$list_sha" ]; then
|
||||
log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later."
|
||||
return 1
|
||||
fi
|
||||
local list_ver="${list_date:+$list_date }$list_sha"
|
||||
|
||||
local local_list_ver="$(cat "$RESOURCES_DIR/$listtype.ver" 2>"/dev/null" || echo "NOT_FOUND")"
|
||||
local local_list_sha="${local_list_ver##* }"
|
||||
local local_list_disp="${local_list_ver%% *}"
|
||||
if [ "$local_list_sha" = "$list_sha" ]; then
|
||||
[ "$local_list_ver" = "$local_list_sha" ] && [ -n "$list_date" ] && \
|
||||
echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
|
||||
log "[$(to_upper "$listtype")] Current version: ${list_ver%% *}."
|
||||
log "[$(to_upper "$listtype")] You're already at the latest version."
|
||||
return 3
|
||||
else
|
||||
log "[$(to_upper "$LIST_FILE")] Local version: $OLD_VER, latest version: $NEW_VER."
|
||||
log "[$(to_upper "$listtype")] Local version: $local_list_disp, latest version: ${list_ver%% *}."
|
||||
fi
|
||||
|
||||
if ! curl -sL -o "$RUN_DIR/$REPO_FILE" "https://cdn.jsdelivr.net/gh/$REPO_NAME@$REPO_BRANCH/$REPO_FILE" || [ ! -s "$RUN_DIR/$REPO_FILE" ]; then
|
||||
rm -f "$RUN_DIR/$REPO_FILE"
|
||||
log "[$(to_upper "$LIST_FILE")] Update failed."
|
||||
|
||||
if ! $wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname" || [ ! -s "$RUN_DIR/$listname" ]; then
|
||||
rm -f "$RUN_DIR/$listname"
|
||||
log "[$(to_upper "$listtype")] Download failed."
|
||||
return 1
|
||||
fi
|
||||
|
||||
mv -f "$RUN_DIR/$REPO_FILE" "$RESOURCES_DIR/$LIST_FILE.${REPO_FILE##*.}"
|
||||
echo -e "$NEW_VER" > "$RESOURCES_DIR/$LIST_FILE.ver"
|
||||
log "[$(to_upper "$LIST_FILE")] Successfully updated."
|
||||
if mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}"; then
|
||||
echo -e "$list_ver" > "$RESOURCES_DIR/$listtype.ver"
|
||||
log "[$(to_upper "$listtype")] Successfully updated."
|
||||
else
|
||||
rm -f "$RUN_DIR/$listname"
|
||||
log "[$(to_upper "$listtype")] Failed to install update (mv failed)."
|
||||
return 1
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
"china_ip4")
|
||||
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "cncidr.txt" && \
|
||||
sed -i "/IP-CIDR6,/d; s/IP-CIDR,//g" "$RESOURCES_DIR/china_ip4.txt"
|
||||
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv4.txt"
|
||||
;;
|
||||
"china_ip6")
|
||||
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "cncidr.txt" && \
|
||||
sed -i "/IP-CIDR,/d; s/IP-CIDR6,//g" "$RESOURCES_DIR/china_ip6.txt"
|
||||
check_list_update "$1" "1715173329/IPCIDR-CHINA" "master" "ipv6.txt"
|
||||
;;
|
||||
"gfw_list")
|
||||
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "gfw.txt" && \
|
||||
sed -i "s/^\.//g" "$RESOURCES_DIR/gfw_list.txt"
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "gfw.txt"
|
||||
;;
|
||||
"china_list")
|
||||
check_list_update "$1" "Loyalsoldier/surge-rules" "release" "direct.txt" && \
|
||||
sed -i "s/^\.//g" "$RESOURCES_DIR/china_list.txt"
|
||||
check_list_update "$1" "Loyalsoldier/v2ray-rules-dat" "release" "direct-list.txt" && \
|
||||
sed -i -e "s/full://g" -e "/:/d" "$RESOURCES_DIR/china_list.txt"
|
||||
;;
|
||||
*)
|
||||
echo -e "Usage: $0 <china_ip4 / china_ip6 / gfw_list / china_list>"
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2025 ImmortalWrt.org
|
||||
|
||||
CACHE_PATH="$(uci -q get homeproxy.cache.path)"
|
||||
[ -n "$CACHE_PATH" ] || CACHE_PATH="/etc/homeproxy/cache.db"
|
||||
|
||||
rm -f "$CACHE_PATH"
|
||||
/etc/init.d/homeproxy restart
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,94 +15,60 @@ 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
|
||||
# fw4 chain/set inventory; single source of truth shared with firewall_post.ut
|
||||
[ -f "$HP_DIR/scripts/fw4_names.sh" ] && . "$HP_DIR/scripts/fw4_names.sh"
|
||||
|
||||
log() {
|
||||
echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH"
|
||||
}
|
||||
|
||||
init_logs() {
|
||||
mkdir -p "$RUN_DIR"
|
||||
mkdir -p "$HP_DIR/run/ui"
|
||||
touch "$LOG_PATH" "$RUN_DIR/sing-box-c.log" "$RUN_DIR/sing-box-s.log"
|
||||
chown -R sing-box:sing-box "$RUN_DIR" 2>"/dev/null"
|
||||
chown -R sing-box:sing-box "$HP_DIR/run" 2>"/dev/null"
|
||||
}
|
||||
# Locate the directory dnsmasq reads the homeproxy snippets from. The UCI
|
||||
# section name of the dnsmasq config is part of /tmp/etc/dnsmasq.conf.<name>,
|
||||
# but `uci get` cannot return a section name (there is no `__name__`), so it is
|
||||
# derived from the first `uci show` line. Either step can fail on an unusual
|
||||
# setup; fall back to the generic directory and say so in the log instead of
|
||||
# silently writing dnsmasq snippets nobody reads.
|
||||
DNSMASQ_UCI_CONFIG="$(uci -q show "dhcp.@dnsmasq[0]" 2>"/dev/null" | head -n1)"
|
||||
DNSMASQ_UCI_CONFIG="${DNSMASQ_UCI_CONFIG#*.}"
|
||||
DNSMASQ_UCI_CONFIG="${DNSMASQ_UCI_CONFIG%%=*}"
|
||||
|
||||
valid_update_cron() {
|
||||
echo "$1" | grep -Eq '^([0-9]|[0-5][0-9]) ([0-9]|[01][0-9]|2[0-3]) \* \* ([0-7]|\*)$'
|
||||
}
|
||||
DNSMASQ_DIR=""
|
||||
if [ -n "$DNSMASQ_UCI_CONFIG" ] && [ -f "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG" ]; then
|
||||
DNSMASQ_CONF_DIR="$(awk -F '=' '/^conf-dir=/ {print $2; exit}' "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG")"
|
||||
[ -n "$DNSMASQ_CONF_DIR" ] && DNSMASQ_DIR="$DNSMASQ_CONF_DIR/dnsmasq-homeproxy.d"
|
||||
fi
|
||||
|
||||
has_ruleset_cron() {
|
||||
printf '%s\n' "$ruleset_cron_list" | grep -Fxq -- "$1"
|
||||
}
|
||||
if [ -z "$DNSMASQ_DIR" ]; then
|
||||
log "WARNING: unable to derive the dnsmasq conf-dir (section '${DNSMASQ_UCI_CONFIG:-unknown}'), using /tmp/dnsmasq.d/dnsmasq-homeproxy.d."
|
||||
DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-homeproxy.d"
|
||||
fi
|
||||
|
||||
add_ruleset_cron() {
|
||||
local section="$1"
|
||||
local enabled auto_update type update_time
|
||||
|
||||
config_get_bool enabled "$section" "enabled" "0"
|
||||
config_get_bool auto_update "$section" "auto_update" "1"
|
||||
config_get type "$section" "type" "local"
|
||||
config_get update_time "$section" "update_interval" ""
|
||||
|
||||
[ "$enabled" = "1" ] || return
|
||||
[ "$auto_update" = "1" ] || return
|
||||
[ "$type" = "remote" ] || return
|
||||
[ -n "$update_time" ] || return
|
||||
valid_update_cron "$update_time" || return
|
||||
has_ruleset_cron "$update_time" && return
|
||||
|
||||
echo -e "$update_time sh $HP_DIR/scripts/update_rulesets.sh #${CONF}_autosetup" >> "/etc/crontabs/root"
|
||||
ruleset_cron_list="${ruleset_cron_list}
|
||||
${update_time}"
|
||||
cron_reload="1"
|
||||
}
|
||||
|
||||
setup_auto_update_cron() {
|
||||
local cron_reload="0"
|
||||
mkdir -p "/etc/crontabs"
|
||||
touch "/etc/crontabs/root"
|
||||
grep -q "#${CONF}_autosetup" "/etc/crontabs/root" 2>"/dev/null" && cron_reload="1"
|
||||
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null"
|
||||
|
||||
local auto_update auto_update_time
|
||||
config_get_bool auto_update "subscription" "auto_update" "0"
|
||||
if [ "$auto_update" = "1" ]; then
|
||||
config_get auto_update_time "subscription" "auto_update_time" "0 0 * * *"
|
||||
if valid_update_cron "$auto_update_time"; then
|
||||
echo -e "$auto_update_time sh $HP_DIR/scripts/update_crond.sh #${CONF}_autosetup" >> "/etc/crontabs/root"
|
||||
cron_reload="1"
|
||||
fi
|
||||
fi
|
||||
|
||||
local ruleset_cron_list=""
|
||||
config_foreach add_ruleset_cron "ruleset"
|
||||
[ "$cron_reload" = "1" ] && /etc/init.d/cron restart
|
||||
restore_upnp_mappings() {
|
||||
[ -x /etc/init.d/miniupnpd ] && /etc/init.d/miniupnpd enabled && /etc/init.d/miniupnpd running \
|
||||
&& /etc/init.d/miniupnpd restart >"/dev/null" 2>&1
|
||||
}
|
||||
|
||||
start_service() {
|
||||
init_logs
|
||||
|
||||
config_load "$CONF"
|
||||
setup_auto_update_cron
|
||||
|
||||
local sb_ver sb_major sb_minor
|
||||
sb_ver="$(sing-box version -n 2>/dev/null)"
|
||||
if [ -z "$sb_ver" ]; then
|
||||
log "Error: cannot detect sing-box version, abort."
|
||||
return 1
|
||||
fi
|
||||
sb_major="${sb_ver%%.*}"
|
||||
sb_minor="${sb_ver#*.}"
|
||||
sb_minor="${sb_minor%%.*}"
|
||||
if [ "$sb_major" -lt 1 ] || { [ "$sb_major" -eq 1 ] && [ "$sb_minor" -lt 14 ]; }; then
|
||||
log "Error: sing-box >= 1.14.0 required, found ${sb_ver}."
|
||||
return 1
|
||||
fi
|
||||
|
||||
local routing_mode proxy_mode
|
||||
config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
|
||||
config_get proxy_mode "config" "proxy_mode" "redirect_tproxy"
|
||||
|
||||
local cache_path
|
||||
config_get cache_path "cache" "path" "$HP_DIR/cache.db"
|
||||
mkdir -p "$(dirname "$cache_path")"
|
||||
[ -e "$cache_path" ] || touch "$cache_path"
|
||||
chown sing-box:sing-box "$cache_path" 2>"/dev/null"
|
||||
|
||||
local outbound_node
|
||||
if [ "$routing_mode" != "custom" ]; then
|
||||
config_get outbound_node "config" "main_node" "nil"
|
||||
@@ -114,11 +80,28 @@ start_service() {
|
||||
config_get_bool server_enabled "server" "enabled" "0"
|
||||
|
||||
if [ "$outbound_node" = "nil" ] && [ "$server_enabled" = "0" ]; then
|
||||
log "Neither client nor server is configured. Service not started."
|
||||
return 1
|
||||
fi
|
||||
|
||||
mkdir -p "$RUN_DIR"
|
||||
|
||||
local disable_gso=0
|
||||
uname -r | grep -Eq "^6\.6" && disable_gso=1
|
||||
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
# Generate/Validate client config
|
||||
local wan_waited=0
|
||||
while ! { ip route show default 2>"/dev/null" | grep -q . \
|
||||
|| ip -6 route show default 2>"/dev/null" | grep -q . \
|
||||
|| ifstatus wan 2>"/dev/null" | grep -q '"up": true'; }; do
|
||||
wan_waited=$((wan_waited + 1))
|
||||
if [ "$wan_waited" -ge 60 ]; then
|
||||
log "WARNING: No network route after 60s, starting sing-box anyway."
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH"
|
||||
|
||||
if [ ! -e "$RUN_DIR/sing-box-c.json" ]; then
|
||||
@@ -129,15 +112,26 @@ start_service() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# DNSMasq rules
|
||||
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" \
|
||||
|| log "Warning: failed to drop the previous auto-update cron entry."
|
||||
echo -e "0 $auto_update_time * * * $HP_DIR/scripts/update_crond.sh #${CONF}_autosetup" >> "/etc/crontabs/root" \
|
||||
|| log "Warning: failed to install the auto-update cron entry."
|
||||
/etc/init.d/cron restart >"/dev/null" 2>&1 || log "Warning: failed to restart cron."
|
||||
fi
|
||||
|
||||
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"
|
||||
mkdir -p "$DNSMASQ_DIR" || log "Warning: failed to create ${DNSMASQ_DIR}."
|
||||
echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" \
|
||||
|| log "Warning: failed to write the dnsmasq conf-dir file."
|
||||
case "$routing_mode" in
|
||||
"bypass_mainland_china"|"custom"|"global")
|
||||
cat <<-EOF >> "$DNSMASQ_DIR/redirect-dns.conf"
|
||||
cat <<-EOF > "$DNSMASQ_DIR/redirect-dns.conf"
|
||||
no-poll
|
||||
no-resolv
|
||||
server=127.0.0.1#$dns_port
|
||||
@@ -161,7 +155,9 @@ start_service() {
|
||||
sed -r -e '/^\s*$/d' -e "s/(.*)/server=\/\1\/127.0.0.1#$dns_port\nnftset=\/\1\\/4#inet#fw4#homeproxy_wan_proxy_addr_v4$wan_nftset_v6/g" \
|
||||
"$HP_DIR/resources/proxy_list.txt" > "$DNSMASQ_DIR/proxy_list.conf"
|
||||
fi
|
||||
# Setup routing table
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1 \
|
||||
|| log "Warning: failed to restart dnsmasq, DNS-based routing may be stale."
|
||||
|
||||
local table_mark
|
||||
config_get table_mark "infra" "table_mark" "100"
|
||||
case "$proxy_mode" in
|
||||
@@ -172,12 +168,16 @@ start_service() {
|
||||
local tproxy_mark
|
||||
config_get tproxy_mark "infra" "tproxy_mark" "101"
|
||||
|
||||
ip rule add fwmark "$tproxy_mark" table "$table_mark"
|
||||
ip route add local 0.0.0.0/0 dev lo table "$table_mark"
|
||||
ip rule add fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null" \
|
||||
|| log "Error: failed to add the tproxy ip rule (fwmark ${tproxy_mark}, table ${table_mark})."
|
||||
ip route add local 0.0.0.0/0 dev lo table "$table_mark" 2>"/dev/null" \
|
||||
|| log "Error: failed to add the tproxy local route (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"
|
||||
ip -6 rule add fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null" \
|
||||
|| log "Error: failed to add the tproxy IPv6 rule (fwmark ${tproxy_mark}, table ${table_mark})."
|
||||
ip -6 route add local ::/0 dev lo table "$table_mark" 2>"/dev/null" \
|
||||
|| log "Error: failed to add the tproxy IPv6 local route (table ${table_mark})."
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
@@ -186,33 +186,38 @@ start_service() {
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
config_get tun_mark "infra" "tun_mark" "102"
|
||||
|
||||
ip tuntap add mode tun user root name "$tun_name"
|
||||
ip tuntap add mode tun user root name "$tun_name" 2>"/dev/null" \
|
||||
|| log "Error: failed to create the TUN device ${tun_name}."
|
||||
sleep 1s
|
||||
ip link set "$tun_name" up
|
||||
ip link set "$tun_name" up 2>"/dev/null" \
|
||||
|| log "Error: failed to bring up the TUN device ${tun_name}."
|
||||
|
||||
ip route replace default dev "$tun_name" table "$table_mark"
|
||||
ip rule add fwmark "$tun_mark" lookup "$table_mark"
|
||||
ip route replace default dev "$tun_name" table "$table_mark" 2>"/dev/null" \
|
||||
|| log "Error: failed to set the TUN default route (table ${table_mark})."
|
||||
ip rule add fwmark "$tun_mark" lookup "$table_mark" 2>"/dev/null" \
|
||||
|| log "Error: failed to add the TUN ip rule (fwmark ${tun_mark}, table ${table_mark})."
|
||||
|
||||
ip -6 route replace default dev "$tun_name" table "$table_mark"
|
||||
ip -6 rule add fwmark "$tun_mark" lookup "$table_mark"
|
||||
if [ "$ipv6_support" -eq "1" ]; then
|
||||
ip -6 route replace default dev "$tun_name" table "$table_mark" 2>"/dev/null" \
|
||||
|| log "Error: failed to set the TUN IPv6 default route (table ${table_mark})."
|
||||
ip -6 rule add fwmark "$tun_mark" lookup "$table_mark" 2>"/dev/null" \
|
||||
|| log "Error: failed to add the TUN IPv6 rule (fwmark ${tun_mark}, table ${table_mark})."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# sing-box (client)
|
||||
procd_open_instance "sing-box-c"
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command run --config "$RUN_DIR/sing-box-c.json"
|
||||
|
||||
# QUIC-GO GSO is broken on kernel 6.6 currently
|
||||
uname -r | grep -Eq "^6\.6" && procd_set_param env "QUIC_GO_DISABLE_GSO"="true"
|
||||
[ "$disable_gso" -eq "1" ] && procd_set_param env "QUIC_GO_DISABLE_GSO"="true"
|
||||
|
||||
if [ -x "/sbin/ujail" ] && [ "$routing_mode" != "custom" ] && ! grep -Eq '"type": "(wireguard|tun)"' "$RUN_DIR/sing-box-c.json"; then
|
||||
procd_add_jail "sing-box-c" log procfs
|
||||
procd_add_jail_mount "$RUN_DIR/sing-box-c.json"
|
||||
procd_add_jail_mount_rw "$RUN_DIR/sing-box-c.log"
|
||||
procd_add_jail_mount_rw "$cache_path"
|
||||
procd_add_jail_mount_rw "$HP_DIR/run/"
|
||||
[ "$routing_mode" != "bypass_mainland_china" ] || procd_add_jail_mount_rw "$HP_DIR/cache.db"
|
||||
procd_add_jail_mount "$HP_DIR/certs/"
|
||||
procd_add_jail_mount "/etc/ssl/"
|
||||
procd_add_jail_mount "/etc/localtime"
|
||||
@@ -232,7 +237,6 @@ start_service() {
|
||||
fi
|
||||
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
# Generate/Validate server config
|
||||
ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH"
|
||||
|
||||
if [ ! -e "$RUN_DIR/sing-box-s.json" ]; then
|
||||
@@ -243,14 +247,12 @@ start_service() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# sing-box (server)
|
||||
procd_open_instance "sing-box-s"
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command run --config "$RUN_DIR/sing-box-s.json"
|
||||
|
||||
# QUIC-GO GSO is broken on kernel 6.6 currently
|
||||
uname -r | grep -Eq "^6\.6" && procd_set_param env "QUIC_GO_DISABLE_GSO"="true"
|
||||
[ "$disable_gso" -eq "1" ] && procd_set_param env "QUIC_GO_DISABLE_GSO"="true"
|
||||
|
||||
if [ -x "/sbin/ujail" ]; then
|
||||
procd_add_jail "sing-box-s" log procfs
|
||||
@@ -275,43 +277,46 @@ start_service() {
|
||||
procd_close_instance
|
||||
fi
|
||||
|
||||
# log-cleaner
|
||||
procd_open_instance "log-cleaner"
|
||||
procd_set_param command "$HP_DIR/scripts/clean_log.sh"
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
|
||||
case "$routing_mode" in
|
||||
"bypass_mainland_china")
|
||||
[ -e "$HP_DIR/cache.db" ] || touch "$HP_DIR/cache.db" \
|
||||
|| log "Warning: failed to create ${HP_DIR}/cache.db."
|
||||
;;
|
||||
"custom")
|
||||
# Prepare ruleset directory
|
||||
[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset"
|
||||
[ -d "$HP_DIR/ruleset" ] || mkdir -p "$HP_DIR/ruleset" \
|
||||
|| log "Warning: failed to create ${HP_DIR}/ruleset."
|
||||
;;
|
||||
esac
|
||||
|
||||
[ "$outbound_node" = "nil" ] || : > "$RUN_DIR/sing-box-c.log"
|
||||
[ "$outbound_node" = "nil" ] || echo > "$RUN_DIR/sing-box-c.log"
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
: > "$RUN_DIR/sing-box-s.log"
|
||||
mkdir -p "$HP_DIR/certs"
|
||||
echo > "$RUN_DIR/sing-box-s.log"
|
||||
mkdir -p "$HP_DIR/certs" || log "Warning: failed to create ${HP_DIR}/certs."
|
||||
fi
|
||||
|
||||
# Update permissions for ujail
|
||||
chown -R sing-box:sing-box "$RUN_DIR"
|
||||
chown sing-box:sing-box "$RUN_DIR"/sing-box-*.json "$RUN_DIR"/sing-box-*.log "$HP_DIR"/cache.db 2>"/dev/null" \
|
||||
|| log "Warning: failed to change the ownership of the runtime files to sing-box."
|
||||
|
||||
# Setup firewall
|
||||
ucode "$HP_DIR/scripts/firewall_pre.uc"
|
||||
[ "$outbound_node" = "nil" ] || utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft"
|
||||
fw4 reload >"/dev/null" 2>&1
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
ucode "$HP_DIR/scripts/firewall_pre.uc" 2>"/dev/null" || log "Error: firewall pre-script failed."
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
utpl -S "$HP_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fw4_post.nft" 2>"/dev/null" || log "Error: firewall post-script failed."
|
||||
fi
|
||||
fw4 reload >"/dev/null" 2>&1 || log "Error: fw4 reload failed."
|
||||
restore_upnp_mappings
|
||||
|
||||
log "sing-box $(sing-box version -n) started."
|
||||
log "sing-box $(sing-box version -n 2>/dev/null || echo "unknown") started."
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null"
|
||||
/etc/init.d/cron restart >"/dev/null" 2>&1
|
||||
sed -i "/#${CONF}_autosetup/d" "/etc/crontabs/root" 2>"/dev/null" \
|
||||
|| log "Warning: failed to drop the auto-update cron entry."
|
||||
/etc/init.d/cron restart >"/dev/null" 2>&1 || log "Warning: failed to restart cron."
|
||||
|
||||
# Setup firewall
|
||||
# Load config
|
||||
config_load "$CONF"
|
||||
local table_mark tproxy_mark tun_mark tun_name
|
||||
config_get table_mark "infra" "table_mark" "100"
|
||||
@@ -319,61 +324,53 @@ stop_service() {
|
||||
config_get tun_mark "infra" "tun_mark" "102"
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
|
||||
# Tproxy
|
||||
ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"
|
||||
# ip rule del removes a single matching rule, so a repeated start (or an
|
||||
# upgrade that starts before the old instance stopped) leaves duplicates
|
||||
# behind. Delete every match instead of only the first one.
|
||||
while ip rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"; do :; done
|
||||
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"
|
||||
while ip -6 rule del fwmark "$tproxy_mark" table "$table_mark" 2>"/dev/null"; do :; done
|
||||
ip -6 route del local ::/0 dev lo table "$table_mark" 2>"/dev/null"
|
||||
|
||||
# TUN
|
||||
ip route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
|
||||
ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
|
||||
while ip rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"; do :; done
|
||||
|
||||
ip -6 route del default dev "$tun_name" table "$table_mark" 2>"/dev/null"
|
||||
ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"
|
||||
while ip -6 rule del fwmark "$tun_mark" table "$table_mark" 2>"/dev/null"; do :; done
|
||||
|
||||
# Nftables rules
|
||||
for i in "homeproxy_dstnat_redir" "homeproxy_output_redir" \
|
||||
"homeproxy_redirect" "homeproxy_redirect_proxy" \
|
||||
"homeproxy_redirect_proxy_port" "homeproxy_redirect_lanac" \
|
||||
"homeproxy_mangle_prerouting" "homeproxy_mangle_output" \
|
||||
"homeproxy_mangle_tproxy" "homeproxy_mangle_tproxy_port" \
|
||||
"homeproxy_mangle_tproxy_lanac" "homeproxy_mangle_mark" \
|
||||
"homeproxy_mangle_tun" "homeproxy_mangle_tun_mark"; do
|
||||
nft flush chain inet fw4 "$i"
|
||||
nft delete chain inet fw4 "$i"
|
||||
done 2>"/dev/null"
|
||||
for i in "homeproxy_local_addr_v4" "homeproxy_local_addr_v6" \
|
||||
"homeproxy_gfw_list_v4" "homeproxy_gfw_list_v6" \
|
||||
"homeproxy_mainland_addr_v4" "homeproxy_mainland_addr_v6" \
|
||||
"homeproxy_wan_proxy_addr_v4" "homeproxy_wan_proxy_addr_v6" \
|
||||
"homeproxy_wan_direct_addr_v4" "homeproxy_wan_direct_addr_v6" \
|
||||
"homeproxy_routing_port"; do
|
||||
nft flush set inet fw4 "$i"
|
||||
nft delete set inet fw4 "$i"
|
||||
done 2>"/dev/null"
|
||||
echo 2>"/dev/null" > "$RUN_DIR/fw4_forward.nft"
|
||||
echo 2>"/dev/null" > "$RUN_DIR/fw4_input.nft"
|
||||
echo 2>"/dev/null" > "$RUN_DIR/fw4_post.nft"
|
||||
fw4 reload >"/dev/null" 2>&1
|
||||
# Flush and delete every fw4 object homeproxy owns. Deleting one at a
|
||||
# time matters: `nft -f` batches are atomic, so a single object that
|
||||
# does not exist in the current proxy mode would roll back the rest.
|
||||
local fw4_name
|
||||
for fw4_name in $HP_FW4_CHAINS; do
|
||||
nft flush chain inet fw4 "$fw4_name" 2>"/dev/null"
|
||||
nft delete chain inet fw4 "$fw4_name" 2>"/dev/null"
|
||||
done
|
||||
for fw4_name in $HP_FW4_SETS; do
|
||||
nft flush set inet fw4 "$fw4_name" 2>"/dev/null"
|
||||
nft delete set inet fw4 "$fw4_name" 2>"/dev/null"
|
||||
done
|
||||
: > "$RUN_DIR/fw4_forward.nft"
|
||||
: > "$RUN_DIR/fw4_input.nft"
|
||||
: > "$RUN_DIR/fw4_post.nft"
|
||||
fw4 reload >"/dev/null" 2>&1 || log "Warning: fw4 reload failed during stop."
|
||||
restore_upnp_mappings
|
||||
|
||||
# Remove DNS hijack
|
||||
rm -rf "$DNSMASQ_DIR/../dnsmasq-homeproxy.conf" "$DNSMASQ_DIR"
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1
|
||||
/etc/init.d/dnsmasq restart >"/dev/null" 2>&1 \
|
||||
|| log "Warning: failed to restart dnsmasq after removing the homeproxy snippets."
|
||||
|
||||
rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-s.json"
|
||||
init_logs
|
||||
rm -f "$RUN_DIR/sing-box-c.json" "$RUN_DIR/sing-box-c.log" \
|
||||
"$RUN_DIR/sing-box-s.json" "$RUN_DIR/sing-box-s.log"
|
||||
|
||||
log "Service stopped."
|
||||
}
|
||||
|
||||
service_stopped() {
|
||||
# Load config
|
||||
config_load "$CONF"
|
||||
local tun_name
|
||||
config_get tun_name "infra" "tun_name" "singtun0"
|
||||
|
||||
# TUN
|
||||
ip link set "$tun_name" down 2>"/dev/null"
|
||||
ip tuntap del mode tun name "$tun_name" 2>"/dev/null"
|
||||
}
|
||||
@@ -381,6 +378,35 @@ service_stopped() {
|
||||
reload_service() {
|
||||
log "Reloading service..."
|
||||
|
||||
config_load "$CONF"
|
||||
local routing_mode
|
||||
config_get routing_mode "config" "routing_mode" "bypass_mainland_china"
|
||||
|
||||
local outbound_node
|
||||
if [ "$routing_mode" != "custom" ]; then
|
||||
config_get outbound_node "config" "main_node" "nil"
|
||||
else
|
||||
config_get outbound_node "routing" "default_outbound" "nil"
|
||||
fi
|
||||
|
||||
if [ "$outbound_node" != "nil" ]; then
|
||||
ucode -S "$HP_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH"
|
||||
if ! "$PROG" check --config "$RUN_DIR/sing-box-c.json" 2>>"$LOG_PATH"; then
|
||||
log "Error: new client config is invalid, reload aborted."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
local server_enabled
|
||||
config_get_bool server_enabled "server" "enabled" "0"
|
||||
if [ "$server_enabled" = "1" ]; then
|
||||
ucode -S "$HP_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH"
|
||||
if ! "$PROG" check --config "$RUN_DIR/sing-box-s.json" 2>>"$LOG_PATH"; then
|
||||
log "Error: new server config is invalid, reload aborted."
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
Executable → Regular
-30
@@ -3,36 +3,6 @@
|
||||
[ -f "/www/luci-static/resources/icons/loading.gif" ] && \
|
||||
sed -i "s,/loading.svg,/loading.gif,g" "/www/luci-static/resources/view/homeproxy/status.js"
|
||||
|
||||
uci -q set luci.main.lang='zh_cn'
|
||||
uci -q commit luci
|
||||
|
||||
api_secret="$(uci -q get homeproxy.clash_api.secret)"
|
||||
if [ -z "$api_secret" ] || [ "$api_secret" = "homeproxy" ]; then
|
||||
api_secret="$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>"/dev/null" | head -c 16)"
|
||||
[ -n "$api_secret" ] || api_secret="$(awk 'BEGIN{srand(); printf "%06d", int(rand() * 1000000)}')"
|
||||
uci -q set homeproxy.clash_api.secret="$api_secret"
|
||||
uci -q commit homeproxy
|
||||
fi
|
||||
|
||||
legacy_panel_url="https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip"
|
||||
legacy_panel_proxy_url="https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip"
|
||||
old_panel_url="https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
old_panel_proxy_url="https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
default_panel_url="https://gh-proxy.com/https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip"
|
||||
panel_url="$(uci -q get homeproxy.clash_api.external_ui_download_url)"
|
||||
if [ -z "$panel_url" ] || [ "$panel_url" = "$legacy_panel_url" ] || [ "$panel_url" = "$legacy_panel_proxy_url" ] || [ "$panel_url" = "$old_panel_proxy_url" ]; then
|
||||
uci -q set homeproxy.clash_api.external_ui_download_url="$default_panel_url"
|
||||
uci -q commit homeproxy
|
||||
fi
|
||||
|
||||
rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>"/dev/null"
|
||||
|
||||
mkdir -p /var/run/homeproxy
|
||||
touch /var/run/homeproxy/homeproxy.log \
|
||||
/var/run/homeproxy/sing-box-c.log \
|
||||
/var/run/homeproxy/sing-box-s.log
|
||||
chown -R sing-box:sing-box /var/run/homeproxy 2>"/dev/null"
|
||||
|
||||
uci -q batch <<-EOF >"/dev/null"
|
||||
delete firewall.homeproxy_pre
|
||||
|
||||
|
||||
Executable → Regular
File mode changed.
@@ -9,7 +9,6 @@
|
||||
"/var/run/homeproxy/sing-box-s.log": [ "read" ]
|
||||
},
|
||||
"ubus": {
|
||||
"rc": [ "*" ],
|
||||
"service": [ "list" ],
|
||||
"luci.homeproxy": [ "*" ]
|
||||
},
|
||||
@@ -17,11 +16,7 @@
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/tmp/homeproxy_certificate.tmp": [ "write" ],
|
||||
"/tmp/homeproxy_panel.tmp": [ "write" ]
|
||||
},
|
||||
"ubus": {
|
||||
"rc": [ "*" ]
|
||||
"/tmp/homeproxy_certificate.tmp": [ "write" ]
|
||||
},
|
||||
"uci": [ "homeproxy" ]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,60 @@
|
||||
# luci-app-homeproxy tests
|
||||
|
||||
Automated checks for the pieces that `sing-box check` cannot validate: the
|
||||
share-link parsers, the UCI→JSON generators and the LuCI form definitions.
|
||||
|
||||
## Running
|
||||
|
||||
```sh
|
||||
tests/run.sh
|
||||
```
|
||||
|
||||
* The **LuCI form snapshots** run locally and only need `node`. They re-render
|
||||
the node/server views through a mock LuCI runtime and diff the result against
|
||||
`tests/snapshots/*.json`.
|
||||
* The **ucode tests** need `ucode`; the generator cases additionally run
|
||||
`sing-box check`, so they need sing-box ≥ 1.14 as well (available on an
|
||||
OpenWrt/ImmortalWrt target). When the host running `tests/run.sh` has neither,
|
||||
the checkout is copied over ssh to `$HP_TEST_HOST` (default
|
||||
`root@192.168.1.1`) and the tests run there:
|
||||
|
||||
```sh
|
||||
HP_TEST_HOST=root@192.168.1.1 tests/run.sh
|
||||
HP_TEST_DIR=/tmp/hp-tests tests/run.sh
|
||||
```
|
||||
|
||||
`tests/run.sh` exits non-zero if anything fails. The ucode part can also be run
|
||||
on its own on a target:
|
||||
|
||||
```sh
|
||||
sh tests/ucode/run.sh "$PWD"
|
||||
```
|
||||
|
||||
## What is covered
|
||||
|
||||
| Path | Checks |
|
||||
| --- | --- |
|
||||
| `tests/i18n-coverage.py` | Reports how many `po/templates/homeproxy.pot` strings have a non-fuzzy, non-empty `po/zh_Hans/homeproxy.po` translation. Technical tokens that stay as-is are listed in `tests/i18n-ignore.txt`; anything else missing produces a warning annotation (`--warn-below 100` by default) and a job-summary entry. Run by `.github/workflows/i18n.yml` on push/PR and before a release. |
|
||||
| `tests/luci-form-snapshot.js` | Dumps every option (name, kind, title, description, depends, values, datatype/default/…) of the node and server views. Diffs against `tests/snapshots/{node,server}.json`, so a refactor that changes a field or its visibility fails. |
|
||||
| `tests/ucode/test_homeproxy_utils.uc` | `executeCommand()` return shape, stderr/exit-code capture, binary detection, and a descriptor-leak check (200 calls). |
|
||||
| `tests/ucode/test_homeproxy_utils_inject.uc` | The failure path of `executeCommand()`: the script stages a copy of `homeproxy.uc` whose `system()` call is replaced by `die()`, then checks that the exception still propagates and that neither descriptor leaks. |
|
||||
| `tests/ucode/test_fw4_names.sh` | Keeps the fw4 chain/set inventory in `scripts/fw4_names.sh` (used by `init.d/homeproxy` to clean up on stop) in sync with the objects declared in `scripts/firewall_post.ut`. |
|
||||
| `tests/ucode/test_parse_uri.uc` | Per-scheme assertions on the `parse_uri()` result: anytls, http/https, hysteria, hysteria2/hy2, snell, socks(4/4a/5/5h), shadowsocks (SIP002 base64 + plain + plugin, Shadowrocket), trojan (ws/grpc), tuic, vless (reality/ws/http/httpupgrade), vmess (ws/h2/httpupgrade/grpc) and SIP008 objects, plus the rejection paths (unsupported kcp/quic, no QUIC support, invalid port, unknown scheme). |
|
||||
| `tests/ucode/test_generators.sh` | Runs `generate_client.uc`/`generate_server.uc` against the UCI fixtures in `tests/fixtures/generators/` inside a scratch directory (the `uci` cursor and `HP_DIR`/`RUN_DIR` are redirected) and validates each result with `sing-box check`. |
|
||||
|
||||
`tests/ucode/mocks/homeproxy.uc` is a test double for the real module: only
|
||||
`validation()` is stubbed (the real one runs `/sbin/validate_data`, which does
|
||||
not exist outside OpenWrt), everything else is a copy of the production code.
|
||||
|
||||
## Adding cases
|
||||
|
||||
* New share link → add an `expect_fields(...)` case to `test_parse_uri.uc`.
|
||||
* New protocol option that reaches the generator → extend the fixtures in
|
||||
`tests/fixtures/generators/`, keeping values schema-valid so `sing-box check`
|
||||
still passes.
|
||||
* Intentional LuCI form change → regenerate the snapshots:
|
||||
|
||||
```sh
|
||||
node tests/luci-form-snapshot.js . node > tests/snapshots/node.json
|
||||
node tests/luci-form-snapshot.js . server > tests/snapshots/server.json
|
||||
```
|
||||
@@ -0,0 +1,170 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Regression fixture for generate_client.uc: a node of every supported
|
||||
# protocol reachable through a single urltest main node, so one run exercises
|
||||
# all outbound builders (TLS, transport, reality, mux, obfuscation).
|
||||
|
||||
config homeproxy 'config'
|
||||
option routing_mode 'bypass_mainland_china'
|
||||
option proxy_mode 'tun'
|
||||
option ipv6_support '0'
|
||||
option log_level 'error'
|
||||
option main_node 'urltest'
|
||||
option main_udp_node 'nil'
|
||||
option dns_server 'https://cloudflare-dns.com/dns-query'
|
||||
list main_urltest_nodes 'n_anytls'
|
||||
list main_urltest_nodes 'n_vless_reality_ws'
|
||||
list main_urltest_nodes 'n_vmess_http'
|
||||
list main_urltest_nodes 'n_trojan_grpc'
|
||||
list main_urltest_nodes 'n_ss'
|
||||
list main_urltest_nodes 'n_hy2'
|
||||
list main_urltest_nodes 'n_snell'
|
||||
list main_urltest_nodes 'n_http'
|
||||
list main_urltest_nodes 'n_socks'
|
||||
list main_urltest_nodes 'n_shadowtls'
|
||||
|
||||
config homeproxy 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
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 self_mark '100'
|
||||
option tun_mark '102'
|
||||
option table_mark '100'
|
||||
option ntp_server 'nil'
|
||||
|
||||
config node 'n_anytls'
|
||||
option label 'anytls'
|
||||
option type 'anytls'
|
||||
option address 'a.example.com'
|
||||
option port '443'
|
||||
option password 'secret'
|
||||
option tls '1'
|
||||
option tls_sni 'a.example.com'
|
||||
option tls_insecure '0'
|
||||
option anytls_idle_session_check_interval '30'
|
||||
option anytls_idle_session_timeout '30'
|
||||
option anytls_min_idle_session '0'
|
||||
|
||||
config node 'n_vless_reality_ws'
|
||||
option label 'vless-reality-ws'
|
||||
option type 'vless'
|
||||
option address 'b.example.com'
|
||||
option port '443'
|
||||
option uuid '3af88561-9c69-4b19-8f7e-f08d580bc339'
|
||||
option transport 'ws'
|
||||
option ws_host 'b.example.com'
|
||||
option ws_path '/ws?ed=2048'
|
||||
option tls '1'
|
||||
option tls_sni 'b.example.com'
|
||||
option tls_reality '1'
|
||||
option tls_reality_public_key '4UAg690QziXeelpUJoAmXiim0gfQESSiJkNfvF-o8Uw'
|
||||
option tls_reality_short_id 'abcd'
|
||||
option tls_utls 'chrome'
|
||||
option vless_flow 'xtls-rprx-vision'
|
||||
option packet_encoding 'xudp'
|
||||
|
||||
config node 'n_vmess_http'
|
||||
option label 'vmess-http'
|
||||
option type 'vmess'
|
||||
option address 'c.example.com'
|
||||
option port '80'
|
||||
option uuid '3af88561-9c69-4b19-8f7e-f08d580bc339'
|
||||
option transport 'http'
|
||||
option http_host 'c.example.com'
|
||||
option http_path '/path'
|
||||
option http_method 'GET'
|
||||
option vmess_alterid '0'
|
||||
option vmess_encrypt 'auto'
|
||||
option vmess_global_padding '1'
|
||||
option tls '0'
|
||||
option packet_encoding 'packetaddr'
|
||||
|
||||
config node 'n_trojan_grpc'
|
||||
option label 'trojan-grpc'
|
||||
option type 'trojan'
|
||||
option address 'd.example.com'
|
||||
option port '443'
|
||||
option password 'secret'
|
||||
option transport 'grpc'
|
||||
option grpc_servicename 'gs'
|
||||
option grpc_permit_without_stream '1'
|
||||
option tls '1'
|
||||
option tls_sni 'd.example.com'
|
||||
option tls_handshake_timeout '15'
|
||||
option tls_min_version '1.2'
|
||||
option tls_max_version '1.3'
|
||||
option tls_cipher_suites 'TLS_AES_128_GCM_SHA256'
|
||||
|
||||
config node 'n_ss'
|
||||
option label 'ss'
|
||||
option type 'shadowsocks'
|
||||
option address 'e.example.com'
|
||||
option port '8388'
|
||||
option shadowsocks_encrypt_method 'aes-256-gcm'
|
||||
option password 'secret'
|
||||
option multiplex '1'
|
||||
option multiplex_protocol 'h2mux'
|
||||
option multiplex_padding '1'
|
||||
option multiplex_max_connections '4'
|
||||
option udp_over_tcp '1'
|
||||
option udp_over_tcp_version '2'
|
||||
|
||||
config node 'n_hy2'
|
||||
option label 'hy2'
|
||||
option type 'hysteria2'
|
||||
option address 'f.example.com'
|
||||
option port '443'
|
||||
option password 'secret'
|
||||
option tls '1'
|
||||
option tls_sni 'f.example.com'
|
||||
option hysteria_obfs_type 'salamander'
|
||||
option hysteria_obfs_password 'obfspass'
|
||||
option hysteria_up_mbps '100'
|
||||
option hysteria_down_mbps '500'
|
||||
option hysteria_hopping_port '20000:30000'
|
||||
option hysteria_hop_interval '30'
|
||||
option hysteria_bbr_profile 'conservative'
|
||||
|
||||
config node 'n_snell'
|
||||
option label 'snell'
|
||||
option type 'snell'
|
||||
option address 'g.example.com'
|
||||
option port '443'
|
||||
option password 'psk123456789012'
|
||||
option snell_version '4'
|
||||
option snell_obfs_mode 'http'
|
||||
option snell_obfs_host 'bing.com'
|
||||
option snell_reuse '1'
|
||||
option snell_userkey 'ukey'
|
||||
|
||||
config node 'n_http'
|
||||
option label 'http'
|
||||
option type 'http'
|
||||
option address 'h.example.com'
|
||||
option port '8080'
|
||||
option username 'u'
|
||||
option password 'p'
|
||||
option tls '0'
|
||||
|
||||
config node 'n_socks'
|
||||
option label 'socks'
|
||||
option type 'socks'
|
||||
option address 'i.example.com'
|
||||
option port '1080'
|
||||
option username 'u'
|
||||
option password 'p'
|
||||
option socks_version '5'
|
||||
|
||||
config node 'n_shadowtls'
|
||||
option label 'shadowtls'
|
||||
option type 'shadowtls'
|
||||
option address 'j.example.com'
|
||||
option port '443'
|
||||
option password 'secret'
|
||||
option shadowtls_version '3'
|
||||
option tls '1'
|
||||
option tls_sni 'j.example.com'
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Regression fixture for routing_mode='custom'. It reproduces two crashes
|
||||
# that only the custom path could hit:
|
||||
#
|
||||
# * a direct node with an override forces generate_outbound() to write into
|
||||
# direct_overrides, and the routing rule below forces get_direct_override()
|
||||
# to read it -- so the table has to be declared before both helpers;
|
||||
# * a local rule-set must not carry the legacy download_detour field, which
|
||||
# sing-box 1.14 rejects on anything but a remote rule-set.
|
||||
#
|
||||
# __RULESET_DIR__ is substituted by tests/ucode/test_generators.sh with a
|
||||
# directory holding a real compiled .srs file.
|
||||
|
||||
config homeproxy 'config'
|
||||
option routing_mode 'custom'
|
||||
option proxy_mode 'redirect_tproxy'
|
||||
option ipv6_support '0'
|
||||
option log_level 'error'
|
||||
|
||||
config homeproxy 'routing'
|
||||
option default_outbound 'direct-out'
|
||||
option default_outbound_dns 'default-dns'
|
||||
|
||||
config homeproxy 'infra'
|
||||
option mixed_port '5330'
|
||||
option dns_port '5333'
|
||||
option redirect_port '5331'
|
||||
option self_mark '100'
|
||||
|
||||
# direct node: exercises the direct_overrides write path
|
||||
config node 'n_direct'
|
||||
option label 'direct-node'
|
||||
option type 'direct'
|
||||
option override_address '1.2.3.4'
|
||||
option override_port '8443'
|
||||
|
||||
# routing node + rule: exercises get_direct_override() reading direct_overrides
|
||||
config routing_node 'rn_direct'
|
||||
option enabled '1'
|
||||
option label 'rn-direct'
|
||||
option node 'n_direct'
|
||||
|
||||
config routing_rule 'rr_direct'
|
||||
option enabled '1'
|
||||
option label 'rr-direct'
|
||||
option outbound 'rn_direct'
|
||||
option action 'route'
|
||||
|
||||
# local rule-set: must never carry download_detour
|
||||
config ruleset 'rs_local'
|
||||
option enabled '1'
|
||||
option label 'local-rs'
|
||||
option type 'local'
|
||||
option format 'binary'
|
||||
option path '__RULESET_DIR__/test.srs'
|
||||
@@ -0,0 +1,112 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Regression fixture for generate_server.uc: one inbound per protocol family,
|
||||
# covering the shared TLS/transport builders (ACME provider, ECH key, REALITY,
|
||||
# WebSocket/gRPC/HTTP transports) on the server side.
|
||||
|
||||
config homeproxy 'server'
|
||||
option enabled '1'
|
||||
option log_level 'warn'
|
||||
|
||||
config server 's_vless_ws'
|
||||
option enabled '1'
|
||||
option type 'vless'
|
||||
option listen_port '8443'
|
||||
option uuid '3af88561-9c69-4b19-8f7e-f08d580bc339'
|
||||
option transport 'ws'
|
||||
option ws_host 'a.example.com'
|
||||
option ws_path '/ws'
|
||||
option websocket_early_data '2048'
|
||||
option tls '1'
|
||||
option tls_sni 'a.example.com'
|
||||
option tls_acme '1'
|
||||
option tls_acme_domain 'a.example.com'
|
||||
option tls_acme_provider 'letsencrypt'
|
||||
option tls_acme_email 'admin@example.com'
|
||||
option tls_acme_dsn 'a.example.com'
|
||||
option tls_acme_key_type 'p256'
|
||||
option tls_alpn 'h2'
|
||||
option tls_min_version '1.2'
|
||||
|
||||
config server 's_anytls'
|
||||
option enabled '1'
|
||||
option type 'anytls'
|
||||
option listen_port '8444'
|
||||
option password 'secret'
|
||||
option anytls_padding_scheme 'stop=8'
|
||||
option tls '1'
|
||||
option tls_sni 'b.example.com'
|
||||
option tls_acme '1'
|
||||
option tls_acme_domain 'b.example.com'
|
||||
option tls_acme_provider 'zerossl'
|
||||
option tls_acme_email 'admin@example.com'
|
||||
option tls_acme_dsn 'b.example.com'
|
||||
|
||||
config server 's_hy2'
|
||||
option enabled '1'
|
||||
option type 'hysteria2'
|
||||
option listen_port '8445'
|
||||
option password 'secret'
|
||||
option hysteria_up_mbps '100'
|
||||
option hysteria_down_mbps '500'
|
||||
option hysteria_obfs_type 'salamander'
|
||||
option hysteria_obfs_password 'obfspass'
|
||||
option hysteria_masquerade 'https://example.com'
|
||||
option tls '1'
|
||||
option tls_sni 'c.example.com'
|
||||
option tls_acme '1'
|
||||
option tls_acme_domain 'c.example.com'
|
||||
option tls_acme_provider 'letsencrypt'
|
||||
option tls_acme_email 'admin@example.com'
|
||||
option tls_acme_dsn 'c.example.com'
|
||||
|
||||
config server 's_trojan_grpc'
|
||||
option enabled '1'
|
||||
option type 'trojan'
|
||||
option listen_port '8446'
|
||||
option password 'secret'
|
||||
option transport 'grpc'
|
||||
option grpc_servicename 'gs'
|
||||
option multiplex '1'
|
||||
option multiplex_padding '1'
|
||||
option tls '1'
|
||||
option tls_sni 'd.example.com'
|
||||
option tls_acme '1'
|
||||
option tls_acme_domain 'd.example.com'
|
||||
option tls_acme_provider 'letsencrypt'
|
||||
option tls_acme_email 'admin@example.com'
|
||||
option tls_acme_dsn 'd.example.com'
|
||||
|
||||
config server 's_ss'
|
||||
option enabled '1'
|
||||
option type 'shadowsocks'
|
||||
option listen_port '8447'
|
||||
option network 'tcp'
|
||||
option shadowsocks_encrypt_method 'aes-256-gcm'
|
||||
option password 'secret'
|
||||
option udp_timeout '300'
|
||||
|
||||
config server 's_snell'
|
||||
option enabled '1'
|
||||
option type 'snell'
|
||||
option listen_port '8448'
|
||||
option password 'psk123456789012'
|
||||
option snell_version '5'
|
||||
option snell_obfs_mode 'http'
|
||||
|
||||
config server 's_tuic'
|
||||
option enabled '1'
|
||||
option type 'tuic'
|
||||
option listen_port '8449'
|
||||
option uuid '3af88561-9c69-4b19-8f7e-f08d580bc339'
|
||||
option password 'secret'
|
||||
option tuic_congestion_control 'bbr'
|
||||
option tuic_auth_timeout '3'
|
||||
option tuic_heartbeat '10'
|
||||
option tls '1'
|
||||
option tls_sni 'e.example.com'
|
||||
option tls_acme '1'
|
||||
option tls_acme_domain 'e.example.com'
|
||||
option tls_acme_provider 'letsencrypt'
|
||||
option tls_acme_email 'admin@example.com'
|
||||
option tls_acme_dsn 'e.example.com'
|
||||
Executable
+152
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Copyright (C) 2025 ImmortalWrt.org
|
||||
#
|
||||
# Report how much of the gettext template (po/templates/homeproxy.pot) is
|
||||
# translated in po/zh_Hans/homeproxy.po. Missing, empty and fuzzy entries all
|
||||
# count as untranslated. The check only warns by default so it does not block
|
||||
# a build; pass --fail-below to turn the warning into a failure.
|
||||
#
|
||||
# Usage:
|
||||
# tests/i18n-coverage.py # warn below 95%
|
||||
# tests/i18n-coverage.py --warn-below 90
|
||||
# tests/i18n-coverage.py --fail-below 95
|
||||
# tests/i18n-coverage.py --list 20
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
TEMPLATE = os.path.join(ROOT, "po/templates/homeproxy.pot")
|
||||
TRANSLATION = os.path.join(ROOT, "po/zh_Hans/homeproxy.po")
|
||||
IGNORE = os.path.join(ROOT, "tests/i18n-ignore.txt")
|
||||
|
||||
|
||||
def load_ignore(path):
|
||||
"""msgids that are deliberately not translated (technical tokens)."""
|
||||
if not os.path.exists(path):
|
||||
return set()
|
||||
ignored = set()
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for raw in handle:
|
||||
line = raw.strip()
|
||||
if line and not line.startswith("#"):
|
||||
ignored.add(line)
|
||||
return ignored
|
||||
|
||||
|
||||
def unquote(value):
|
||||
value = value.strip()
|
||||
if len(value) >= 2 and value[0] == '"' and value[-1] == '"':
|
||||
return value[1:-1]
|
||||
return value
|
||||
|
||||
|
||||
def parse_po(path):
|
||||
"""Return {msgid: {'msgstr': str, 'fuzzy': bool}} for non-empty msgids."""
|
||||
entries = {}
|
||||
msgid = None
|
||||
msgstr = ""
|
||||
state = None
|
||||
pending_fuzzy = False
|
||||
entry_fuzzy = False
|
||||
|
||||
def flush():
|
||||
if msgid:
|
||||
entries[msgid] = {"msgstr": msgstr, "fuzzy": entry_fuzzy}
|
||||
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
for raw in handle:
|
||||
line = raw.rstrip("\n")
|
||||
|
||||
if line.startswith("#,"):
|
||||
if "fuzzy" in line:
|
||||
pending_fuzzy = True
|
||||
continue
|
||||
|
||||
if line.startswith("#"):
|
||||
continue
|
||||
|
||||
if line.startswith("msgid "):
|
||||
flush()
|
||||
msgid = unquote(line[len("msgid "):])
|
||||
msgstr = ""
|
||||
entry_fuzzy = pending_fuzzy
|
||||
pending_fuzzy = False
|
||||
state = "msgid"
|
||||
elif line.startswith("msgstr "):
|
||||
msgstr = unquote(line[len("msgstr "):])
|
||||
state = "msgstr"
|
||||
elif line.startswith('"'):
|
||||
if state == "msgid":
|
||||
msgid += unquote(line)
|
||||
elif state == "msgstr":
|
||||
msgstr += unquote(line)
|
||||
|
||||
flush()
|
||||
return entries
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--warn-below", type=float, default=100.0,
|
||||
help="warn when coverage is below this percentage (default: 100)")
|
||||
parser.add_argument("--fail-below", type=float, default=None,
|
||||
help="exit non-zero when coverage is below this percentage")
|
||||
parser.add_argument("--list", type=int, default=10,
|
||||
help="list at most this many untranslated entries (default: 10)")
|
||||
args = parser.parse_args()
|
||||
|
||||
template = parse_po(TEMPLATE)
|
||||
translation = parse_po(TRANSLATION)
|
||||
ignored = load_ignore(IGNORE)
|
||||
|
||||
total = 0
|
||||
untranslated = []
|
||||
for msgid in template:
|
||||
if msgid in ignored:
|
||||
continue
|
||||
total += 1
|
||||
entry = translation.get(msgid)
|
||||
if not entry or entry["fuzzy"] or not entry["msgstr"].strip():
|
||||
untranslated.append(msgid)
|
||||
|
||||
translated = total - len(untranslated)
|
||||
coverage = (translated / total * 100) if total else 100.0
|
||||
|
||||
print(f"zh_Hans translation coverage: {translated}/{total} ({coverage:.1f}%)"
|
||||
f", {len(ignored)} msgid(s) ignored by tests/i18n-ignore.txt")
|
||||
|
||||
if untranslated and args.list:
|
||||
print(f"{len(untranslated)} untranslated entries, first {min(args.list, len(untranslated))}:")
|
||||
for msgid in untranslated[:args.list]:
|
||||
print(f" - {msgid}")
|
||||
|
||||
summary = os.environ.get("GITHUB_STEP_SUMMARY")
|
||||
if summary:
|
||||
with open(summary, "a", encoding="utf-8") as handle:
|
||||
handle.write("## zh_Hans translation coverage\n\n")
|
||||
handle.write(f"**{translated}/{total} ({coverage:.1f}%)** — "
|
||||
f"{len(untranslated)} untranslated entry/entries.\n\n")
|
||||
if untranslated:
|
||||
handle.write("<details><summary>Untranslated</summary>\n\n")
|
||||
for msgid in untranslated:
|
||||
handle.write(f"- `{msgid}`\n")
|
||||
handle.write("\n</details>\n")
|
||||
|
||||
if args.fail_below is not None and coverage < args.fail_below:
|
||||
print(f"::error title=Translation coverage::zh_Hans coverage {coverage:.1f}% "
|
||||
f"is below {args.fail_below}%")
|
||||
return 1
|
||||
|
||||
if coverage < args.warn_below:
|
||||
print(f"::warning title=Translation coverage::zh_Hans coverage {coverage:.1f}% "
|
||||
f"is below {args.warn_below}% ({len(untranslated)} untranslated entry/entries)")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,13 @@
|
||||
# msgids that are intentionally left untranslated (protocol/technical tokens
|
||||
# whose zh_Hans form is the same as the source). One msgid per line, matching
|
||||
# the unquoted template string; '#' starts a comment.
|
||||
http
|
||||
unsafe-raw
|
||||
unshaped
|
||||
v4
|
||||
v5
|
||||
v6
|
||||
wrong-ack
|
||||
wrong-checksum
|
||||
wrong-md5
|
||||
wrong-timestamp
|
||||
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*
|
||||
* Dump the LuCI form definition of the homeproxy node/client/server views as
|
||||
* JSON, using a minimal mock of the LuCI client runtime. It is meant to be run
|
||||
* against two checkouts (e.g. HEAD and the working tree) and the resulting
|
||||
* snapshots diffed, so that a pure refactor can be proven not to change any
|
||||
* option name, dependency, default or label.
|
||||
*
|
||||
* Usage: node tests/luci-form-snapshot.js <repo-root> [node|server]
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/* --- LuCI runtime mock start ------------------------------------------- */
|
||||
|
||||
String.prototype.format = function (...args) {
|
||||
const values = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
||||
let i = 0;
|
||||
return this.replace(/%[sdj%]/g, (m) => m === '%%' ? '%' : (i < values.length ? String(values[i++]) : m));
|
||||
};
|
||||
|
||||
function E(tag, attrs, children) {
|
||||
return { tag: tag, attrs: attrs, children: children };
|
||||
}
|
||||
|
||||
function _(text) { return text; }
|
||||
|
||||
const L = {
|
||||
bind: (fn, self, ...args) => fn.bind(self, ...args),
|
||||
resolveDefault: (promise, fallback) => promise.catch(() => fallback),
|
||||
ui: { hideModal: () => {}, addNotification: () => {}, changes: { apply: () => {} } },
|
||||
env: { paths: {} }
|
||||
};
|
||||
|
||||
let sectionSeq = 0;
|
||||
|
||||
class Option {
|
||||
constructor(kind, name, title, description) {
|
||||
this.__kind = kind;
|
||||
this.__name = name;
|
||||
this.__title = title;
|
||||
this.__description = description;
|
||||
this.__depends = [];
|
||||
this.__values = [];
|
||||
}
|
||||
|
||||
depends(...args) { this.__depends.push(args.length === 1 ? args[0] : args); }
|
||||
value(value, label) { this.__values.push([ value, label === undefined ? null : label ]); }
|
||||
|
||||
toJSON() {
|
||||
const props = {};
|
||||
for (const key of Object.keys(this).sort()) {
|
||||
if (key.startsWith('__') || key === 'subsection')
|
||||
continue;
|
||||
const value = this[key];
|
||||
if (typeof value === 'function' || value === undefined)
|
||||
continue;
|
||||
props[key] = value;
|
||||
}
|
||||
return {
|
||||
kind: this.__kind,
|
||||
name: this.__name,
|
||||
title: this.__title,
|
||||
description: this.__description === undefined ? null : this.__description,
|
||||
depends: this.__depends,
|
||||
values: this.__values,
|
||||
props: props
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Section {
|
||||
constructor(kind, name) {
|
||||
this.__kind = kind;
|
||||
this.__name = name;
|
||||
this.__id = `${kind}:${name || ''}#${sectionSeq++}`;
|
||||
this.__options = [];
|
||||
this.__tabs = [];
|
||||
if (kind === 'SectionValue')
|
||||
this.subsection = new Section('GridSection', name);
|
||||
}
|
||||
|
||||
option(kind, name, title, description) {
|
||||
const kindName = kind && kind.__name__ ? kind.__name__ : String(kind);
|
||||
const option = new Option(kindName, name, title, description);
|
||||
if (kindName === 'SectionValue')
|
||||
option.subsection = new Section('GridSection', name);
|
||||
this.__options.push(option);
|
||||
return option;
|
||||
}
|
||||
|
||||
tab(id, title) { this.__tabs.push([ id, title ]); }
|
||||
taboption(_tab, ...args) { return this.option(...args); }
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
id: this.__id,
|
||||
kind: this.__kind,
|
||||
name: this.__name,
|
||||
tabs: this.__tabs,
|
||||
options: this.__options.map((o) => o.toJSON()),
|
||||
subsection: this.subsection ? this.subsection.toJSON() : null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const classes = [ 'Value', 'ListValue', 'Flag', 'DynamicList', 'MultiValue', 'TextValue', 'Button', 'TypedSection',
|
||||
'NamedSection', 'GridSection', 'SectionValue' ];
|
||||
|
||||
const form = {};
|
||||
for (const name of classes) {
|
||||
form[name] = class {};
|
||||
form[name].__name__ = name;
|
||||
}
|
||||
|
||||
form.DynamicList.extend = function (obj) {
|
||||
const cls = class extends form.DynamicList {};
|
||||
Object.assign(cls.prototype, obj);
|
||||
cls.__name__ = obj.__name__ || 'DynamicList';
|
||||
return cls;
|
||||
};
|
||||
|
||||
form.Value.extend = function (obj) {
|
||||
const cls = class extends form.Value {};
|
||||
Object.assign(cls.prototype, obj);
|
||||
cls.__name__ = obj.__name__ || 'Value';
|
||||
return cls;
|
||||
};
|
||||
|
||||
form.Map = class {
|
||||
constructor(title) { this.title = title; this.sections = []; }
|
||||
section(kind, name) {
|
||||
const section = new Section(kind && kind.__name__ ? kind.__name__ : String(kind), name);
|
||||
this.sections.push(section);
|
||||
return section;
|
||||
}
|
||||
render() { return { sections: this.sections.map((s) => s.toJSON()) }; }
|
||||
};
|
||||
|
||||
const uci = {
|
||||
load: () => Promise.resolve(),
|
||||
get: () => undefined,
|
||||
get_first: () => null,
|
||||
get_all: () => ({}),
|
||||
sections: () => {},
|
||||
foreach: () => {},
|
||||
set: () => {}, add: () => 'section', remove: () => {}, save: () => Promise.resolve()
|
||||
};
|
||||
|
||||
const ui = {
|
||||
addNotification: () => {},
|
||||
showModal: () => {},
|
||||
hideModal: () => {},
|
||||
uploadFile: () => Promise.resolve({}),
|
||||
addValidator: () => {},
|
||||
Textarea: class { render() { return E('textarea'); } getValue() { return ''; } },
|
||||
createHandlerFn: (self, fn) => fn
|
||||
};
|
||||
|
||||
const baseclass = { extend: (obj) => obj };
|
||||
const view = { extend: (obj) => obj };
|
||||
const poll = { add: () => {} };
|
||||
const rpc = { declare: () => () => Promise.resolve({}) };
|
||||
const fsMock = { exec_direct: () => Promise.resolve(''), read: () => Promise.resolve(''), write: () => Promise.resolve() };
|
||||
const widgets = { DeviceSelect: class {} };
|
||||
|
||||
const deps = {
|
||||
baseclass, form, fs: fsMock, rpc, uci, ui, view, poll,
|
||||
'luci.http': { urldecode: (s) => s, urlencode: (s) => s, urldecode_params: () => ({}) },
|
||||
'luci.sys': { init_action: () => {} },
|
||||
'tools.widgets': widgets
|
||||
};
|
||||
|
||||
/* --- LuCI runtime mock end --------------------------------------------- */
|
||||
|
||||
function loadLuciModule(file, extraDeps) {
|
||||
let src = fs.readFileSync(file, 'utf8');
|
||||
src = src.replace(/^'require ([^']+) as (\w+)';$/gm,
|
||||
(_m, mod, alias) => `const ${alias} = __deps[${JSON.stringify(mod)}];`);
|
||||
src = src.replace(/^'require ([^']+)';$/gm,
|
||||
(_m, mod) => `const ${mod.split('.').pop().replace(/[^\w]/g, '_')} = __deps[${JSON.stringify(mod)}];`);
|
||||
/* eslint-disable-next-line no-new-func */
|
||||
const factory = new Function('__deps', '_', 'E', 'L', src);
|
||||
return factory(Object.assign({}, deps, extraDeps), _, E, L);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const root = process.argv[2];
|
||||
const target = process.argv[3] || 'node';
|
||||
if (!root)
|
||||
throw new Error('usage: luci-form-snapshot.js <repo-root> [node|server]');
|
||||
|
||||
const viewDir = path.join(root, 'htdocs/luci-static/resources');
|
||||
const homeproxy = loadLuciModule(path.join(viewDir, 'homeproxy.js'), {});
|
||||
const mod = loadLuciModule(path.join(viewDir, 'view/homeproxy', target + '.js'), { homeproxy });
|
||||
|
||||
const features = {
|
||||
version: '1.14.0',
|
||||
with_acme: true, with_grpc: true, with_gvisor: true, with_quic: true,
|
||||
with_utls: true, with_wireguard: true, hp_has_tcp_brutal: true
|
||||
};
|
||||
|
||||
const rendered = mod.render([ undefined, features ]);
|
||||
process.stdout.write(JSON.stringify(rendered, null, '\t') + '\n');
|
||||
}
|
||||
|
||||
main();
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# One-shot entry point for the test suite:
|
||||
#
|
||||
# tests/run.sh
|
||||
#
|
||||
# Translation coverage and the LuCI form snapshots run locally (they only need
|
||||
# python3 / node). The ucode tests run locally when ucode and sing-box are
|
||||
# available, otherwise the checkout is copied to $HP_TEST_HOST and run there.
|
||||
#
|
||||
# HP_TEST_HOST=root@192.168.1.1 tests/run.sh
|
||||
# HP_TEST_DIR=/tmp/hp-tests tests/run.sh
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
HOST="${HP_TEST_HOST:-root@192.168.1.1}"
|
||||
REMOTE_DIR="${HP_TEST_DIR:-/tmp/hp-tests}"
|
||||
FAILED=0
|
||||
|
||||
echo "== zh_Hans translation coverage =="
|
||||
python3 "$ROOT/tests/i18n-coverage.py" --warn-below 100
|
||||
|
||||
echo "== LuCI form snapshots =="
|
||||
for target in node server; do
|
||||
if ! node "$ROOT/tests/luci-form-snapshot.js" "$ROOT" "$target" > "/tmp/hp-snapshot-$target.json" 2> "/tmp/hp-snapshot-$target.err"; then
|
||||
echo "FAIL: could not render the $target form"
|
||||
cat "/tmp/hp-snapshot-$target.err"
|
||||
FAILED=1
|
||||
continue
|
||||
fi
|
||||
|
||||
if diff -q "$ROOT/tests/snapshots/$target.json" "/tmp/hp-snapshot-$target.json" > "/dev/null"; then
|
||||
echo "PASS: $target form snapshot"
|
||||
else
|
||||
echo "FAIL: $target form snapshot changed:"
|
||||
diff "$ROOT/tests/snapshots/$target.json" "/tmp/hp-snapshot-$target.json" | head -40
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "== ucode tests =="
|
||||
if command -v ucode > "/dev/null" 2>&1 && command -v sing-box > "/dev/null" 2>&1; then
|
||||
sh "$ROOT/tests/ucode/run.sh" "$ROOT" || FAILED=1
|
||||
else
|
||||
echo "(no local ucode/sing-box, executing on $HOST)"
|
||||
if tar czf - -C "$ROOT" --exclude .git --exclude node_modules . \
|
||||
| ssh "$HOST" "rm -rf $REMOTE_DIR && mkdir -p $REMOTE_DIR && tar xzf - -C $REMOTE_DIR"; then
|
||||
ssh "$HOST" "sh $REMOTE_DIR/tests/ucode/run.sh $REMOTE_DIR" || FAILED=1
|
||||
else
|
||||
echo "FAIL: could not stage the tests on $HOST"
|
||||
FAILED=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$FAILED" -eq 0 ]; then
|
||||
echo "ALL TESTS PASSED"
|
||||
else
|
||||
echo "SOME TESTS FAILED"
|
||||
fi
|
||||
|
||||
exit $FAILED
|
||||
@@ -0,0 +1,293 @@
|
||||
{
|
||||
"sections": [
|
||||
{
|
||||
"id": "NamedSection:subscription#0",
|
||||
"kind": "NamedSection",
|
||||
"name": "subscription",
|
||||
"tabs": [
|
||||
[
|
||||
"node",
|
||||
"Nodes"
|
||||
],
|
||||
[
|
||||
"subscription",
|
||||
"Subscriptions"
|
||||
]
|
||||
],
|
||||
"options": [
|
||||
{
|
||||
"kind": "SectionValue",
|
||||
"name": "_node",
|
||||
"description": "node",
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"kind": "Flag",
|
||||
"name": "auto_update",
|
||||
"title": "Auto update",
|
||||
"description": "Auto update subscriptions and geodata.",
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "ListValue",
|
||||
"name": "auto_update_time",
|
||||
"title": "Update time",
|
||||
"description": null,
|
||||
"depends": [
|
||||
[
|
||||
"auto_update",
|
||||
"1"
|
||||
]
|
||||
],
|
||||
"values": [
|
||||
[
|
||||
0,
|
||||
"0:00"
|
||||
],
|
||||
[
|
||||
1,
|
||||
"1:00"
|
||||
],
|
||||
[
|
||||
2,
|
||||
"2:00"
|
||||
],
|
||||
[
|
||||
3,
|
||||
"3:00"
|
||||
],
|
||||
[
|
||||
4,
|
||||
"4:00"
|
||||
],
|
||||
[
|
||||
5,
|
||||
"5:00"
|
||||
],
|
||||
[
|
||||
6,
|
||||
"6:00"
|
||||
],
|
||||
[
|
||||
7,
|
||||
"7:00"
|
||||
],
|
||||
[
|
||||
8,
|
||||
"8:00"
|
||||
],
|
||||
[
|
||||
9,
|
||||
"9:00"
|
||||
],
|
||||
[
|
||||
10,
|
||||
"10:00"
|
||||
],
|
||||
[
|
||||
11,
|
||||
"11:00"
|
||||
],
|
||||
[
|
||||
12,
|
||||
"12:00"
|
||||
],
|
||||
[
|
||||
13,
|
||||
"13:00"
|
||||
],
|
||||
[
|
||||
14,
|
||||
"14:00"
|
||||
],
|
||||
[
|
||||
15,
|
||||
"15:00"
|
||||
],
|
||||
[
|
||||
16,
|
||||
"16:00"
|
||||
],
|
||||
[
|
||||
17,
|
||||
"17:00"
|
||||
],
|
||||
[
|
||||
18,
|
||||
"18:00"
|
||||
],
|
||||
[
|
||||
19,
|
||||
"19:00"
|
||||
],
|
||||
[
|
||||
20,
|
||||
"20:00"
|
||||
],
|
||||
[
|
||||
21,
|
||||
"21:00"
|
||||
],
|
||||
[
|
||||
22,
|
||||
"22:00"
|
||||
],
|
||||
[
|
||||
23,
|
||||
"23:00"
|
||||
]
|
||||
],
|
||||
"props": {
|
||||
"default": "2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Flag",
|
||||
"name": "update_via_proxy",
|
||||
"title": "Update via proxy",
|
||||
"description": "Update subscriptions via proxy.",
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "DynamicList",
|
||||
"name": "subscription_url",
|
||||
"title": "Subscription URL-s",
|
||||
"description": "Support Hysteria, Shadowsocks, Trojan, v2rayN (VMess), and XTLS (VLESS) online configuration delivery standard.",
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"kind": "ListValue",
|
||||
"name": "filter_nodes",
|
||||
"title": "Filter nodes",
|
||||
"description": "Drop/keep specific nodes from subscriptions.",
|
||||
"depends": [],
|
||||
"values": [
|
||||
[
|
||||
"disabled",
|
||||
"Disable"
|
||||
],
|
||||
[
|
||||
"blacklist",
|
||||
"Blacklist mode"
|
||||
],
|
||||
[
|
||||
"whitelist",
|
||||
"Whitelist mode"
|
||||
]
|
||||
],
|
||||
"props": {
|
||||
"default": "disabled",
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "DynamicList",
|
||||
"name": "filter_keywords",
|
||||
"title": "Filter keywords",
|
||||
"description": "Drop/keep nodes that contain the specific keywords. <a target=\"_blank\" href=\"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions\">Regex</a> is supported.",
|
||||
"depends": [
|
||||
{
|
||||
"filter_nodes": "disabled",
|
||||
"!reverse": true
|
||||
}
|
||||
],
|
||||
"values": [],
|
||||
"props": {
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Value",
|
||||
"name": "user_agent",
|
||||
"title": "User-Agent",
|
||||
"description": null,
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {
|
||||
"placeholder": "Wget/1.21 (HomeProxy, like v2rayN)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Flag",
|
||||
"name": "allow_insecure",
|
||||
"title": "Allow insecure",
|
||||
"description": "Allow insecure connection by default when add nodes from subscriptions.<br/>This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!",
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {
|
||||
"rmempty": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "ListValue",
|
||||
"name": "packet_encoding",
|
||||
"title": "Default packet encoding",
|
||||
"description": null,
|
||||
"depends": [],
|
||||
"values": [
|
||||
[
|
||||
"",
|
||||
"none"
|
||||
],
|
||||
[
|
||||
"packetaddr",
|
||||
"packet addr (v2ray-core v5+)"
|
||||
],
|
||||
[
|
||||
"xudp",
|
||||
"Xudp (Xray-core)"
|
||||
]
|
||||
],
|
||||
"props": {}
|
||||
},
|
||||
{
|
||||
"kind": "Button",
|
||||
"name": "_save_subscriptions",
|
||||
"title": "Save subscriptions settings",
|
||||
"description": "NOTE: Save current settings before updating subscriptions.",
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {
|
||||
"inputstyle": "apply",
|
||||
"inputtitle": "Save current settings"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Button",
|
||||
"name": "_update_subscriptions",
|
||||
"title": "Update nodes from subscriptions",
|
||||
"description": null,
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {
|
||||
"inputstyle": "apply"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "Button",
|
||||
"name": "_remove_subscriptions",
|
||||
"title": "Remove all nodes from subscriptions",
|
||||
"description": null,
|
||||
"depends": [],
|
||||
"values": [],
|
||||
"props": {
|
||||
"inputstyle": "reset"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subsection": null
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*
|
||||
* Test double for root/etc/homeproxy/scripts/homeproxy.uc.
|
||||
*
|
||||
* Only `validation()` is stubbed: the real one shells out to
|
||||
* /sbin/validate_data, which does not exist outside OpenWrt. Everything else
|
||||
* (isEmpty, decodeBase64Str, parseURL) is a verbatim copy so the parser tests
|
||||
* exercise the same string handling as production. Keep these in sync when
|
||||
* homeproxy.uc changes.
|
||||
*/
|
||||
|
||||
import { urldecode_params } from 'luci.http';
|
||||
|
||||
export function isEmpty(res) {
|
||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||
};
|
||||
|
||||
export function decodeBase64Str(str) {
|
||||
if (isEmpty(str))
|
||||
return null;
|
||||
|
||||
str = trim(str);
|
||||
str = replace(str, /_/g, '/');
|
||||
str = replace(str, /-/g, '+');
|
||||
|
||||
const padding = length(str) % 4;
|
||||
if (padding)
|
||||
str = str + substr('====', padding);
|
||||
|
||||
return b64dec(str);
|
||||
};
|
||||
|
||||
/* Pure-ucode stand-in for /sbin/validate_data (host/port/ip4/ip6/hostname). */
|
||||
export function validation(datatype, data) {
|
||||
if (!datatype || !data)
|
||||
return null;
|
||||
|
||||
switch (datatype) {
|
||||
case 'port':
|
||||
return match(data, /^\d+$/) != null && int(data) >= 0 && int(data) <= 65535;
|
||||
case 'ip4addr':
|
||||
if (match(data, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/) == null)
|
||||
return false;
|
||||
for (let octet in split(data, '.'))
|
||||
if (int(octet) > 255)
|
||||
return false;
|
||||
return true;
|
||||
case 'ip6addr':
|
||||
return match(data, /^[0-9a-fA-F:]+$/) != null && match(data, /::.*::/) == null;
|
||||
case 'hostname':
|
||||
return match(data, /^[A-Za-z0-9_][A-Za-z0-9_%-.]*[A-Za-z0-9]$/) != null ||
|
||||
match(data, /^[A-Za-z0-9_]$/) != null;
|
||||
case 'host':
|
||||
return validation('ip4addr', data) === true ||
|
||||
validation('ip6addr', data) === true ||
|
||||
validation('hostname', data) === true;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Run the ucode-level tests: the parse_uri unit tests, the fw4 inventory check
|
||||
# and the generator regression fixtures. Requires ucode; sing-box is needed for
|
||||
# the generator cases (they validate the emitted config with `sing-box check`).
|
||||
#
|
||||
# Usage: sh tests/ucode/run.sh <repo-root> [work-dir]
|
||||
|
||||
ROOT="${1:-.}"
|
||||
WORK="${2:-/tmp/hp-ucode-tests}"
|
||||
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
FAILED=0
|
||||
|
||||
echo "== ucode syntax check =="
|
||||
for file in "$ROOT"/root/etc/homeproxy/scripts/*.uc "$ROOT"/root/usr/share/rpcd/ucode/*; do
|
||||
[ -f "$file" ] || continue
|
||||
# Modules (with export statements) cannot be compiled as a program; they
|
||||
# are loaded through `import` below instead.
|
||||
case "$file" in
|
||||
*homeproxy.uc|*parse_uri.uc) continue ;;
|
||||
esac
|
||||
if ! ucode -c -o "/dev/null" "$file" 2> "/tmp/hp-ucode-syntax.err"; then
|
||||
echo "FAIL: $file"
|
||||
head -8 "/tmp/hp-ucode-syntax.err"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
for module in homeproxy parse_uri; do
|
||||
if ! ucode -L "$ROOT/root/etc/homeproxy/scripts" -e "import * as m from \"$module\";" 2> "/tmp/hp-ucode-syntax.err"; then
|
||||
echo "FAIL: module $module"
|
||||
head -8 "/tmp/hp-ucode-syntax.err"
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
[ "$FAILED" -eq 0 ] && echo "PASS: all ucode sources compile"
|
||||
|
||||
echo "== fw4 chain/set inventory =="
|
||||
sh "$ROOT/tests/ucode/test_fw4_names.sh" "$ROOT" || FAILED=1
|
||||
|
||||
echo "== firewall template rendering =="
|
||||
sh "$ROOT/tests/ucode/test_firewall_template.sh" "$ROOT" || FAILED=1
|
||||
|
||||
echo "== parse_uri unit tests =="
|
||||
rm -rf "$WORK/parse_uri"
|
||||
mkdir -p "$WORK/parse_uri"
|
||||
cp "$ROOT/root/etc/homeproxy/scripts/parse_uri.uc" "$WORK/parse_uri/"
|
||||
cp "$ROOT/tests/ucode/mocks/homeproxy.uc" "$WORK/parse_uri/"
|
||||
cp "$ROOT/tests/ucode/test_parse_uri.uc" "$WORK/parse_uri/"
|
||||
|
||||
if ( cd "$WORK/parse_uri" && ucode test_parse_uri.uc ); then
|
||||
echo "PASS: parse_uri unit tests"
|
||||
else
|
||||
echo "FAIL: parse_uri unit tests"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
echo "== homeproxy helper tests =="
|
||||
rm -rf "$WORK/homeproxy"
|
||||
mkdir -p "$WORK/homeproxy"
|
||||
cp "$ROOT/root/etc/homeproxy/scripts/homeproxy.uc" "$WORK/homeproxy/"
|
||||
cp "$ROOT/tests/ucode/test_homeproxy_utils.uc" "$WORK/homeproxy/"
|
||||
if ( cd "$WORK/homeproxy" && ucode test_homeproxy_utils.uc ); then
|
||||
echo "PASS: executeCommand() regression tests"
|
||||
else
|
||||
echo "FAIL: executeCommand() regression tests"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
echo "== executeCommand() failure-path test =="
|
||||
rm -rf "$WORK/homeproxy_inject"
|
||||
mkdir -p "$WORK/homeproxy_inject"
|
||||
sed 's|const exitcode = system(.*);|die("injected failure");|' \
|
||||
"$ROOT/root/etc/homeproxy/scripts/homeproxy.uc" > "$WORK/homeproxy_inject/homeproxy.uc"
|
||||
cp "$ROOT/tests/ucode/test_homeproxy_utils_inject.uc" "$WORK/homeproxy_inject/"
|
||||
if ( cd "$WORK/homeproxy_inject" && ucode test_homeproxy_utils_inject.uc 2>"/dev/null" ); then
|
||||
echo "PASS: executeCommand() failure path"
|
||||
else
|
||||
echo "FAIL: executeCommand() failure path"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
echo "== generator regression tests =="
|
||||
sh "$ROOT/tests/ucode/test_generators.sh" "$ROOT" "$WORK/generators" || FAILED=1
|
||||
|
||||
exit $FAILED
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Render firewall_post.ut and verify the homeproxy fw4 objects survive as real
|
||||
# nft statements.
|
||||
#
|
||||
# The template opens with `{%-`, which trims the whitespace in front of it: any
|
||||
# text placed above that tag loses its trailing newline and is glued onto the
|
||||
# first generated statement, turning it into a comment. That silently dropped
|
||||
# every homeproxy chain/set from the fw4 ruleset once, so keep this check.
|
||||
#
|
||||
# Usage: sh tests/ucode/test_firewall_template.sh <repo-root>
|
||||
|
||||
ROOT="${1:-.}"
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
OUT="$(mktemp)"
|
||||
FAILED=0
|
||||
|
||||
if ! utpl -S "$ROOT/root/etc/homeproxy/scripts/firewall_post.ut" > "$OUT" 2>"/tmp/hp-fw4tpl.err"; then
|
||||
echo "FAIL: could not render firewall_post.ut"
|
||||
head -5 "/tmp/hp-fw4tpl.err"
|
||||
rm -f "$OUT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Declared unconditionally by the template, so they must always be present as
|
||||
# standalone statements.
|
||||
for name in homeproxy_local_addr_v4 homeproxy_wan_proxy_addr_v4; do
|
||||
if ! grep -q "^set $name {" "$OUT"; then
|
||||
echo "FAIL: 'set $name {' is missing from the rendered firewall template"
|
||||
grep -n "$name" "$OUT" | head -2
|
||||
FAILED=1
|
||||
fi
|
||||
done
|
||||
|
||||
# A statement glued onto the preceding comment looks like
|
||||
# "# <comment text>.set homeproxy_x {" -- exactly the failure mode above.
|
||||
if grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" > "/dev/null"; then
|
||||
echo "FAIL: an nft statement is glued onto a comment line"
|
||||
grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" | head -2
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
[ "$FAILED" -eq 0 ] && echo "PASS: firewall_post.ut renders homeproxy objects as standalone nft statements"
|
||||
|
||||
rm -f "$OUT"
|
||||
exit $FAILED
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# The fw4 chain/set inventory in scripts/fw4_names.sh must stay in sync with
|
||||
# the objects declared by scripts/firewall_post.ut: the first drives the
|
||||
# cleanup in init.d/homeproxy, the second creates them. A drift means either a
|
||||
# leftover object or a delete of something that never existed (which used to
|
||||
# abort the whole cleanup batch).
|
||||
#
|
||||
# Usage: sh tests/ucode/test_fw4_names.sh <repo-root>
|
||||
|
||||
ROOT="${1:-.}"
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
|
||||
NAMES="$ROOT/root/etc/homeproxy/scripts/fw4_names.sh"
|
||||
POST="$ROOT/root/etc/homeproxy/scripts/firewall_post.ut"
|
||||
PRE="$ROOT/root/etc/homeproxy/scripts/firewall_pre.uc"
|
||||
|
||||
FAILED=0
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
. "$NAMES"
|
||||
|
||||
check_group() {
|
||||
group="$1"
|
||||
list="$(printf '%s\n' $2 | tr '\n' ' ')"
|
||||
declared="$(printf '%s\n' $3 | tr '\n' ' ')"
|
||||
missing=""
|
||||
extra=""
|
||||
|
||||
for name in $declared; do
|
||||
case " $list " in
|
||||
*" $name "*) ;;
|
||||
*) missing="$missing $name" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
for name in $list; do
|
||||
case " $declared " in
|
||||
*" $name "*) ;;
|
||||
*) extra="$extra $name" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$missing$extra" ]; then
|
||||
echo "PASS: $group inventory matches firewall_post.ut"
|
||||
else
|
||||
[ -n "$missing" ] && echo "FAIL: $group declared in firewall_post.ut but missing from fw4_names.sh:$missing"
|
||||
[ -n "$extra" ] && echo "FAIL: $group listed in fw4_names.sh but not declared in firewall_post.ut:$extra"
|
||||
FAILED=1
|
||||
fi
|
||||
}
|
||||
|
||||
post_chains="$(grep -oE '^chain homeproxy_[a-z0-9_]+' "$POST" | awk '{print $2}' | sort -u)"
|
||||
post_sets="$(grep -oE '^set homeproxy_[a-z0-9_]+' "$POST" | awk '{print $2}' | sort -u)"
|
||||
|
||||
check_group "chain" "$HP_FW4_CHAINS" "$post_chains"
|
||||
check_group "set" "$HP_FW4_SETS" "$post_sets"
|
||||
|
||||
# firewall_pre.uc only writes forward/input accept rules; it must not own any
|
||||
# chain/set of its own.
|
||||
pre_refs="$(grep -oE 'homeproxy_[a-z0-9_]+' "$PRE" | sort -u)"
|
||||
if [ -n "$pre_refs" ]; then
|
||||
echo "NOTE: firewall_pre.uc references fw4 objects:$pre_refs"
|
||||
fi
|
||||
|
||||
exit $FAILED
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# Run generate_client.uc / generate_server.uc against the UCI fixtures and let
|
||||
# sing-box validate the result. The generators read /etc/config and write
|
||||
# /var/run, so this script materialises an isolated copy of them: homeproxy.uc
|
||||
# is rewritten to point HP_DIR/RUN_DIR at a scratch directory, and the uci
|
||||
# cursor is pointed at the fixture config.
|
||||
#
|
||||
# Usage: sh tests/ucode/test_generators.sh <repo-root> [work-dir]
|
||||
|
||||
ROOT="${1:-.}"
|
||||
WORK="${2:-/tmp/hp-generator-test}"
|
||||
|
||||
ROOT="$(cd "$ROOT" && pwd)"
|
||||
FAILED=0
|
||||
|
||||
run_case() {
|
||||
name="$1"
|
||||
fixture="$2"
|
||||
generator="$3"
|
||||
outfile="$4"
|
||||
dir="$WORK/$name"
|
||||
|
||||
rm -rf "$dir"
|
||||
mkdir -p "$dir/config" "$dir/run" "$dir/scripts" "$dir/resources" "$dir/ruleset"
|
||||
: > "$dir/resources/direct_list.txt"
|
||||
: > "$dir/resources/proxy_list.txt"
|
||||
|
||||
if grep -q "__RULESET_DIR__" "$fixture"; then
|
||||
# The fixture needs a real local rule-set on disk.
|
||||
printf '%s' '{"version":1,"rules":[{"domain_suffix":["example.com"]}]}' > "$dir/ruleset/src.json"
|
||||
if ! sing-box rule-set compile "$dir/ruleset/src.json" -o "$dir/ruleset/test.srs"; then
|
||||
echo "FAIL: $name: could not compile the local rule-set fixture"
|
||||
FAILED=1
|
||||
return
|
||||
fi
|
||||
sed "s#__RULESET_DIR__#$dir/ruleset#" "$fixture" > "$dir/config/homeproxy"
|
||||
else
|
||||
cp "$fixture" "$dir/config/homeproxy"
|
||||
fi
|
||||
|
||||
sed -e "s#^export const HP_DIR = '/etc/homeproxy';#export const HP_DIR = '$dir';#" \
|
||||
-e "s#^export const RUN_DIR = '/var/run/homeproxy';#export const RUN_DIR = '$dir/run';#" \
|
||||
"$ROOT/root/etc/homeproxy/scripts/homeproxy.uc" > "$dir/scripts/homeproxy.uc"
|
||||
|
||||
sed -e "s#const uci = cursor();#const uci = cursor('$dir/config');#" \
|
||||
"$ROOT/root/etc/homeproxy/scripts/$generator" > "$dir/scripts/$generator"
|
||||
|
||||
if ! ( cd "$dir/scripts" && ucode "$generator" ); then
|
||||
echo "FAIL: $name: $generator exited non-zero"
|
||||
FAILED=1
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -f "$dir/run/$outfile" ]; then
|
||||
echo "FAIL: $name: $outfile was not generated"
|
||||
FAILED=1
|
||||
return
|
||||
fi
|
||||
|
||||
if ! sing-box check --config "$dir/run/$outfile"; then
|
||||
echo "FAIL: $name: sing-box rejected the generated $outfile"
|
||||
FAILED=1
|
||||
return
|
||||
fi
|
||||
|
||||
echo "PASS: $name ($(wc -c < "$dir/run/$outfile") bytes)"
|
||||
}
|
||||
|
||||
run_case client "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
|
||||
|
||||
# The preset remote rule-sets must be fetched through the node. A direct
|
||||
# download depends on the CDN staying reachable from mainland China and fails
|
||||
# intermittently under DNS pollution, which shows up as "open connection to
|
||||
# <ip>:443 using outbound/direct[direct]: i/o timeout" in sing-box-c.log.
|
||||
if grep -q '"detour": "direct-out"' "$WORK/client/run/sing-box-c.json"; then
|
||||
echo "FAIL: client: a remote rule-set would still be downloaded directly"
|
||||
FAILED=1
|
||||
fi
|
||||
if ! grep -q '"detour": "main-out"' "$WORK/client/run/sing-box-c.json"; then
|
||||
echo "FAIL: client: no remote rule-set is configured to download through main-out"
|
||||
FAILED=1
|
||||
fi
|
||||
|
||||
run_case custom "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json
|
||||
run_case server "$ROOT/tests/fixtures/generators/server.uci" generate_server.uc sing-box-s.json
|
||||
|
||||
exit $FAILED
|
||||
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*
|
||||
* Regression tests for homeproxy.uc helpers, focused on executeCommand():
|
||||
* return shape, stderr/exit-code capture, binary detection and (most
|
||||
* importantly) that the temporary descriptors are closed on every run.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { lsdir } from 'fs';
|
||||
import { executeCommand, isValidPEM, shellQuote } from 'homeproxy';
|
||||
|
||||
let failures = 0,
|
||||
checks = 0;
|
||||
|
||||
function expect(name, actual, want) {
|
||||
checks++;
|
||||
if (sprintf('%J', actual) !== sprintf('%J', want)) {
|
||||
printf('FAIL %s: expected %J, got %J\n', name, want, actual);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
function fd_count() {
|
||||
let n = 0;
|
||||
for (let _entry in lsdir('/proc/self/fd'))
|
||||
n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
/* successful command */
|
||||
const ok = executeCommand('echo', 'hello');
|
||||
expect('ok.exitcode', ok.exitcode, 0);
|
||||
expect('ok.stdout', ok.stdout, 'hello\n');
|
||||
expect('ok.stderr', ok.stderr, '');
|
||||
expect('ok.binary', ok.binary, false);
|
||||
expect('ok.command', ok.command, 'echo hello');
|
||||
|
||||
/* failing command keeps stderr and the exit code */
|
||||
const bad = executeCommand('sh', '-c', shellQuote('echo oops >&2; exit 3'));
|
||||
expect('bad.exitcode', bad.exitcode, 3);
|
||||
expect('bad.stderr', bad.stderr, 'oops\n');
|
||||
expect('bad.stdout', bad.stdout, '');
|
||||
|
||||
/* nondescript exit code */
|
||||
expect('false.exitcode', executeCommand('false').exitcode, 1);
|
||||
|
||||
/* binary output is detected and withheld */
|
||||
const bin = executeCommand('sh', '-c', shellQuote('printf "\\001\\002\\003"'));
|
||||
expect('bin.binary', bin.binary, true);
|
||||
expect('bin.stdout', bin.stdout, null);
|
||||
|
||||
/* isValidPEM(): certificate vs private key, boundaries and body */
|
||||
const pem_cert = '-----BEGIN CERTIFICATE-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END CERTIFICATE-----';
|
||||
const pem_key = '-----BEGIN RSA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END RSA PRIVATE KEY-----';
|
||||
expect('pem.cert', isValidPEM(pem_cert, false), true);
|
||||
expect('pem.cert-as-key', isValidPEM(pem_cert, true), false);
|
||||
expect('pem.key', isValidPEM(pem_key, true), true);
|
||||
expect('pem.key-as-cert', isValidPEM(pem_key, false), false);
|
||||
expect('pem.garbage', isValidPEM('not a pem at all', false), false);
|
||||
expect('pem.empty', isValidPEM('', false), false);
|
||||
|
||||
/* descriptors must not leak across calls */
|
||||
const before = fd_count();
|
||||
for (let i = 0; i < 200; i++)
|
||||
executeCommand('true');
|
||||
const after = fd_count();
|
||||
|
||||
checks++;
|
||||
if (after > before) {
|
||||
printf('FAIL descriptor leak: %d -> %d after 200 calls\n', before, after);
|
||||
failures++;
|
||||
}
|
||||
|
||||
printf('%d checks, %d failures\n', checks, failures);
|
||||
exit(failures === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*
|
||||
* Exceptional-path test for executeCommand(): the copy of homeproxy.uc staged
|
||||
* next to this file has its `system()` call replaced by `die()`, so every call
|
||||
* throws before the descriptors are read. ucode has no `finally`, so this
|
||||
* checks that the catch block still closes both descriptors.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { lsdir } from 'fs';
|
||||
import { executeCommand } from 'homeproxy';
|
||||
|
||||
let failures = 0;
|
||||
|
||||
function fd_count() {
|
||||
let n = 0;
|
||||
for (let _entry in lsdir('/proc/self/fd'))
|
||||
n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
const before = fd_count();
|
||||
let caught = 0;
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
try {
|
||||
executeCommand('true');
|
||||
} catch (e) {
|
||||
caught++;
|
||||
}
|
||||
}
|
||||
|
||||
const after = fd_count();
|
||||
|
||||
if (caught !== 50) {
|
||||
printf('FAIL: expected 50 propagated exceptions, got %d\n', caught);
|
||||
failures++;
|
||||
}
|
||||
|
||||
if (after > before) {
|
||||
printf('FAIL descriptor leak on the exceptional path: %d -> %d\n', before, after);
|
||||
failures++;
|
||||
}
|
||||
|
||||
if (failures === 0)
|
||||
printf('PASS: injected failure propagated and both descriptors were closed\n');
|
||||
|
||||
exit(failures === 0 ? 0 : 1);
|
||||
@@ -0,0 +1,170 @@
|
||||
#!/usr/bin/ucode
|
||||
/*
|
||||
* SPDX-License-Identifier: GPL-2.0-only
|
||||
*
|
||||
* Copyright (C) 2025 ImmortalWrt.org
|
||||
*
|
||||
* Unit tests for the share-link parsers in parse_uri.uc. Run through
|
||||
* tests/ucode/run.sh, which stages parse_uri.uc next to the mocked
|
||||
* homeproxy.uc.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { parse_uri } from 'parse_uri';
|
||||
|
||||
const FEATURES = { with_quic: true, with_utls: true };
|
||||
const NO_QUIC = { with_quic: false, with_utls: true };
|
||||
const LOG = (..._args) => {};
|
||||
|
||||
let failures = 0,
|
||||
checks = 0;
|
||||
|
||||
function expect_fields(name, cfg, expected) {
|
||||
if (type(cfg) !== 'object') {
|
||||
printf('FAIL %s: expected an object, got %J\n', name, cfg);
|
||||
failures++;
|
||||
return;
|
||||
}
|
||||
|
||||
for (let key in expected) {
|
||||
checks++;
|
||||
if (sprintf('%J', cfg[key]) !== sprintf('%J', expected[key])) {
|
||||
printf('FAIL %s: %s expected %J, got %J\n', name, key, expected[key], cfg[key]);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function expect_null(name, cfg) {
|
||||
checks++;
|
||||
if (cfg !== null) {
|
||||
printf('FAIL %s: expected null, got %J\n', name, cfg);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
|
||||
/* anytls */
|
||||
expect_fields('anytls', parse_uri('anytls://secret@a.example.com:443?sni=a.example.com&insecure=1#AnyTLS-Node', FEATURES, LOG), {
|
||||
label: 'AnyTLS-Node', type: 'anytls', address: 'a.example.com', port: '443',
|
||||
password: 'secret', tls: '1', tls_sni: 'a.example.com', tls_insecure: '1'
|
||||
});
|
||||
|
||||
/* http / https */
|
||||
expect_fields('https', parse_uri('https://user:pass@b.example.com:8443#HTTPS-Node', FEATURES, LOG), {
|
||||
label: 'HTTPS-Node', type: 'http', address: 'b.example.com', port: '8443',
|
||||
username: 'user', password: 'pass', tls: '1'
|
||||
});
|
||||
expect_fields('http', parse_uri('http://c.example.com:8080#HTTP-Node', FEATURES, LOG), {
|
||||
label: 'HTTP-Node', type: 'http', address: 'c.example.com', port: '8080', tls: '0'
|
||||
});
|
||||
|
||||
/* hysteria */
|
||||
expect_fields('hysteria', parse_uri('hysteria://d.example.com:36712?auth=pass&peer=d.example.com&insecure=1&upmbps=100&downmbps=200&alpn=h3&protocol=udp#Hy1', FEATURES, LOG), {
|
||||
label: 'Hy1', type: 'hysteria', address: 'd.example.com', port: '36712',
|
||||
hysteria_protocol: 'udp', hysteria_auth_type: 'string', hysteria_auth_payload: 'pass',
|
||||
hysteria_up_mbps: '100', hysteria_down_mbps: '200',
|
||||
tls: '1', tls_insecure: '1', tls_sni: 'd.example.com', tls_alpn: 'h3'
|
||||
});
|
||||
|
||||
/* hysteria2 */
|
||||
expect_fields('hysteria2', parse_uri('hysteria2://pass@e.example.com:443?sni=e.example.com&obfs=salamander&obfs-password=op#Hy2', FEATURES, LOG), {
|
||||
label: 'Hy2', type: 'hysteria2', address: 'e.example.com', port: '443',
|
||||
password: 'pass', hysteria_obfs_type: 'salamander', hysteria_obfs_password: 'op',
|
||||
tls: '1', tls_insecure: '0', tls_sni: 'e.example.com'
|
||||
});
|
||||
expect_fields('hy2-alias', parse_uri('hy2://pass@e.example.com:443#Hy2Alias', FEATURES, LOG), {
|
||||
label: 'Hy2Alias', type: 'hysteria2', address: 'e.example.com', port: '443'
|
||||
});
|
||||
|
||||
/* snell */
|
||||
expect_fields('snell', parse_uri('snell://f.example.com:443?psk=pskvalue&version=4&obfs=http&obfs-host=bing.com&reuse=1#Snell', FEATURES, LOG), {
|
||||
label: 'Snell', type: 'snell', address: 'f.example.com', port: '443',
|
||||
password: 'pskvalue', snell_version: '4', snell_obfs_mode: 'http',
|
||||
snell_obfs_host: 'bing.com', snell_reuse: '1'
|
||||
});
|
||||
|
||||
/* socks */
|
||||
expect_fields('socks5', parse_uri('socks5://user:pass@g.example.com:1080#Socks', FEATURES, LOG), {
|
||||
label: 'Socks', type: 'socks', address: 'g.example.com', port: '1080',
|
||||
username: 'user', password: 'pass', socks_version: '5'
|
||||
});
|
||||
expect_fields('socks4', parse_uri('socks4://h.example.com:1080#Socks4', FEATURES, LOG), {
|
||||
socks_version: '4'
|
||||
});
|
||||
|
||||
/* shadowsocks: SIP002 with base64 user info */
|
||||
expect_fields('ss-sip002-b64', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@i.example.com:8388#SS-Node', FEATURES, LOG), {
|
||||
label: 'SS-Node', type: 'shadowsocks', address: 'i.example.com', port: '8388',
|
||||
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
|
||||
});
|
||||
|
||||
/* shadowsocks: SIP002 with plugin */
|
||||
expect_fields('ss-sip002-plugin', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ=@j.example.com:8388?plugin=simple-obfs%3Bobfs%3Dhttp#SS-Plugin', FEATURES, LOG), {
|
||||
label: 'SS-Plugin', type: 'shadowsocks', address: 'j.example.com', port: '8388',
|
||||
shadowsocks_plugin: 'obfs-local', shadowsocks_plugin_opts: 'obfs=http'
|
||||
});
|
||||
|
||||
/* shadowsocks: Shadowrocket whole-payload base64 */
|
||||
expect_fields('ss-shadowrocket', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmRAay5leGFtcGxlLmNvbTo4Mzg4#SS-SR', FEATURES, LOG), {
|
||||
label: 'SS-SR', type: 'shadowsocks', address: 'k.example.com', port: '8388',
|
||||
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
|
||||
});
|
||||
|
||||
/* trojan */
|
||||
expect_fields('trojan-ws', parse_uri('trojan://pass@l.example.com:443?type=ws&host=l.example.com&path=%2Fpath%3Fed%3D2048&sni=l.example.com#TrojanWs', FEATURES, LOG), {
|
||||
label: 'TrojanWs', type: 'trojan', address: 'l.example.com', port: '443', password: 'pass',
|
||||
transport: 'ws', ws_host: 'l.example.com', ws_path: '/path',
|
||||
websocket_early_data: '2048', websocket_early_data_header: 'Sec-WebSocket-Protocol',
|
||||
tls: '1', tls_sni: 'l.example.com'
|
||||
});
|
||||
expect_fields('trojan-grpc', parse_uri('trojan://pass@l.example.com:443?type=grpc&serviceName=gs#TrojanGrpc', FEATURES, LOG), {
|
||||
transport: 'grpc', grpc_servicename: 'gs'
|
||||
});
|
||||
|
||||
/* tuic */
|
||||
expect_fields('tuic', parse_uri('tuic://tuic-uuid:pass@m.example.com:443?congestion_control=bbr&udp_relay_mode=native&alpn=h3&sni=m.example.com#Tuic', FEATURES, LOG), {
|
||||
label: 'Tuic', type: 'tuic', address: 'm.example.com', port: '443',
|
||||
uuid: 'tuic-uuid', password: 'pass', tuic_congestion_control: 'bbr',
|
||||
tuic_udp_relay_mode: 'native', tls: '1', tls_sni: 'm.example.com', tls_alpn: [ 'h3' ]
|
||||
});
|
||||
|
||||
/* vless */
|
||||
expect_fields('vless-reality-grpc', parse_uri('vless://vless-uuid@n.example.com:443?security=reality&pbk=PUBKEY&sid=abcd&fp=chrome&type=grpc&serviceName=gs&sni=n.example.com&flow=xtls-rprx-vision#Vless', FEATURES, LOG), {
|
||||
label: 'Vless', type: 'vless', address: 'n.example.com', port: '443', uuid: 'vless-uuid',
|
||||
transport: 'grpc', grpc_servicename: 'gs', tls: '1', tls_sni: 'n.example.com',
|
||||
tls_reality: '1', tls_reality_public_key: 'PUBKEY', tls_reality_short_id: 'abcd',
|
||||
tls_utls: 'chrome', vless_flow: 'xtls-rprx-vision'
|
||||
});
|
||||
expect_fields('vless-ws-early-data', parse_uri('vless://vless-uuid@o.example.com:443?security=tls&type=ws&host=o.example.com&path=%2Fws%3Fed%3D1024&sni=o.example.com&alpn=h2%2Chttp%2F1.1#VlessWs', FEATURES, LOG), {
|
||||
type: 'vless', transport: 'ws', ws_host: 'o.example.com', ws_path: '/ws',
|
||||
websocket_early_data: '1024', websocket_early_data_header: 'Sec-WebSocket-Protocol',
|
||||
tls: '1', tls_alpn: [ 'h2', 'http/1.1' ]
|
||||
});
|
||||
|
||||
/* vmess */
|
||||
expect_fields('vmess-ws', parse_uri('vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQiOiJwLmV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiM2FmODg1NjEtOWM2OS00YjE5LThmN2UtZjA4ZDU4MGJjMzM5IiwiYWlkIjoiMCIsIm5ldCI6IndzIiwidHlwZSI6Im5vbmUiLCJob3N0IjoicC5leGFtcGxlLmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJwLmV4YW1wbGUuY29tIn0=', FEATURES, LOG), {
|
||||
label: 'VMess', type: 'vmess', address: 'p.example.com', port: '443',
|
||||
uuid: '3af88561-9c69-4b19-8f7e-f08d580bc339', vmess_alterid: '0', vmess_encrypt: 'auto',
|
||||
vmess_global_padding: '1', transport: 'ws', ws_host: 'p.example.com', ws_path: '/ws',
|
||||
tls: '1', tls_sni: 'p.example.com'
|
||||
});
|
||||
|
||||
/* sip008 object */
|
||||
expect_fields('sip008', parse_uri({
|
||||
nodetype: 'sip008', remarks: 'SIP008', server: 'q.example.com', server_port: 8388,
|
||||
method: 'aes-256-gcm', password: 'pw'
|
||||
}, FEATURES, LOG), {
|
||||
label: 'SIP008', type: 'shadowsocks', address: 'q.example.com', port: 8388,
|
||||
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'pw'
|
||||
});
|
||||
|
||||
/* rejection paths */
|
||||
expect_null('vless-kcp-unsupported', parse_uri('vless://u@r.example.com:443?type=kcp#Kcp', FEATURES, LOG));
|
||||
expect_null('hysteria-without-quic', parse_uri('hysteria://s.example.com:443?auth=a#NoQuic', NO_QUIC, LOG));
|
||||
expect_null('tuic-without-quic', parse_uri('tuic://u:p@s.example.com:443#NoQuic', NO_QUIC, LOG));
|
||||
expect_null('invalid-port', parse_uri('vless://u@t.example.com:99999?security=tls#BadPort', FEATURES, LOG));
|
||||
expect_null('unknown-scheme', parse_uri('unknown://u@t.example.com:443', FEATURES, LOG));
|
||||
|
||||
printf('%d checks, %d failures\n', checks, failures);
|
||||
exit(failures === 0 ? 0 : 1);
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.9
|
||||
PKG_RELEASE:=273
|
||||
PKG_RELEASE:=274
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -93,15 +93,11 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
end
|
||||
local remarks = node.remarks
|
||||
|
||||
local proxy_tag = nil
|
||||
local fragment = nil
|
||||
local record_fragment = nil
|
||||
local run_socks_instance = true
|
||||
local proxy_tag, fragment, record_fragment
|
||||
if proxy_table ~= nil and type(proxy_table) == "table" then
|
||||
proxy_tag = proxy_table.tag or nil
|
||||
fragment = (proxy_table.fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil
|
||||
record_fragment = (proxy_table.record_fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil
|
||||
run_socks_instance = proxy_table.run_socks_instance
|
||||
end
|
||||
|
||||
if node.type ~= "sing-box" then
|
||||
@@ -110,7 +106,12 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
proxy_tag = "socks <- " .. node_id
|
||||
else
|
||||
local new_port
|
||||
if run_socks_instance then
|
||||
local run_socks_instance = true
|
||||
if NO_RUN then
|
||||
TMP_PORT = TMP_PORT and TMP_PORT + 1 or 3001
|
||||
new_port = TMP_PORT
|
||||
run_socks_instance = nil
|
||||
else
|
||||
local relay_port = (proxy_tag and node.port) and tostring(node.port) or ""
|
||||
if relay_port == "" then
|
||||
local cache = api.get_socks_port_by_cache(node_id)
|
||||
@@ -140,9 +141,6 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
api.set_socks_port_to_cache(node_id, new_port)
|
||||
end
|
||||
end
|
||||
else
|
||||
TMP_PORT = TMP_PORT and TMP_PORT + 1 or 3001 -- 导出配置时作为演示
|
||||
new_port = TMP_PORT
|
||||
end
|
||||
if new_port then
|
||||
node = {
|
||||
@@ -1163,10 +1161,11 @@ function gen_config(var)
|
||||
local dns_cache = var["dns_cache"]
|
||||
local dns_socks_address = var["dns_socks_address"]
|
||||
local dns_socks_port = var["dns_socks_port"]
|
||||
local no_run = var["no_run"]
|
||||
local use_proxy_list = var["use_proxy_list"]
|
||||
local use_gfw_list = var["use_gfw_list"]
|
||||
local chn_list = var["chn_list"]
|
||||
local run_in_global = var["run_in_global"]
|
||||
NO_RUN = var["no_run"]
|
||||
|
||||
local dns_domain_rules = {}
|
||||
local dns = nil
|
||||
@@ -1360,7 +1359,9 @@ function gen_config(var)
|
||||
ut_nodes = _node.urltest_node
|
||||
end
|
||||
|
||||
-- api.log(" - 加载 Sing-Box URLTest 节点【" .. (_node.remarks or "") .. "】,子节点数量:" .. #(ut_nodes or {}))
|
||||
if not NO_RUN and run_in_global then
|
||||
api.log(" - 加载 Sing-Box URLTest 节点【" .. (_node.remarks or "") .. "】,子节点数量:" .. #(ut_nodes or {}))
|
||||
end
|
||||
|
||||
local valid_nodes = {}
|
||||
for i = 1, #(ut_nodes or {}) do
|
||||
@@ -1375,7 +1376,7 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
if is_new_ut_node then
|
||||
local outboundTag = gen_outbound_get_tag(flag, ut_node_id, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
|
||||
local outboundTag = gen_outbound_get_tag(flag, ut_node_id, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil })
|
||||
if outboundTag then
|
||||
valid_nodes[#valid_nodes + 1] = outboundTag
|
||||
end
|
||||
@@ -1478,7 +1479,6 @@ function gen_config(var)
|
||||
to_node.port = new_port
|
||||
to_outbound = gen_outbound(node[".name"], to_node, tag, {
|
||||
tag = tag,
|
||||
run_socks_instance = not no_run
|
||||
})
|
||||
else
|
||||
to_outbound = gen_outbound(node[".name"], to_node)
|
||||
@@ -1571,7 +1571,6 @@ function gen_config(var)
|
||||
local proxy_table = {
|
||||
fragment = singbox_settings.fragment == "1",
|
||||
record_fragment = singbox_settings.record_fragment == "1",
|
||||
run_socks_instance = not no_run,
|
||||
}
|
||||
local preproxy_node_id = node[rule_name .. "_proxy_tag"]
|
||||
if preproxy_node_id == _node_id then preproxy_node_id = nil end
|
||||
@@ -1855,7 +1854,6 @@ function gen_config(var)
|
||||
COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node or node_id, nil, {
|
||||
fragment = singbox_settings.fragment == "1" or nil,
|
||||
record_fragment = singbox_settings.record_fragment == "1" or nil,
|
||||
run_socks_instance = not no_run
|
||||
})
|
||||
end
|
||||
|
||||
@@ -2316,7 +2314,7 @@ function gen_config(var)
|
||||
routing_mark = 255,
|
||||
})
|
||||
for index, value in ipairs(config.outbounds) do
|
||||
if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and (value.server_port or value.server_ports) and not no_run then
|
||||
if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and (value.server_port or value.server_ports) and not NO_RUN then
|
||||
sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list"))
|
||||
end
|
||||
if not value.detour and not value.bind_interface and value.server then
|
||||
@@ -2464,7 +2462,7 @@ if arg[1] then
|
||||
var = jsonc.parse(arg[2])
|
||||
end
|
||||
print(func(var))
|
||||
if (next(GEO_VAR.SITE_TAGS) or next(GEO_VAR.IP_TAGS)) and not no_run then
|
||||
if (next(GEO_VAR.SITE_TAGS) or next(GEO_VAR.IP_TAGS)) and not NO_RUN then
|
||||
convert_geofile()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,16 +43,11 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
end
|
||||
local remarks = node.remarks
|
||||
|
||||
local proxy_tag = nil
|
||||
local dialer_proxy_tag = nil
|
||||
local fragment = nil
|
||||
local noise = nil
|
||||
local run_socks_instance = true
|
||||
local proxy_tag, dialer_proxy_tag, fragment, noise
|
||||
if proxy_table ~= nil and type(proxy_table) == "table" then
|
||||
proxy_tag = proxy_table.tag or nil
|
||||
fragment = (proxy_table.fragment and not node.hysteria2_realms) and true or nil
|
||||
noise = (proxy_table.noise and not node.hysteria2_realms) and true or nil
|
||||
run_socks_instance = proxy_table.run_socks_instance
|
||||
end
|
||||
|
||||
if node.type ~= "Xray" then
|
||||
@@ -61,7 +56,12 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
node.transport = "raw"
|
||||
else
|
||||
local new_port
|
||||
if run_socks_instance then
|
||||
local run_socks_instance = true
|
||||
if NO_RUN then
|
||||
TMP_PORT = TMP_PORT and TMP_PORT + 1 or 3001
|
||||
new_port = TMP_PORT
|
||||
run_socks_instance = nil
|
||||
else
|
||||
local relay_port = (proxy_tag and node.port) and tostring(node.port) or ""
|
||||
if relay_port == "" then
|
||||
local cache = api.get_socks_port_by_cache(node_id)
|
||||
@@ -91,9 +91,6 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
api.set_socks_port_to_cache(node_id, new_port)
|
||||
end
|
||||
end
|
||||
else
|
||||
TMP_PORT = TMP_PORT and TMP_PORT + 1 or 3001 -- 导出配置时作为演示
|
||||
new_port = TMP_PORT
|
||||
end
|
||||
if new_port then
|
||||
node = {}
|
||||
@@ -913,10 +910,11 @@ function gen_config(var)
|
||||
local dns_socks_address = var["dns_socks_address"]
|
||||
local dns_socks_port = var["dns_socks_port"]
|
||||
local loglevel = var["loglevel"] or "warning"
|
||||
local no_run = var["no_run"]
|
||||
local use_proxy_list = var["use_proxy_list"]
|
||||
local use_gfw_list = var["use_gfw_list"]
|
||||
local chn_list = var["chn_list"]
|
||||
local run_in_global = var["run_in_global"]
|
||||
NO_RUN = var["no_run"]
|
||||
|
||||
local dns_domain_rules = {}
|
||||
local dns = nil
|
||||
@@ -1091,7 +1089,9 @@ function gen_config(var)
|
||||
blc_nodes = _node.balancing_node
|
||||
end
|
||||
|
||||
-- api.log(" - 加载 Xray 负载均衡 节点【" .. (_node.remarks or "") .. "】,子节点数量:" .. #(blc_nodes or {}))
|
||||
if not NO_RUN and run_in_global then
|
||||
api.log(" - 加载 Xray 负载均衡 节点【" .. (_node.remarks or "") .. "】,子节点数量:" .. #(blc_nodes or {}))
|
||||
end
|
||||
|
||||
local valid_nodes = {}
|
||||
for i = 1, #(blc_nodes or {}) do
|
||||
@@ -1106,7 +1106,7 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
if is_new_blc_node then
|
||||
local outboundTag = gen_outbound_get_tag(flag, blc_node_id, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
|
||||
local outboundTag = gen_outbound_get_tag(flag, blc_node_id, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil })
|
||||
if outboundTag then
|
||||
valid_nodes[#valid_nodes + 1] = outboundTag
|
||||
end
|
||||
@@ -1135,7 +1135,7 @@ function gen_config(var)
|
||||
local fallback_node = get_node_by_id(fallback_node_id)
|
||||
if fallback_node then
|
||||
if fallback_node.protocol ~= "_balancing" then
|
||||
local outboundTag = gen_outbound_get_tag(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
|
||||
local outboundTag = gen_outbound_get_tag(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil })
|
||||
if outboundTag then
|
||||
fallback_node_tag = outboundTag
|
||||
end
|
||||
@@ -1267,7 +1267,6 @@ function gen_config(var)
|
||||
})
|
||||
to_outbound = gen_outbound(node[".name"], to_node, to_node[".name"], {
|
||||
tag = to_node[".name"],
|
||||
run_socks_instance = not no_run
|
||||
})
|
||||
else
|
||||
to_outbound = gen_outbound(node[".name"], to_node)
|
||||
@@ -1375,7 +1374,6 @@ function gen_config(var)
|
||||
local proxy_table = {
|
||||
fragment = xray_settings.fragment == "1",
|
||||
noise = xray_settings.noise == "1",
|
||||
run_socks_instance = not no_run,
|
||||
}
|
||||
local preproxy_node_id = node[rule_name .. "_proxy_tag"]
|
||||
if preproxy_node_id == _node_id then preproxy_node_id = nil end
|
||||
@@ -1577,7 +1575,6 @@ function gen_config(var)
|
||||
COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node or node_id, nil, {
|
||||
fragment = xray_settings.fragment == "1" or nil,
|
||||
noise = xray_settings.noise == "1" or nil,
|
||||
run_socks_instance = not no_run
|
||||
})
|
||||
if COMMON.default_outbound_tag then
|
||||
routing = {
|
||||
@@ -2073,7 +2070,7 @@ function gen_config(var)
|
||||
for index, value in ipairs(config.outbounds) do
|
||||
local pt = value.protocol
|
||||
local exclude = { blackhole=1, dns=1, freedom=1, loopback=1 }
|
||||
if not value["_flag_proxy_tag"] and value["_id"] and pt and not exclude[pt] and not no_run then
|
||||
if not value["_flag_proxy_tag"] and value["_id"] and pt and not exclude[pt] and not NO_RUN then
|
||||
sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list"))
|
||||
end
|
||||
for k, v in pairs(config.outbounds[index]) do
|
||||
|
||||
@@ -87,7 +87,7 @@ run_ipt2socks() {
|
||||
run_singbox() {
|
||||
local flag type node redir_port tcp_proxy_way socks_address socks_port socks_username socks_password http_address http_port http_username http_password
|
||||
local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy remote_rewrite_ttl dns_cache dns_socks_address dns_socks_port
|
||||
local loglevel log_file config_file server_host server_port no_run use_proxy_list use_gfw_list chn_list
|
||||
local loglevel log_file config_file server_host server_port no_run use_proxy_list use_gfw_list chn_list run_in_global
|
||||
eval_set_val "$@"
|
||||
[ -z "$type" ] && {
|
||||
type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z')
|
||||
@@ -182,13 +182,17 @@ run_singbox() {
|
||||
[ "$remote_fakedns" = "1" ] && json_add_string "remote_dns_fake" "1"
|
||||
[ -n "$remote_rewrite_ttl" ] && json_add_string "remote_rewrite_ttl" "${remote_rewrite_ttl}"
|
||||
[ -n "$no_run" ] && json_add_string "no_run" "1"
|
||||
[ -n "$run_in_global" ] && json_add_string "run_in_global" "1"
|
||||
local _json_arg="$(json_dump)"
|
||||
lua $UTIL_SINGBOX gen_config "${_json_arg}" > $config_file
|
||||
[ -n "$no_run" ] && return
|
||||
|
||||
local test_log_file=$log_file
|
||||
local status=0
|
||||
[ "$test_log_file" = "/dev/null" ] && test_log_file="${TMP_PATH}/${config_file##*/}_test.log"
|
||||
$SINGBOX_BIN check -c "$config_file" > $test_log_file 2>&1; local status=$?
|
||||
[ -n "$run_in_global" ] && {
|
||||
$SINGBOX_BIN check -c "$config_file" > $test_log_file 2>&1; status=$?
|
||||
}
|
||||
if [ "${status}" = 0 ]; then
|
||||
ln_run "$SINGBOX_BIN" "sing-box" "${log_file}" run -c "$config_file"
|
||||
else
|
||||
@@ -201,7 +205,7 @@ run_singbox() {
|
||||
run_xray() {
|
||||
local flag type node redir_port tcp_proxy_way socks_address socks_port socks_username socks_password http_address http_port http_username http_password
|
||||
local dns_listen_port direct_dns_query_strategy direct_dns_port direct_dns_udp_server direct_dns_tcp_server remote_dns_protocol remote_dns_udp_server remote_dns_tcp_server remote_dns_doh remote_dns_client_ip remote_fakedns remote_dns_query_strategy dns_cache dns_socks_address dns_socks_port
|
||||
local loglevel log_file config_file server_host server_port no_run use_proxy_list use_gfw_list chn_list
|
||||
local loglevel log_file config_file server_host server_port no_run use_proxy_list use_gfw_list chn_list run_in_global
|
||||
eval_set_val "$@"
|
||||
[ -z "$type" ] && {
|
||||
type=$(echo $(config_n_get $node type) | tr 'A-Z' 'a-z')
|
||||
@@ -278,13 +282,17 @@ run_xray() {
|
||||
|
||||
json_add_string "loglevel" "$loglevel"
|
||||
[ -n "$no_run" ] && json_add_string "no_run" "1"
|
||||
[ -n "$run_in_global" ] && json_add_string "run_in_global" "1"
|
||||
local _json_arg="$(json_dump)"
|
||||
lua $UTIL_XRAY gen_config "${_json_arg}" > $config_file
|
||||
[ -n "$no_run" ] && return
|
||||
|
||||
local test_log_file=$log_file
|
||||
local status=0
|
||||
[ "$test_log_file" = "/dev/null" ] && test_log_file="${TMP_PATH}/${config_file##*/}_test.log"
|
||||
$XRAY_BIN run -test -c "$config_file" > $test_log_file; local status=$?
|
||||
[ -n "$run_in_global" ] && {
|
||||
$XRAY_BIN run -test -c "$config_file" > $test_log_file; status=$?
|
||||
}
|
||||
if [ "${status}" = 0 ]; then
|
||||
ln_run "$XRAY_BIN" "xray" "${log_file}" run -c "$config_file"
|
||||
else
|
||||
@@ -599,7 +607,7 @@ start_global() {
|
||||
;;
|
||||
sing-box)
|
||||
local _flag="global"
|
||||
local _args=""
|
||||
local _args="run_in_global=1"
|
||||
[ "$on_node_socks" = "1" ] && {
|
||||
node_socks_flag=1
|
||||
_args="${_args} socks_address=${node_socks_bind} socks_port=${GLOBAL_SOCKS_port}"
|
||||
@@ -677,7 +685,7 @@ start_global() {
|
||||
;;
|
||||
xray)
|
||||
local _flag="global"
|
||||
local _args=""
|
||||
local _args="run_in_global=1"
|
||||
[ "$on_node_socks" = "1" ] && {
|
||||
node_socks_flag=1
|
||||
_args="${_args} socks_address=${node_socks_bind} socks_port=${GLOBAL_SOCKS_port}"
|
||||
|
||||
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||
|
||||
PKG_NAME:=mise
|
||||
PKG_VERSION:=2026.9.8
|
||||
PKG_RELEASE:=14
|
||||
PKG_VERSION:=2026.9.9
|
||||
PKG_RELEASE:=15
|
||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=sing-box
|
||||
PKG_VERSION:=1.14.1
|
||||
PKG_RELEASE:=33
|
||||
PKG_RELEASE:=34
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/SagerNet/sing-box/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -1,403 +0,0 @@
|
||||
diff --git a/experimental/clashapi/provider.go b/experimental/clashapi/provider.go
|
||||
index 352b289..ef07e6d 100644
|
||||
--- a/experimental/clashapi/provider.go
|
||||
+++ b/experimental/clashapi/provider.go
|
||||
@@ -3,43 +3,65 @@ package clashapi
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
+ "os/exec"
|
||||
+ "regexp"
|
||||
+
|
||||
+ "github.com/sagernet/sing-box/option"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
-func proxyProviderRouter() http.Handler {
|
||||
+func proxyProviderRouter(server *Server) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
- r.Get("/", getProviders)
|
||||
+ r.Get("/", getProviders(server))
|
||||
|
||||
r.Route("/{name}", func(r chi.Router) {
|
||||
- r.Use(parseProviderName, findProviderByName)
|
||||
- r.Get("/", getProvider)
|
||||
+ r.Use(parseProviderName, findProviderByName(server))
|
||||
+ r.Get("/", getProvider(server))
|
||||
r.Put("/", updateProvider)
|
||||
r.Get("/healthcheck", healthCheckProvider)
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
-func getProviders(w http.ResponseWriter, r *http.Request) {
|
||||
- render.JSON(w, r, render.M{
|
||||
- "providers": render.M{},
|
||||
- })
|
||||
+func getProviders(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
+ return func(w http.ResponseWriter, r *http.Request) {
|
||||
+ providers := render.M{}
|
||||
+ for _, provider := range server.proxyProviders {
|
||||
+ if provider.Name == "" {
|
||||
+ continue
|
||||
+ }
|
||||
+ providers[provider.Name] = proxyProviderInfo(server, provider)
|
||||
+ }
|
||||
+ render.JSON(w, r, render.M{
|
||||
+ "providers": providers,
|
||||
+ })
|
||||
+ }
|
||||
}
|
||||
|
||||
-func getProvider(w http.ResponseWriter, r *http.Request) {
|
||||
- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
|
||||
- render.JSON(w, r, provider)*/
|
||||
- render.NoContent(w, r)
|
||||
+func getProvider(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
+ return func(w http.ResponseWriter, r *http.Request) {
|
||||
+ provider := r.Context().Value(CtxKeyProvider).(option.ClashProxyProviderOptions)
|
||||
+ render.JSON(w, r, proxyProviderInfo(server, provider))
|
||||
+ }
|
||||
}
|
||||
|
||||
func updateProvider(w http.ResponseWriter, r *http.Request) {
|
||||
- /*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
|
||||
- if err := provider.Update(); err != nil {
|
||||
+ provider := r.Context().Value(CtxKeyProvider).(option.ClashProxyProviderOptions)
|
||||
+ if !regexp.MustCompile(`^[0-9a-f]{32}$`).MatchString(provider.UpdateID) {
|
||||
+ render.Status(r, http.StatusServiceUnavailable)
|
||||
+ render.JSON(w, r, newError("Provider update is unavailable"))
|
||||
+ return
|
||||
+ }
|
||||
+
|
||||
+ command := exec.Command("/bin/sh", "/etc/homeproxy/scripts/update_provider.sh", provider.UpdateID)
|
||||
+ if err := command.Start(); err != nil {
|
||||
render.Status(r, http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, newError(err.Error()))
|
||||
return
|
||||
- }*/
|
||||
+ }
|
||||
+ _ = command.Process.Release()
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
@@ -57,18 +79,58 @@ func parseProviderName(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
-func findProviderByName(next http.Handler) http.Handler {
|
||||
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
- /*name := r.Context().Value(CtxKeyProviderName).(string)
|
||||
- providers := tunnel.ProxyProviders()
|
||||
- provider, exist := providers[name]
|
||||
- if !exist {*/
|
||||
- render.Status(r, http.StatusNotFound)
|
||||
- render.JSON(w, r, ErrNotFound)
|
||||
- //return
|
||||
- //}
|
||||
+func findProviderByName(server *Server) func(next http.Handler) http.Handler {
|
||||
+ return func(next http.Handler) http.Handler {
|
||||
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
+ name := r.Context().Value(CtxKeyProviderName).(string)
|
||||
+ for _, provider := range server.proxyProviders {
|
||||
+ if provider.Name == name {
|
||||
+ ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
|
||||
+ next.ServeHTTP(w, r.WithContext(ctx))
|
||||
+ return
|
||||
+ }
|
||||
+ }
|
||||
+ render.Status(r, http.StatusNotFound)
|
||||
+ render.JSON(w, r, ErrNotFound)
|
||||
+ })
|
||||
+ }
|
||||
+}
|
||||
|
||||
- // ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
|
||||
- // next.ServeHTTP(w, r.WithContext(ctx))
|
||||
- })
|
||||
+func proxyProviderInfo(server *Server, provider option.ClashProxyProviderOptions) render.M {
|
||||
+ providerType := provider.Type
|
||||
+ if providerType == "" {
|
||||
+ providerType = "Proxy"
|
||||
+ }
|
||||
+ vehicleType := provider.VehicleType
|
||||
+ if vehicleType == "" {
|
||||
+ vehicleType = "HTTP"
|
||||
+ }
|
||||
+
|
||||
+ proxies := make([]any, 0, len(provider.Proxies))
|
||||
+ for _, proxyName := range provider.Proxies {
|
||||
+ proxy, exists := lookupProxyByName(server, proxyName)
|
||||
+ if !exists {
|
||||
+ continue
|
||||
+ }
|
||||
+ proxies = append(proxies, proxyInfo(server, proxy))
|
||||
+ }
|
||||
+
|
||||
+ info := render.M{
|
||||
+ "name": provider.Name,
|
||||
+ "type": providerType,
|
||||
+ "vehicleType": vehicleType,
|
||||
+ "proxies": proxies,
|
||||
+ }
|
||||
+ if provider.UpdatedAt != "" {
|
||||
+ info["updatedAt"] = provider.UpdatedAt
|
||||
+ }
|
||||
+ if provider.SubscriptionInfo != nil {
|
||||
+ info["subscriptionInfo"] = render.M{
|
||||
+ "Upload": provider.SubscriptionInfo.Upload,
|
||||
+ "Download": provider.SubscriptionInfo.Download,
|
||||
+ "Total": provider.SubscriptionInfo.Total,
|
||||
+ "Expire": provider.SubscriptionInfo.Expire,
|
||||
+ }
|
||||
+ }
|
||||
+ return info
|
||||
}
|
||||
diff --git a/experimental/clashapi/proxies.go b/experimental/clashapi/proxies.go
|
||||
index ef88ff3..1f7d14d 100644
|
||||
--- a/experimental/clashapi/proxies.go
|
||||
+++ b/experimental/clashapi/proxies.go
|
||||
@@ -21,6 +21,50 @@ import (
|
||||
"github.com/go-chi/render"
|
||||
)
|
||||
|
||||
+func proxyDisplayName(tag string) string {
|
||||
+ switch tag {
|
||||
+ case "GLOBAL":
|
||||
+ return "GLOBAL"
|
||||
+ case "main-out":
|
||||
+ return "主代理"
|
||||
+ case "main-udp-out":
|
||||
+ return "UDP 主代理"
|
||||
+ case "direct-out":
|
||||
+ return "直连"
|
||||
+ case "block-out":
|
||||
+ return "拦截"
|
||||
+ default:
|
||||
+ return tag
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func lookupProxyByName(server *Server, name string) (adapter.Outbound, bool) {
|
||||
+ proxy, exist := server.outbound.Outbound(name)
|
||||
+ if exist {
|
||||
+ return proxy, true
|
||||
+ }
|
||||
+
|
||||
+ for _, detour := range server.outbound.Outbounds() {
|
||||
+ if proxyDisplayName(detour.Tag()) == name {
|
||||
+ return detour, true
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ for _, endpoint := range server.endpoint.Endpoints() {
|
||||
+ if proxyDisplayName(endpoint.Tag()) == name {
|
||||
+ return endpoint, true
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ return nil, false
|
||||
+}
|
||||
+
|
||||
+func proxyDisplayNames(tags []string) []string {
|
||||
+ return common.Map(tags, func(tag string) string {
|
||||
+ return proxyDisplayName(tag)
|
||||
+ })
|
||||
+}
|
||||
+
|
||||
func proxyRouter(server *Server, router adapter.Router) http.Handler {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", getProxies(server))
|
||||
@@ -29,7 +73,7 @@ func proxyRouter(server *Server, router adapter.Router) http.Handler {
|
||||
r.Use(parseProxyName, findProxyByName(server))
|
||||
r.Get("/", getProxy(server))
|
||||
r.Get("/delay", getProxyDelay(server))
|
||||
- r.Put("/", updateProxy)
|
||||
+ r.Put("/", updateProxy(server))
|
||||
})
|
||||
return r
|
||||
}
|
||||
@@ -46,7 +90,7 @@ func findProxyByName(server *Server) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.Context().Value(CtxKeyProxyName).(string)
|
||||
- proxy, exist := server.outbound.Outbound(name)
|
||||
+ proxy, exist := lookupProxyByName(server, name)
|
||||
if !exist {
|
||||
render.Status(r, http.StatusNotFound)
|
||||
render.JSON(w, r, ErrNotFound)
|
||||
@@ -68,7 +112,7 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
|
||||
clashType = C.ProxyDisplayName(detour.Type())
|
||||
}
|
||||
info.Put("type", clashType)
|
||||
- info.Put("name", detour.Tag())
|
||||
+ info.Put("name", proxyDisplayName(detour.Tag()))
|
||||
info.Put("udp", common.Contains(detour.Network(), N.NetworkUDP))
|
||||
delayHistory := server.urlTestHistory.LoadURLTestHistory(adapter.OutboundTag(detour))
|
||||
if delayHistory != nil {
|
||||
@@ -77,8 +121,8 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
|
||||
info.Put("history", []*adapter.URLTestHistory{})
|
||||
}
|
||||
if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
|
||||
- info.Put("now", group.Now())
|
||||
- info.Put("all", group.All())
|
||||
+ info.Put("now", proxyDisplayName(group.Now()))
|
||||
+ info.Put("all", proxyDisplayNames(group.All()))
|
||||
}
|
||||
return &info
|
||||
}
|
||||
@@ -102,10 +146,10 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
case C.TypeDirect, C.TypeBlock, C.TypeDNS:
|
||||
continue
|
||||
}
|
||||
- allProxies = append(allProxies, detour.Tag())
|
||||
+ allProxies = append(allProxies, proxyDisplayName(detour.Tag()))
|
||||
}
|
||||
|
||||
- defaultTag := server.outbound.Default().Tag()
|
||||
+ defaultTag := proxyDisplayName(server.outbound.Default().Tag())
|
||||
|
||||
sort.SliceStable(allProxies, func(i, j int) bool {
|
||||
return allProxies[i] == defaultTag
|
||||
@@ -114,7 +158,7 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
// fix clash dashboard
|
||||
proxyMap.Put("GLOBAL", map[string]any{
|
||||
"type": "Fallback",
|
||||
- "name": "GLOBAL",
|
||||
+ "name": proxyDisplayName("GLOBAL"),
|
||||
"udp": true,
|
||||
"history": []*adapter.URLTestHistory{},
|
||||
"all": allProxies,
|
||||
@@ -126,7 +170,7 @@ func getProxies(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
if detour.Tag() == "" {
|
||||
tag = F.ToString(i)
|
||||
} else {
|
||||
- tag = detour.Tag()
|
||||
+ tag = proxyDisplayName(detour.Tag())
|
||||
}
|
||||
proxyMap.Put(tag, proxyInfo(server, detour))
|
||||
}
|
||||
@@ -159,29 +203,36 @@ type UpdateProxyRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
-func updateProxy(w http.ResponseWriter, r *http.Request) {
|
||||
- req := UpdateProxyRequest{}
|
||||
- if err := render.DecodeJSON(r.Body, &req); err != nil {
|
||||
- render.Status(r, http.StatusBadRequest)
|
||||
- render.JSON(w, r, ErrBadRequest)
|
||||
- return
|
||||
- }
|
||||
+func updateProxy(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
+ return func(w http.ResponseWriter, r *http.Request) {
|
||||
+ req := UpdateProxyRequest{}
|
||||
+ if err := render.DecodeJSON(r.Body, &req); err != nil {
|
||||
+ render.Status(r, http.StatusBadRequest)
|
||||
+ render.JSON(w, r, ErrBadRequest)
|
||||
+ return
|
||||
+ }
|
||||
|
||||
- proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
|
||||
- selector, ok := proxy.(*group.Selector)
|
||||
- if !ok {
|
||||
- render.Status(r, http.StatusBadRequest)
|
||||
- render.JSON(w, r, newError("Must be a Selector"))
|
||||
- return
|
||||
- }
|
||||
+ proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
|
||||
+ selector, ok := proxy.(*group.Selector)
|
||||
+ if !ok {
|
||||
+ render.Status(r, http.StatusBadRequest)
|
||||
+ render.JSON(w, r, newError("Must be a Selector"))
|
||||
+ return
|
||||
+ }
|
||||
|
||||
- if !selector.SelectOutbound(req.Name) {
|
||||
- render.Status(r, http.StatusBadRequest)
|
||||
- render.JSON(w, r, newError("Selector update error: not found"))
|
||||
- return
|
||||
- }
|
||||
+ selectedName := req.Name
|
||||
+ if targetProxy, exist := lookupProxyByName(server, req.Name); exist {
|
||||
+ selectedName = targetProxy.Tag()
|
||||
+ }
|
||||
+
|
||||
+ if !selector.SelectOutbound(selectedName) {
|
||||
+ render.Status(r, http.StatusBadRequest)
|
||||
+ render.JSON(w, r, newError("Selector update error: not found"))
|
||||
+ return
|
||||
+ }
|
||||
|
||||
- render.NoContent(w, r)
|
||||
+ render.NoContent(w, r)
|
||||
+ }
|
||||
}
|
||||
|
||||
func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
|
||||
diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go
|
||||
index b9222ad5440..d4fd9dd3db1 100644
|
||||
--- a/experimental/clashapi/server.go
|
||||
+++ b/experimental/clashapi/server.go
|
||||
@@ -57,6 +57,7 @@ type Server struct {
|
||||
externalUI string
|
||||
externalUIDownloadURL string
|
||||
externalUIDownloadDetour string
|
||||
+ proxyProviders []option.ClashProxyProviderOptions
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.LifecycleService, error) {
|
||||
@@ -92,6 +93,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
externalController: options.ExternalController != "",
|
||||
externalUIDownloadURL: options.ExternalUIDownloadURL,
|
||||
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
|
||||
+ proxyProviders: options.ProxyProviders,
|
||||
}
|
||||
//goland:noinspection GoDeprecation
|
||||
//nolint:staticcheck
|
||||
@@ -120,7 +122,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
|
||||
r.Mount("/proxies", proxyRouter(s, s.router))
|
||||
r.Mount("/rules", ruleRouter(s.router))
|
||||
r.Mount("/connections", connectionRouter(s.ctx, s.network, trafficManager))
|
||||
- r.Mount("/providers/proxies", proxyProviderRouter())
|
||||
+ r.Mount("/providers/proxies", proxyProviderRouter(s))
|
||||
r.Mount("/providers/rules", ruleProviderRouter())
|
||||
r.Mount("/script", scriptRouter())
|
||||
r.Mount("/profile", profileRouter())
|
||||
diff --git a/option/experimental.go b/option/experimental.go
|
||||
index bf144ad4452..89f303a7b26 100644
|
||||
--- a/option/experimental.go
|
||||
+++ b/option/experimental.go
|
||||
@@ -34,6 +34,7 @@ type ClashAPIOptions struct {
|
||||
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty" reference:"outbound"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
DefaultMode string `json:"default_mode,omitempty"`
|
||||
+ ProxyProviders []ClashProxyProviderOptions `json:"proxy_providers,omitempty"`
|
||||
AccessControlAllowOrigin badoption.Listable[string] `json:"access_control_allow_origin,omitempty"`
|
||||
AccessControlAllowPrivateNetwork bool `json:"access_control_allow_private_network,omitempty"`
|
||||
|
||||
@@ -49,6 +50,23 @@ type ClashAPIOptions struct {
|
||||
StoreFakeIP bool `json:"store_fakeip,omitempty" schema:"omit"`
|
||||
}
|
||||
|
||||
+type ClashProxyProviderOptions struct {
|
||||
+ Name string `json:"name,omitempty"`
|
||||
+ Type string `json:"type,omitempty"`
|
||||
+ VehicleType string `json:"vehicle_type,omitempty"`
|
||||
+ Proxies []string `json:"proxies,omitempty"`
|
||||
+ UpdatedAt string `json:"updated_at,omitempty"`
|
||||
+ UpdateID string `json:"update_id,omitempty"`
|
||||
+ SubscriptionInfo *ClashProxyProviderSubscriptionInfo `json:"subscription_info,omitempty"`
|
||||
+}
|
||||
+
|
||||
+type ClashProxyProviderSubscriptionInfo struct {
|
||||
+ Upload uint64 `json:"upload,omitempty"`
|
||||
+ Download uint64 `json:"download,omitempty"`
|
||||
+ Total uint64 `json:"total,omitempty"`
|
||||
+ Expire int64 `json:"expire,omitempty"`
|
||||
+}
|
||||
+
|
||||
type V2RayAPIOptions struct {
|
||||
Listen string `json:"listen,omitempty"`
|
||||
Stats *V2RayStatsServiceOptions `json:"stats,omitempty"`
|
||||
Reference in new issue
Block a user