mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-03 10:53:56 +08:00
✨ Sync 2026-10-11 20:48:25
This commit is contained in:
1 parent
78f0ba8251
commit
5ce75c9f2b
45 files changed
+54
-879
No files matched your search
+4
-4
@@ -1,17 +1,17 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=clashoo
|
PKG_NAME:=clashoo
|
||||||
PKG_SHORT_SHA:=e4dd968
|
PKG_SHORT_SHA:=bc76c13
|
||||||
PKG_COMMIT_DATE:=2026.10.08
|
PKG_COMMIT_DATE:=2026.10.11
|
||||||
|
|
||||||
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
|
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA)
|
||||||
PKG_RELEASE:=8
|
PKG_RELEASE:=9
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
|
||||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
|
||||||
PKG_SOURCE_VERSION:=7c870e53cf5004a1000b5c8d9b8caba5cf889927
|
PKG_SOURCE_VERSION:=ee4c1d93df08d24d6006d5ce8547fee5d75ce99b
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=honk
|
PKG_NAME:=honk
|
||||||
PKG_VERSION:=2026.10.9_pre3
|
PKG_VERSION:=2026.10.9_pre3
|
||||||
PKG_RELEASE:=16
|
PKG_RELEASE:=17
|
||||||
|
|
||||||
# Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49)
|
# Raw upstream release tag. It keeps the dots in qualifiers (e.g. v0.0.1.beta.49)
|
||||||
# because that is what the GitHub release asset name uses.
|
# because that is what the GitHub release asset name uses.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config honk 'config'
|
config honk 'config'
|
||||||
option enabled '0'
|
option enabled '0'
|
||||||
option advanced '0'
|
option advanced '1'
|
||||||
option dashboard 'none'
|
option dashboard 'none'
|
||||||
option config_file '/etc/honk/config.dae'
|
option config_file '/etc/honk/config.dae'
|
||||||
|
|
||||||
@@ -1 +1 @@
|
|||||||
release
|
both
|
||||||
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-clashoo
|
PKG_NAME:=luci-app-clashoo
|
||||||
PKG_VERSION:=1.31.0
|
PKG_VERSION:=1.31.0
|
||||||
PKG_RELEASE:=6
|
PKG_RELEASE:=7
|
||||||
PKG_MAINTAINER:=kenzok8
|
PKG_MAINTAINER:=kenzok8
|
||||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ function clKernelUnlock() {
|
|||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
_busy: false,
|
_busy: false,
|
||||||
_op: null, /* 'start' | 'stop' | 'restart' | 'reload' | null */
|
_op: null, /* 'start' | 'stop' | null */
|
||||||
_opTimers: null, /* array of setTimeout IDs for cleanup */
|
_opTimers: null, /* array of setTimeout IDs for cleanup */
|
||||||
_accessRefreshing: false,
|
_accessRefreshing: false,
|
||||||
_coreSwitchBusy: false,
|
_coreSwitchBusy: false,
|
||||||
@@ -789,29 +789,7 @@ return view.extend({
|
|||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
return running ? self._stop() : self._start();
|
return running ? self._stop() : self._start();
|
||||||
}
|
}
|
||||||
}, [E('span', { 'class': 'cl-service-knob' })]),
|
}, [E('span', { 'class': 'cl-service-knob' })])
|
||||||
E('button', {
|
|
||||||
type: 'button',
|
|
||||||
'class': 'btn cbi-button-action cl-btn-sm',
|
|
||||||
title: _("Restart Service"),
|
|
||||||
disabled: (disabled || !running) ? '' : null,
|
|
||||||
click: function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
if (disabled || !running) return;
|
|
||||||
return self._restart();
|
|
||||||
}
|
|
||||||
}, _("Restart")),
|
|
||||||
E('button', {
|
|
||||||
type: 'button',
|
|
||||||
'class': 'btn cbi-button-action cl-btn-sm',
|
|
||||||
title: _("Reload Service"),
|
|
||||||
disabled: (disabled || !running) ? '' : null,
|
|
||||||
click: function (ev) {
|
|
||||||
ev.preventDefault();
|
|
||||||
if (disabled || !running) return;
|
|
||||||
return self._reload();
|
|
||||||
}
|
|
||||||
}, _("Reload"))
|
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1966,7 +1944,7 @@ return view.extend({
|
|||||||
|
|
||||||
// preflight: refuse start when no profile selected.
|
// preflight: refuse start when no profile selected.
|
||||||
// backend select_config also defends but toast is slow, UI would flash.
|
// backend select_config also defends but toast is slow, UI would flash.
|
||||||
if (opKey === 'start' || opKey === 'restart' || opKey === 'reload') {
|
if (opKey === 'start') {
|
||||||
var st0 = this._lastSt || {};
|
var st0 = this._lastSt || {};
|
||||||
var hasConfig = !!(st0.config || st0.conf_path);
|
var hasConfig = !!(st0.config || st0.conf_path);
|
||||||
if (!hasConfig) {
|
if (!hasConfig) {
|
||||||
@@ -1988,14 +1966,12 @@ return view.extend({
|
|||||||
btn.setAttribute('aria-pressed', newRunning ? 'true' : 'false');
|
btn.setAttribute('aria-pressed', newRunning ? 'true' : 'false');
|
||||||
btn.className = 'cl-service-switch' + (newRunning ? ' is-on' : ' is-off');
|
btn.className = 'cl-service-switch' + (newRunning ? ' is-on' : ' is-off');
|
||||||
}
|
}
|
||||||
self._showOpMsg(opKey === 'stop' ? _("Stopping…") : opKey === 'reload' ? _("Reloading…") : _("Starting…"));
|
self._showOpMsg(opKey === 'stop' ? _("Stopping…") : _("Starting…"));
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxWait = opKey === 'stop' ? 15000 : 35000;
|
var maxWait = opKey === 'stop' ? 15000 : 35000;
|
||||||
var started = Date.now();
|
var started = Date.now();
|
||||||
var pollTimer = null;
|
var pollTimer = null;
|
||||||
var initialPid = (this._lastSt && this._lastSt.pid) || 0;
|
|
||||||
var sawStopped = false;
|
|
||||||
var rpcDone = false;
|
var rpcDone = false;
|
||||||
var finished = false;
|
var finished = false;
|
||||||
|
|
||||||
@@ -2027,10 +2003,8 @@ return view.extend({
|
|||||||
if (opKey === 'stop') {
|
if (opKey === 'stop') {
|
||||||
if (rpcDone && st.running === false) return finish(_("Stopped") + ' ⚪');
|
if (rpcDone && st.running === false) return finish(_("Stopped") + ' ⚪');
|
||||||
} else {
|
} else {
|
||||||
if (st.running === false) sawStopped = true;
|
|
||||||
if (rpcDone && st.health_status === 'fail') return finish(null);
|
if (rpcDone && st.health_status === 'fail') return finish(null);
|
||||||
if (rpcDone && st.running === true &&
|
if (rpcDone && st.running === true)
|
||||||
(opKey !== 'restart' || (initialPid && st.pid && st.pid !== initialPid) || sawStopped))
|
|
||||||
return finish(_("Running 🟢"));
|
return finish(_("Running 🟢"));
|
||||||
}
|
}
|
||||||
if (elapsed >= maxWait) return finish(null);
|
if (elapsed >= maxWait) return finish(null);
|
||||||
@@ -2044,10 +2018,8 @@ return view.extend({
|
|||||||
pollTimer = setTimeout(pollOnce, 100);
|
pollTimer = setTimeout(pollOnce, 100);
|
||||||
},
|
},
|
||||||
|
|
||||||
_start: function () { return this._svc(function () { return clashoo.start(); }, 'start'); },
|
_start: function () { return this._svc(function () { return clashoo.start(); }, 'start'); },
|
||||||
_stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); },
|
_stop: function () { return this._svc(function () { return clashoo.stop(); }, 'stop'); },
|
||||||
_restart: function () { return this._svc(function () { return clashoo.restart(); }, 'restart'); },
|
|
||||||
_reload: function () { return this._svc(function () { return clashoo.reload(); }, 'reload'); },
|
|
||||||
|
|
||||||
_updSubs: function (ev) {
|
_updSubs: function (ev) {
|
||||||
var btn = ev && ev.target;
|
var btn = ev && ev.target;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ LUCI_DEPENDS:= \
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-homeproxy
|
PKG_NAME:=luci-app-homeproxy
|
||||||
PKG_VERSION:=27.919.1.14
|
PKG_VERSION:=27.919.1.14
|
||||||
PKG_RELEASE:=22
|
PKG_RELEASE:=23
|
||||||
|
|
||||||
define Package/luci-app-homeproxy/conffiles
|
define Package/luci-app-homeproxy/conffiles
|
||||||
/etc/config/homeproxy
|
/etc/config/homeproxy
|
||||||
|
|||||||
@@ -12,9 +12,6 @@
|
|||||||
'require uci';
|
'require uci';
|
||||||
'require ui';
|
'require ui';
|
||||||
|
|
||||||
/* 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_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_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.');
|
const TRANSPORT_QUIC_HINT = _('No additional encryption support: It\'s basically duplicate encryption.');
|
||||||
@@ -38,28 +35,23 @@ return baseclass.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
shadowsocks_encrypt_length: {
|
shadowsocks_encrypt_length: {
|
||||||
/* AEAD */
|
|
||||||
'aes-128-gcm': 0,
|
'aes-128-gcm': 0,
|
||||||
'aes-192-gcm': 0,
|
'aes-192-gcm': 0,
|
||||||
'aes-256-gcm': 0,
|
'aes-256-gcm': 0,
|
||||||
'chacha20-ietf-poly1305': 0,
|
'chacha20-ietf-poly1305': 0,
|
||||||
'xchacha20-ietf-poly1305': 0,
|
'xchacha20-ietf-poly1305': 0,
|
||||||
/* AEAD 2022 */
|
|
||||||
'2022-blake3-aes-128-gcm': 16,
|
'2022-blake3-aes-128-gcm': 16,
|
||||||
'2022-blake3-aes-256-gcm': 32,
|
'2022-blake3-aes-256-gcm': 32,
|
||||||
'2022-blake3-chacha20-poly1305': 32
|
'2022-blake3-chacha20-poly1305': 32
|
||||||
},
|
},
|
||||||
|
|
||||||
shadowsocks_encrypt_methods: [
|
shadowsocks_encrypt_methods: [
|
||||||
/* Stream */
|
|
||||||
'none',
|
'none',
|
||||||
/* AEAD */
|
|
||||||
'aes-128-gcm',
|
'aes-128-gcm',
|
||||||
'aes-192-gcm',
|
'aes-192-gcm',
|
||||||
'aes-256-gcm',
|
'aes-256-gcm',
|
||||||
'chacha20-ietf-poly1305',
|
'chacha20-ietf-poly1305',
|
||||||
'xchacha20-ietf-poly1305',
|
'xchacha20-ietf-poly1305',
|
||||||
/* AEAD 2022 */
|
|
||||||
'2022-blake3-aes-128-gcm',
|
'2022-blake3-aes-128-gcm',
|
||||||
'2022-blake3-aes-256-gcm',
|
'2022-blake3-aes-256-gcm',
|
||||||
'2022-blake3-chacha20-poly1305'
|
'2022-blake3-chacha20-poly1305'
|
||||||
@@ -102,10 +94,6 @@ return baseclass.extend({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/* 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) {
|
renderTransportOptions(section, options) {
|
||||||
const features = options.features || {},
|
const features = options.features || {},
|
||||||
side = options.side || 'client';
|
side = options.side || 'client';
|
||||||
@@ -236,10 +224,6 @@ return baseclass.extend({
|
|||||||
return o;
|
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) {
|
renderTlsOptions(section, options) {
|
||||||
const side = options.side || 'client';
|
const side = options.side || 'client';
|
||||||
let o;
|
let o;
|
||||||
@@ -323,7 +307,6 @@ return baseclass.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
calcStringMD5(e) {
|
calcStringMD5(e) {
|
||||||
/* Thanks to https://stackoverflow.com/a/41602636 */
|
|
||||||
let h = (a, b) => {
|
let h = (a, b) => {
|
||||||
let c, d, e, f, g;
|
let c, d, e, f, g;
|
||||||
c = a & 2147483648;
|
c = a & 2147483648;
|
||||||
@@ -400,7 +383,6 @@ return baseclass.extend({
|
|||||||
if (!str)
|
if (!str)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
/* Thanks to luci-app-ssr-plus */
|
|
||||||
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
str = str.replace(/-/g, '+').replace(/_/g, '/');
|
||||||
let padding = (4 - str.length % 4) % 4;
|
let padding = (4 - str.length % 4) % 4;
|
||||||
if (padding)
|
if (padding)
|
||||||
@@ -427,14 +409,12 @@ return baseclass.extend({
|
|||||||
byteArr = crypto.getRandomValues(new Uint8Array(length));
|
byteArr = crypto.getRandomValues(new Uint8Array(length));
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'base64':
|
case 'base64':
|
||||||
/* Thanks to https://stackoverflow.com/questions/9267899 */
|
|
||||||
return btoa(String.fromCharCode.apply(null, byteArr));
|
return btoa(String.fromCharCode.apply(null, byteArr));
|
||||||
case 'hex':
|
case 'hex':
|
||||||
return Array.from(byteArr, (byte) =>
|
return Array.from(byteArr, (byte) =>
|
||||||
(byte & 255).toString(16).padStart(2, '0')
|
(byte & 255).toString(16).padStart(2, '0')
|
||||||
).join('');
|
).join('');
|
||||||
case 'uuid':
|
case 'uuid':
|
||||||
/* Thanks to https://stackoverflow.com/a/2117523 */
|
|
||||||
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
|
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) =>
|
||||||
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
|
||||||
);
|
);
|
||||||
@@ -501,7 +481,6 @@ return baseclass.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
validateBase64Key(length, section_id, value) {
|
validateBase64Key(length, section_id, value) {
|
||||||
/* Thanks to luci-proto-wireguard */
|
|
||||||
if (section_id && value)
|
if (section_id && value)
|
||||||
if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
|
if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=')
|
||||||
return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
|
return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length));
|
||||||
@@ -526,7 +505,6 @@ return baseclass.extend({
|
|||||||
else if (!value[2])
|
else if (!value[2])
|
||||||
value[2] = 65535;
|
value[2] = 65535;
|
||||||
|
|
||||||
/* numeric comparison: string '<' does lexicographic ordering */
|
|
||||||
if (parseInt(value[1], 10) < parseInt(value[2], 10) && value[2] <= 65535)
|
if (parseInt(value[1], 10) < parseInt(value[2], 10) && value[2] <= 65535)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ return view.extend({
|
|||||||
let features = data[1],
|
let features = data[1],
|
||||||
hosts = data[2]?.hosts;
|
hosts = data[2]?.hosts;
|
||||||
|
|
||||||
/* Cache all configured proxy nodes, they will be called multiple times */
|
|
||||||
let proxy_nodes = {};
|
let proxy_nodes = {};
|
||||||
uci.sections(data[0], 'node', (res) => {
|
uci.sections(data[0], 'node', (res) => {
|
||||||
let nodeaddr = ((res.type === 'direct') ? res.override_address : res.address) || '',
|
let nodeaddr = ((res.type === 'direct') ? res.override_address : res.address) || '',
|
||||||
@@ -352,8 +351,6 @@ return view.extend({
|
|||||||
o.depends('proxy_mode', 'redirect_tun');
|
o.depends('proxy_mode', 'redirect_tun');
|
||||||
o.depends('proxy_mode', 'tun');
|
o.depends('proxy_mode', 'tun');
|
||||||
|
|
||||||
/* Custom routing settings start */
|
|
||||||
/* Routing settings start */
|
|
||||||
o = s.taboption('routing', form.SectionValue, '_routing', form.NamedSection, 'routing', 'homeproxy');
|
o = s.taboption('routing', form.SectionValue, '_routing', form.NamedSection, 'routing', 'homeproxy');
|
||||||
o.depends('routing_mode', 'custom');
|
o.depends('routing_mode', 'custom');
|
||||||
|
|
||||||
@@ -443,9 +440,7 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
so.default = 'default-dns';
|
so.default = 'default-dns';
|
||||||
so.rmempty = false;
|
so.rmempty = false;
|
||||||
/* Routing settings end */
|
|
||||||
|
|
||||||
/* Routing nodes start */
|
|
||||||
s.tab('routing_node', _('Routing Nodes'));
|
s.tab('routing_node', _('Routing Nodes'));
|
||||||
o = s.taboption('routing_node', form.SectionValue, '_routing_node', form.GridSection, 'routing_node');
|
o = s.taboption('routing_node', form.SectionValue, '_routing_node', form.GridSection, 'routing_node');
|
||||||
o.depends('routing_mode', 'custom');
|
o.depends('routing_mode', 'custom');
|
||||||
@@ -614,9 +609,7 @@ return view.extend({
|
|||||||
_('Interrupt existing connections when the selected outbound has changed.'));
|
_('Interrupt existing connections when the selected outbound has changed.'));
|
||||||
so.depends('node', 'urltest');
|
so.depends('node', 'urltest');
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
/* Routing nodes end */
|
|
||||||
|
|
||||||
/* Routing rules start */
|
|
||||||
s.tab('routing_rule', _('Routing Rules'));
|
s.tab('routing_rule', _('Routing Rules'));
|
||||||
o = s.taboption('routing_rule', form.SectionValue, '_routing_rule', form.GridSection, 'routing_rule');
|
o = s.taboption('routing_rule', form.SectionValue, '_routing_rule', form.GridSection, 'routing_rule');
|
||||||
o.depends('routing_mode', 'custom');
|
o.depends('routing_mode', 'custom');
|
||||||
@@ -958,9 +951,7 @@ return view.extend({
|
|||||||
so = ss.taboption('fields_process', form.DynamicList, 'process_path_regex', _('Process path (regex)'),
|
so = ss.taboption('fields_process', form.DynamicList, 'process_path_regex', _('Process path (regex)'),
|
||||||
_('Match process path using regular expression.'));
|
_('Match process path using regular expression.'));
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
/* Routing rules end */
|
|
||||||
|
|
||||||
/* DNS settings start */
|
|
||||||
s.tab('dns', _('DNS Settings'));
|
s.tab('dns', _('DNS Settings'));
|
||||||
o = s.taboption('dns', form.SectionValue, '_dns', form.NamedSection, 'dns', 'homeproxy');
|
o = s.taboption('dns', form.SectionValue, '_dns', form.NamedSection, 'dns', 'homeproxy');
|
||||||
o.depends('routing_mode', 'custom');
|
o.depends('routing_mode', 'custom');
|
||||||
@@ -1015,9 +1006,7 @@ return view.extend({
|
|||||||
_('Persist DNS cache across restarts (sing-box 1.14, replaces Store RDRC).'));
|
_('Persist DNS cache across restarts (sing-box 1.14, replaces Store RDRC).'));
|
||||||
so.depends('disable_cache', '0');
|
so.depends('disable_cache', '0');
|
||||||
so.rmempty = false;
|
so.rmempty = false;
|
||||||
/* DNS settings end */
|
|
||||||
|
|
||||||
/* DNS servers start */
|
|
||||||
s.tab('dns_server', _('DNS Servers'));
|
s.tab('dns_server', _('DNS Servers'));
|
||||||
o = s.taboption('dns_server', form.SectionValue, '_dns_server', form.GridSection, 'dns_server');
|
o = s.taboption('dns_server', form.SectionValue, '_dns_server', form.GridSection, 'dns_server');
|
||||||
o.depends('routing_mode', 'custom');
|
o.depends('routing_mode', 'custom');
|
||||||
@@ -1138,9 +1127,7 @@ return view.extend({
|
|||||||
so.default = 'direct-out';
|
so.default = 'direct-out';
|
||||||
so.rmempty = false;
|
so.rmempty = false;
|
||||||
so.editable = true;
|
so.editable = true;
|
||||||
/* DNS servers end */
|
|
||||||
|
|
||||||
/* DNS rules start */
|
|
||||||
s.tab('dns_rule', _('DNS Rules'));
|
s.tab('dns_rule', _('DNS Rules'));
|
||||||
o = s.taboption('dns_rule', form.SectionValue, '_dns_rule', form.GridSection, 'dns_rule');
|
o = s.taboption('dns_rule', form.SectionValue, '_dns_rule', form.GridSection, 'dns_rule');
|
||||||
o.depends('routing_mode', 'custom');
|
o.depends('routing_mode', 'custom');
|
||||||
@@ -1470,10 +1457,7 @@ return view.extend({
|
|||||||
so = ss.taboption('fields_process', form.DynamicList, 'process_path_regex', _('Process path (regex)'),
|
so = ss.taboption('fields_process', form.DynamicList, 'process_path_regex', _('Process path (regex)'),
|
||||||
_('Match process path using regular expression.'));
|
_('Match process path using regular expression.'));
|
||||||
so.modalonly = true;
|
so.modalonly = true;
|
||||||
/* DNS rules end */
|
|
||||||
/* Custom routing settings end */
|
|
||||||
|
|
||||||
/* Rule set settings start */
|
|
||||||
s.tab('ruleset', _('Rule Set'));
|
s.tab('ruleset', _('Rule Set'));
|
||||||
o = s.taboption('ruleset', form.SectionValue, '_ruleset', form.GridSection, 'ruleset');
|
o = s.taboption('ruleset', form.SectionValue, '_ruleset', form.GridSection, 'ruleset');
|
||||||
o.depends('routing_mode', 'custom');
|
o.depends('routing_mode', 'custom');
|
||||||
@@ -1584,15 +1568,12 @@ return view.extend({
|
|||||||
_('Update interval of rule set.'));
|
_('Update interval of rule set.'));
|
||||||
so.placeholder = '1d';
|
so.placeholder = '1d';
|
||||||
so.depends('type', 'remote');
|
so.depends('type', 'remote');
|
||||||
/* Rule set settings end */
|
|
||||||
|
|
||||||
/* ACL settings start */
|
|
||||||
s.tab('control', _('Access Control'));
|
s.tab('control', _('Access Control'));
|
||||||
|
|
||||||
o = s.taboption('control', form.SectionValue, '_control', form.NamedSection, 'control', 'homeproxy');
|
o = s.taboption('control', form.SectionValue, '_control', form.NamedSection, 'control', 'homeproxy');
|
||||||
ss = o.subsection;
|
ss = o.subsection;
|
||||||
|
|
||||||
/* Interface control start */
|
|
||||||
ss.tab('interface', _('Interface Control'));
|
ss.tab('interface', _('Interface Control'));
|
||||||
|
|
||||||
so = ss.taboption('interface', widgets.DeviceSelect, 'listen_interfaces', _('Listen interfaces'),
|
so = ss.taboption('interface', widgets.DeviceSelect, 'listen_interfaces', _('Listen interfaces'),
|
||||||
@@ -1604,9 +1585,7 @@ return view.extend({
|
|||||||
_('Bind outbound traffic to specific interface. Leave empty to auto detect.'));
|
_('Bind outbound traffic to specific interface. Leave empty to auto detect.'));
|
||||||
so.multiple = false;
|
so.multiple = false;
|
||||||
so.noaliases = true;
|
so.noaliases = true;
|
||||||
/* Interface control end */
|
|
||||||
|
|
||||||
/* LAN IP policy start */
|
|
||||||
ss.tab('lan_ip_policy', _('LAN IP Policy'));
|
ss.tab('lan_ip_policy', _('LAN IP Policy'));
|
||||||
|
|
||||||
so = ss.taboption('lan_ip_policy', form.ListValue, 'lan_proxy_mode', _('Proxy filter mode'));
|
so = ss.taboption('lan_ip_policy', form.ListValue, 'lan_proxy_mode', _('Proxy filter mode'));
|
||||||
@@ -1649,9 +1628,7 @@ return view.extend({
|
|||||||
|
|
||||||
so = fwtool.addMACOption(ss, 'lan_ip_policy', 'lan_global_proxy_mac_addrs', _('Global proxy MAC-s'), null, hosts);
|
so = fwtool.addMACOption(ss, 'lan_ip_policy', 'lan_global_proxy_mac_addrs', _('Global proxy MAC-s'), null, hosts);
|
||||||
so.depends({'homeproxy.config.routing_mode': 'custom', '!reverse': true});
|
so.depends({'homeproxy.config.routing_mode': 'custom', '!reverse': true});
|
||||||
/* LAN IP policy end */
|
|
||||||
|
|
||||||
/* WAN IP policy start */
|
|
||||||
ss.tab('wan_ip_policy', _('WAN IP Policy'));
|
ss.tab('wan_ip_policy', _('WAN IP Policy'));
|
||||||
|
|
||||||
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_proxy_ipv4_ips', _('Proxy IPv4 IP-s'));
|
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_proxy_ipv4_ips', _('Proxy IPv4 IP-s'));
|
||||||
@@ -1667,9 +1644,7 @@ return view.extend({
|
|||||||
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_direct_ipv6_ips', _('Direct IPv6 IP-s'));
|
so = ss.taboption('wan_ip_policy', form.DynamicList, 'wan_direct_ipv6_ips', _('Direct IPv6 IP-s'));
|
||||||
so.datatype = 'or(ip6addr, cidr6)';
|
so.datatype = 'or(ip6addr, cidr6)';
|
||||||
so.depends('homeproxy.config.ipv6_support', '1');
|
so.depends('homeproxy.config.ipv6_support', '1');
|
||||||
/* WAN IP policy end */
|
|
||||||
|
|
||||||
/* Proxy domain list start */
|
|
||||||
ss.tab('proxy_domain_list', _('Proxy Domain List'));
|
ss.tab('proxy_domain_list', _('Proxy Domain List'));
|
||||||
|
|
||||||
so = ss.taboption('proxy_domain_list', form.TextValue, '_proxy_domain_list');
|
so = ss.taboption('proxy_domain_list', form.TextValue, '_proxy_domain_list');
|
||||||
@@ -1699,9 +1674,7 @@ return view.extend({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/* Proxy domain list end */
|
|
||||||
|
|
||||||
/* Direct domain list start */
|
|
||||||
ss.tab('direct_domain_list', _('Direct Domain List'));
|
ss.tab('direct_domain_list', _('Direct Domain List'));
|
||||||
|
|
||||||
so = ss.taboption('direct_domain_list', form.TextValue, '_direct_domain_list');
|
so = ss.taboption('direct_domain_list', form.TextValue, '_direct_domain_list');
|
||||||
@@ -1731,10 +1704,7 @@ return view.extend({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/* Direct domain list end */
|
|
||||||
/* ACL settings end */
|
|
||||||
|
|
||||||
/* DNS cache / timeout for preset routing modes (rightmost tab, stored in the dns section) */
|
|
||||||
s.tab('dns_cache', _('DNS Cache (1.14)'));
|
s.tab('dns_cache', _('DNS Cache (1.14)'));
|
||||||
o = s.taboption('dns_cache', form.SectionValue, '_dns_cache', form.NamedSection, 'dns', 'homeproxy');
|
o = s.taboption('dns_cache', form.SectionValue, '_dns_cache', form.NamedSection, 'dns', 'homeproxy');
|
||||||
o.depends({'routing_mode': 'custom', '!reverse': true});
|
o.depends({'routing_mode': 'custom', '!reverse': true});
|
||||||
|
|||||||
@@ -26,11 +26,9 @@ function parseShareLink(uri, features) {
|
|||||||
if (uri[0] && uri[1]) {
|
if (uri[0] && uri[1]) {
|
||||||
switch (uri[0]) {
|
switch (uri[0]) {
|
||||||
case 'anytls':
|
case 'anytls':
|
||||||
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
|
|
||||||
url = new URL('http://' + uri[1]);
|
url = new URL('http://' + uri[1]);
|
||||||
params = url.searchParams;
|
params = url.searchParams;
|
||||||
|
|
||||||
/* Check if password exists */
|
|
||||||
if (!url.username)
|
if (!url.username)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -62,11 +60,9 @@ function parseShareLink(uri, features) {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case 'hysteria':
|
case 'hysteria':
|
||||||
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
|
|
||||||
url = new URL('http://' + uri[1]);
|
url = new URL('http://' + uri[1]);
|
||||||
params = url.searchParams;
|
params = url.searchParams;
|
||||||
|
|
||||||
/* WeChat-Video / FakeTCP are unsupported by sing-box currently */
|
|
||||||
if (!features.with_quic || (params.get('protocol') && params.get('protocol') !== 'udp'))
|
if (!features.with_quic || (params.get('protocol') && params.get('protocol') !== 'udp'))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -90,7 +86,6 @@ function parseShareLink(uri, features) {
|
|||||||
break;
|
break;
|
||||||
case 'hysteria2':
|
case 'hysteria2':
|
||||||
case 'hy2':
|
case 'hy2':
|
||||||
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
|
|
||||||
url = new URL('http://' + uri[1]);
|
url = new URL('http://' + uri[1]);
|
||||||
params = url.searchParams;
|
params = url.searchParams;
|
||||||
|
|
||||||
@@ -114,7 +109,6 @@ function parseShareLink(uri, features) {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case 'snell':
|
case 'snell':
|
||||||
/* Surge Snell share link: snell://host:port?psk=..&obfs=http&obfs-host=..#name */
|
|
||||||
url = new URL('http://' + uri[1]);
|
url = new URL('http://' + uri[1]);
|
||||||
params = url.searchParams;
|
params = url.searchParams;
|
||||||
|
|
||||||
@@ -153,7 +147,6 @@ function parseShareLink(uri, features) {
|
|||||||
break;
|
break;
|
||||||
case 'ss':
|
case 'ss':
|
||||||
try {
|
try {
|
||||||
/* "Lovely" Shadowrocket format */
|
|
||||||
try {
|
try {
|
||||||
let suri = uri[1].split('#'), slabel = '';
|
let suri = uri[1].split('#'), slabel = '';
|
||||||
if (suri.length <= 2) {
|
if (suri.length <= 2) {
|
||||||
@@ -163,15 +156,12 @@ function parseShareLink(uri, features) {
|
|||||||
}
|
}
|
||||||
} catch(e) { }
|
} catch(e) { }
|
||||||
|
|
||||||
/* SIP002 format https://shadowsocks.org/guide/sip002.html */
|
|
||||||
url = new URL('http://' + uri[1]);
|
url = new URL('http://' + uri[1]);
|
||||||
|
|
||||||
let userinfo;
|
let userinfo;
|
||||||
if (url.username && url.password) {
|
if (url.username && url.password) {
|
||||||
/* User info encoded with URIComponent */
|
|
||||||
userinfo = [url.username, decodeURIComponent(url.password)];
|
userinfo = [url.username, decodeURIComponent(url.password)];
|
||||||
} else if (url.username) {
|
} else if (url.username) {
|
||||||
/* User info encoded with base64 */
|
|
||||||
userinfo = hp.decodeBase64Str(decodeURIComponent(url.username)).split(':');
|
userinfo = hp.decodeBase64Str(decodeURIComponent(url.username)).split(':');
|
||||||
if (userinfo.length > 1)
|
if (userinfo.length > 1)
|
||||||
userinfo = [userinfo[0], userinfo.slice(1).join(':')]
|
userinfo = [userinfo[0], userinfo.slice(1).join(':')]
|
||||||
@@ -184,9 +174,6 @@ function parseShareLink(uri, features) {
|
|||||||
if (url.search && url.searchParams.get('plugin')) {
|
if (url.search && url.searchParams.get('plugin')) {
|
||||||
let plugin_info = url.searchParams.get('plugin').split(';');
|
let plugin_info = url.searchParams.get('plugin').split(';');
|
||||||
plugin = plugin_info[0];
|
plugin = plugin_info[0];
|
||||||
/* sing-box only knows obfs-local; simple-obfs (an old
|
|
||||||
alias providers still emit) would otherwise be rejected
|
|
||||||
as "plugin not found" and take the whole config with it */
|
|
||||||
if (plugin === 'simple-obfs')
|
if (plugin === 'simple-obfs')
|
||||||
plugin = 'obfs-local';
|
plugin = 'obfs-local';
|
||||||
plugin_opts = (plugin_info.length > 1) ? plugin_info.slice(1).join(';') : null;
|
plugin_opts = (plugin_info.length > 1) ? plugin_info.slice(1).join(';') : null;
|
||||||
@@ -203,7 +190,6 @@ function parseShareLink(uri, features) {
|
|||||||
shadowsocks_plugin_opts: plugin_opts
|
shadowsocks_plugin_opts: plugin_opts
|
||||||
};
|
};
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
/* Legacy format https://github.com/shadowsocks/shadowsocks-org/commit/78ca46cd6859a4e9475953ed34a2d301454f579e */
|
|
||||||
uri = uri[1].split('@');
|
uri = uri[1].split('@');
|
||||||
if (uri.length < 2)
|
if (uri.length < 2)
|
||||||
return null;
|
return null;
|
||||||
@@ -221,11 +207,9 @@ function parseShareLink(uri, features) {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case 'trojan':
|
case 'trojan':
|
||||||
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
|
|
||||||
url = new URL('http://' + uri[1]);
|
url = new URL('http://' + uri[1]);
|
||||||
params = url.searchParams;
|
params = url.searchParams;
|
||||||
|
|
||||||
/* Check if password exists */
|
|
||||||
if (!url.username)
|
if (!url.username)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -256,11 +240,9 @@ function parseShareLink(uri, features) {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case 'tuic':
|
case 'tuic':
|
||||||
/* https://github.com/daeuniverse/dae/discussions/182 */
|
|
||||||
url = new URL('http://' + uri[1]);
|
url = new URL('http://' + uri[1]);
|
||||||
params = url.searchParams;
|
params = url.searchParams;
|
||||||
|
|
||||||
/* Check if uuid exists */
|
|
||||||
if (!url.username)
|
if (!url.username)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -280,16 +262,13 @@ function parseShareLink(uri, features) {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case 'vless':
|
case 'vless':
|
||||||
/* https://github.com/XTLS/Xray-core/discussions/716 */
|
|
||||||
url = new URL('http://' + uri[1]);
|
url = new URL('http://' + uri[1]);
|
||||||
params = url.searchParams;
|
params = url.searchParams;
|
||||||
|
|
||||||
/* Unsupported protocol */
|
|
||||||
if (params.get('type') === 'kcp')
|
if (params.get('type') === 'kcp')
|
||||||
return null;
|
return null;
|
||||||
else if (params.get('type') === 'quic' && ((params.get('quicSecurity') && params.get('quicSecurity') !== 'none') || !features.with_quic))
|
else if (params.get('type') === 'quic' && ((params.get('quicSecurity') && params.get('quicSecurity') !== 'none') || !features.with_quic))
|
||||||
return null;
|
return null;
|
||||||
/* Check if uuid and type exist */
|
|
||||||
if (!url.username || !params.get('type'))
|
if (!url.username || !params.get('type'))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -307,9 +286,6 @@ function parseShareLink(uri, features) {
|
|||||||
tls_reality_public_key: params.get('pbk') ? decodeURIComponent(params.get('pbk')) : null,
|
tls_reality_public_key: params.get('pbk') ? decodeURIComponent(params.get('pbk')) : null,
|
||||||
tls_reality_short_id: params.get('sid'),
|
tls_reality_short_id: params.get('sid'),
|
||||||
tls_utls: features.with_utls ? params.get('fp') : null,
|
tls_utls: features.with_utls ? params.get('fp') : null,
|
||||||
/* only the value sing-box supports: any other flow is
|
|
||||||
rejected with "unsupported flow" and one bad node breaks
|
|
||||||
the whole generated config */
|
|
||||||
vless_flow: (['tls', 'reality'].includes(params.get('security')) && params.get('flow') === 'xtls-rprx-vision') ? params.get('flow') : null
|
vless_flow: (['tls', 'reality'].includes(params.get('security')) && params.get('flow') === 'xtls-rprx-vision') ? params.get('flow') : null
|
||||||
};
|
};
|
||||||
switch (params.get('type')) {
|
switch (params.get('type')) {
|
||||||
@@ -340,26 +316,17 @@ function parseShareLink(uri, features) {
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case 'vmess':
|
case 'vmess':
|
||||||
/* "Lovely" shadowrocket format; uri is the split scheme/rest
|
|
||||||
array, so this has to test the link body (testing the array
|
|
||||||
itself compared '&' against its elements and never matched) */
|
|
||||||
if (uri[1].includes('&'))
|
if (uri[1].includes('&'))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
|
|
||||||
uri = JSON.parse(hp.decodeBase64Str(uri[1]));
|
uri = JSON.parse(hp.decodeBase64Str(uri[1]));
|
||||||
|
|
||||||
if (uri.v != '2')
|
if (uri.v != '2')
|
||||||
return null;
|
return null;
|
||||||
/* Unsupported protocols */
|
|
||||||
else if (uri.net === 'kcp')
|
else if (uri.net === 'kcp')
|
||||||
return null;
|
return null;
|
||||||
else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || !features.with_quic))
|
else if (uri.net === 'quic' && ((uri.type && uri.type !== 'none') || !features.with_quic))
|
||||||
return null;
|
return null;
|
||||||
/* https://www.v2fly.org/config/protocols/vmess.html#vmess-md5-%E8%AE%A4%E8%AF%81%E4%BF%A1%E6%81%AF-%E6%B7%98%E6%B1%B0%E6%9C%BA%E5%88%B6
|
|
||||||
* else if (uri.aid && parseInt(uri.aid) !== 0)
|
|
||||||
* return null;
|
|
||||||
*/
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
label: uri.ps,
|
label: uri.ps,
|
||||||
@@ -524,7 +491,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
}
|
}
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
/* Direct config */
|
|
||||||
o = s.option(form.ListValue, 'proxy_protocol', _('Proxy protocol'),
|
o = s.option(form.ListValue, 'proxy_protocol', _('Proxy protocol'),
|
||||||
_('Write proxy protocol in the connection header.'));
|
_('Write proxy protocol in the connection header.'));
|
||||||
o.value('', _('Disable'));
|
o.value('', _('Disable'));
|
||||||
@@ -533,7 +499,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.depends('type', 'direct');
|
o.depends('type', 'direct');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
/* AnyTLS config start */
|
|
||||||
o = s.option(form.Value, 'anytls_idle_session_check_interval', _('Idle session check interval'),
|
o = s.option(form.Value, 'anytls_idle_session_check_interval', _('Idle session check interval'),
|
||||||
_('Interval checking for idle sessions, in seconds.'));
|
_('Interval checking for idle sessions, in seconds.'));
|
||||||
o.datatype = 'uinteger';
|
o.datatype = 'uinteger';
|
||||||
@@ -554,9 +519,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.placeholder = '0';
|
o.placeholder = '0';
|
||||||
o.depends('type', 'anytls');
|
o.depends('type', 'anytls');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* AnyTLS config end */
|
|
||||||
|
|
||||||
/* Hysteria (2) config start */
|
|
||||||
o = s.option(form.DynamicList, 'hysteria_hopping_port', _('Hopping port'));
|
o = s.option(form.DynamicList, 'hysteria_hopping_port', _('Hopping port'));
|
||||||
o.depends('type', 'hysteria');
|
o.depends('type', 'hysteria');
|
||||||
o.depends('type', 'hysteria2');
|
o.depends('type', 'hysteria2');
|
||||||
@@ -580,10 +543,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
|
|
||||||
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
|
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
|
||||||
o.value('udp');
|
o.value('udp');
|
||||||
/* WeChat-Video / FakeTCP are unsupported by sing-box currently
|
|
||||||
* o.value('wechat-video');
|
|
||||||
* o.value('faketcp');
|
|
||||||
*/
|
|
||||||
o.default = 'udp';
|
o.default = 'udp';
|
||||||
o.depends('type', 'hysteria');
|
o.depends('type', 'hysteria');
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
@@ -655,13 +614,10 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
_('Disable Chrome QUIC handshake parroting, which is enabled by default since sing-box 1.14. Turn this on only when the server uses an Ed25519 certificate or the handshake otherwise fails.'));
|
_('Disable Chrome QUIC handshake parroting, which is enabled by default since sing-box 1.14. Turn this on only when the server uses an Ed25519 certificate or the handshake otherwise fails.'));
|
||||||
o.depends('type', 'hysteria2');
|
o.depends('type', 'hysteria2');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Hysteria (2) config end */
|
|
||||||
|
|
||||||
/* Shadowsocks config start */
|
|
||||||
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
||||||
for (let i of hp.shadowsocks_encrypt_methods)
|
for (let i of hp.shadowsocks_encrypt_methods)
|
||||||
o.value(i);
|
o.value(i);
|
||||||
/* Stream ciphers */
|
|
||||||
o.value('aes-128-ctr');
|
o.value('aes-128-ctr');
|
||||||
o.value('aes-192-ctr');
|
o.value('aes-192-ctr');
|
||||||
o.value('aes-256-ctr');
|
o.value('aes-256-ctr');
|
||||||
@@ -687,9 +643,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.depends('shadowsocks_plugin', 'obfs-local');
|
o.depends('shadowsocks_plugin', 'obfs-local');
|
||||||
o.depends('shadowsocks_plugin', 'v2ray-plugin');
|
o.depends('shadowsocks_plugin', 'v2ray-plugin');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Shadowsocks config end */
|
|
||||||
|
|
||||||
/* ShadowTLS config */
|
|
||||||
o = s.option(form.ListValue, 'shadowtls_version', _('ShadowTLS version'));
|
o = s.option(form.ListValue, 'shadowtls_version', _('ShadowTLS version'));
|
||||||
o.value('1', _('v1'));
|
o.value('1', _('v1'));
|
||||||
o.value('2', _('v2'));
|
o.value('2', _('v2'));
|
||||||
@@ -699,7 +653,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
/* Socks config */
|
|
||||||
o = s.option(form.ListValue, 'socks_version', _('Socks version'));
|
o = s.option(form.ListValue, 'socks_version', _('Socks version'));
|
||||||
o.value('4', _('Socks4'));
|
o.value('4', _('Socks4'));
|
||||||
o.value('4a', _('Socks4A'));
|
o.value('4a', _('Socks4A'));
|
||||||
@@ -709,7 +662,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
/* SSH config start */
|
|
||||||
o = s.option(form.Value, 'ssh_client_version', _('Client version'),
|
o = s.option(form.Value, 'ssh_client_version', _('Client version'),
|
||||||
_('Random version will be used if empty.'));
|
_('Random version will be used if empty.'));
|
||||||
o.depends('type', 'ssh');
|
o.depends('type', 'ssh');
|
||||||
@@ -733,9 +685,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.password = true;
|
o.password = true;
|
||||||
o.depends('type', 'ssh');
|
o.depends('type', 'ssh');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* SSH config end */
|
|
||||||
|
|
||||||
/* TUIC config start */
|
|
||||||
o = s.option(form.Value, 'uuid', _('UUID'));
|
o = s.option(form.Value, 'uuid', _('UUID'));
|
||||||
o.password = true;
|
o.password = true;
|
||||||
o.depends('type', 'tuic');
|
o.depends('type', 'tuic');
|
||||||
@@ -779,9 +729,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.default = '10';
|
o.default = '10';
|
||||||
o.depends('type', 'tuic');
|
o.depends('type', 'tuic');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Tuic config end */
|
|
||||||
|
|
||||||
/* VMess / VLESS config start */
|
|
||||||
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
|
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
|
||||||
o.value('', _('None'));
|
o.value('', _('None'));
|
||||||
o.value('xtls-rprx-vision');
|
o.value('xtls-rprx-vision');
|
||||||
@@ -816,13 +764,9 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
_('Protocol parameter. Enable length block encryption.'));
|
_('Protocol parameter. Enable length block encryption.'));
|
||||||
o.depends('type', 'vmess');
|
o.depends('type', 'vmess');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* VMess config end */
|
|
||||||
|
|
||||||
/* Transport config start */
|
|
||||||
hp.renderTransportOptions(s, { features: features, side: 'client' });
|
hp.renderTransportOptions(s, { features: features, side: 'client' });
|
||||||
/* Transport config end */
|
|
||||||
|
|
||||||
/* Wireguard config start */
|
|
||||||
o = s.option(form.DynamicList, 'wireguard_local_address', _('Local address'),
|
o = s.option(form.DynamicList, 'wireguard_local_address', _('Local address'),
|
||||||
_('List of IP (v4 or v6) addresses prefixes to be assigned to the interface.'));
|
_('List of IP (v4 or v6) addresses prefixes to be assigned to the interface.'));
|
||||||
o.datatype = 'cidr';
|
o.datatype = 'cidr';
|
||||||
@@ -868,9 +812,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.datatype = 'uinteger';
|
o.datatype = 'uinteger';
|
||||||
o.depends('type', 'wireguard');
|
o.depends('type', 'wireguard');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Wireguard config end */
|
|
||||||
|
|
||||||
/* Mux config start */
|
|
||||||
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
|
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
|
||||||
o.depends('type', 'shadowsocks');
|
o.depends('type', 'shadowsocks');
|
||||||
o.depends('type', 'trojan');
|
o.depends('type', 'trojan');
|
||||||
@@ -927,9 +869,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.datatype = 'uinteger';
|
o.datatype = 'uinteger';
|
||||||
o.depends('multiplex_brutal', '1');
|
o.depends('multiplex_brutal', '1');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Mux config end */
|
|
||||||
|
|
||||||
/* Snell config start */
|
|
||||||
o = s.option(form.ListValue, 'snell_version', _('Snell version'),
|
o = s.option(form.ListValue, 'snell_version', _('Snell version'),
|
||||||
_('sing-box implements Snell v4/v5 wire as v4 and v6. The pre-shared key (Password above) must be 12-255 bytes for v6.'));
|
_('sing-box implements Snell v4/v5 wire as v4 and v6. The pre-shared key (Password above) must be 12-255 bytes for v6.'));
|
||||||
o.value('4', _('v4'));
|
o.value('4', _('v4'));
|
||||||
@@ -967,9 +907,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.value('unsafe-raw', _('unsafe-raw'));
|
o.value('unsafe-raw', _('unsafe-raw'));
|
||||||
o.depends({'type': 'snell', 'snell_version': '6'});
|
o.depends({'type': 'snell', 'snell_version': '6'});
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Snell config end */
|
|
||||||
|
|
||||||
/* TLS config start */
|
|
||||||
hp.renderTlsOptions(s, {
|
hp.renderTlsOptions(s, {
|
||||||
side: 'client',
|
side: 'client',
|
||||||
type_depends: [ 'anytls', 'http', 'hysteria', 'hysteria2', 'shadowtls', 'trojan', 'tuic', 'vless', 'vmess' ],
|
type_depends: [ 'anytls', 'http', 'hysteria', 'hysteria2', 'shadowtls', 'trojan', 'tuic', 'vless', 'vmess' ],
|
||||||
@@ -1063,9 +1001,7 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.depends('tls_reality', '1');
|
o.depends('tls_reality', '1');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
}
|
}
|
||||||
/* TLS config end */
|
|
||||||
|
|
||||||
/* Extra settings start */
|
|
||||||
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'));
|
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'));
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
@@ -1088,7 +1024,6 @@ function renderNodeSettings(section, data, features, main_node, routing_mode) {
|
|||||||
o.default = '2';
|
o.default = '2';
|
||||||
o.depends('udp_over_tcp', '1');
|
o.depends('udp_over_tcp', '1');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Extra settings end */
|
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
@@ -1107,7 +1042,6 @@ return view.extend({
|
|||||||
let routing_mode = uci.get(data[0], 'config', 'routing_mode');
|
let routing_mode = uci.get(data[0], 'config', 'routing_mode');
|
||||||
let features = data[1];
|
let features = data[1];
|
||||||
|
|
||||||
/* Cache subscription information, it will be called multiple times */
|
|
||||||
let subinfo = [];
|
let subinfo = [];
|
||||||
for (let suburl of (uci.get(data[0], 'subscription', 'subscription_url') || [])) {
|
for (let suburl of (uci.get(data[0], 'subscription', 'subscription_url') || [])) {
|
||||||
const url = new URL(suburl);
|
const url = new URL(suburl);
|
||||||
@@ -1120,8 +1054,6 @@ return view.extend({
|
|||||||
|
|
||||||
s = m.section(form.NamedSection, 'subscription', 'homeproxy');
|
s = m.section(form.NamedSection, 'subscription', 'homeproxy');
|
||||||
|
|
||||||
/* Node settings start */
|
|
||||||
/* User nodes start */
|
|
||||||
s.tab('node', _('Nodes'));
|
s.tab('node', _('Nodes'));
|
||||||
o = s.taboption('node', form.SectionValue, '_node', form.GridSection, 'node');
|
o = s.taboption('node', form.SectionValue, '_node', form.GridSection, 'node');
|
||||||
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
|
ss = renderNodeSettings(o.subsection, data, features, main_node, routing_mode);
|
||||||
@@ -1133,8 +1065,6 @@ return view.extend({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/* Import subscription links start */
|
|
||||||
/* Thanks to luci-app-shadowsocks-libev */
|
|
||||||
ss.handleLinkImport = function() {
|
ss.handleLinkImport = function() {
|
||||||
let textarea = new ui.Textarea();
|
let textarea = new ui.Textarea();
|
||||||
ui.showModal(_('Import share links'), [
|
ui.showModal(_('Import share links'), [
|
||||||
@@ -1151,7 +1081,6 @@ return view.extend({
|
|||||||
click: ui.createHandlerFn(this, () => {
|
click: ui.createHandlerFn(this, () => {
|
||||||
let input_links = textarea.getValue().trim().split('\n');
|
let input_links = textarea.getValue().trim().split('\n');
|
||||||
if (input_links && input_links[0]) {
|
if (input_links && input_links[0]) {
|
||||||
/* Remove duplicate lines */
|
|
||||||
input_links = input_links.reduce((pre, cur) =>
|
input_links = input_links.reduce((pre, cur) =>
|
||||||
(!pre.includes(cur) && pre.push(cur), pre), []);
|
(!pre.includes(cur) && pre.push(cur), pre), []);
|
||||||
|
|
||||||
@@ -1163,9 +1092,6 @@ return view.extend({
|
|||||||
try {
|
try {
|
||||||
config = parseShareLink(l, features);
|
config = parseShareLink(l, features);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* a single unparsable link must not abort
|
|
||||||
the whole import (the changes made so
|
|
||||||
far were left unsaved before) */
|
|
||||||
config = null;
|
config = null;
|
||||||
}
|
}
|
||||||
if (config) {
|
if (config) {
|
||||||
@@ -1177,10 +1103,6 @@ return view.extend({
|
|||||||
let nameHash = hp.calcStringMD5(config.label);
|
let nameHash = hp.calcStringMD5(config.label);
|
||||||
let previous = uci.get(data[0], nameHash) || {};
|
let previous = uci.get(data[0], nameHash) || {};
|
||||||
let sid = uci.add(data[0], 'node', nameHash);
|
let sid = uci.add(data[0], 'node', nameHash);
|
||||||
/* drop options the node no longer has:
|
|
||||||
uci.set() only writes the keys below,
|
|
||||||
so re-importing a changed protocol
|
|
||||||
used to leave the old fields behind */
|
|
||||||
Object.keys(previous).forEach((k) => {
|
Object.keys(previous).forEach((k) => {
|
||||||
if (k.charAt(0) !== '.' && !(k in config))
|
if (k.charAt(0) !== '.' && !(k in config))
|
||||||
uci.unset(data[0], sid, k);
|
uci.unset(data[0], sid, k);
|
||||||
@@ -1239,10 +1161,7 @@ return view.extend({
|
|||||||
|
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
/* Import subscription links end */
|
|
||||||
/* User nodes end */
|
|
||||||
|
|
||||||
/* Subscription nodes start */
|
|
||||||
for (const info of subinfo) {
|
for (const info of subinfo) {
|
||||||
s.tab('sub_' + info.hash, _('Sub (%s)').format(info.title));
|
s.tab('sub_' + info.hash, _('Sub (%s)').format(info.title));
|
||||||
o = s.taboption('sub_' + info.hash, form.SectionValue, '_sub_' + info.hash, form.GridSection, 'node');
|
o = s.taboption('sub_' + info.hash, form.SectionValue, '_sub_' + info.hash, form.GridSection, 'node');
|
||||||
@@ -1251,10 +1170,7 @@ return view.extend({
|
|||||||
return (uci.get(data[0], section_id, 'grouphash') === info.hash);
|
return (uci.get(data[0], section_id, 'grouphash') === info.hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Subscription nodes end */
|
|
||||||
/* Node settings end */
|
|
||||||
|
|
||||||
/* Subscriptions settings start */
|
|
||||||
s.tab('subscription', _('Subscriptions'));
|
s.tab('subscription', _('Subscriptions'));
|
||||||
|
|
||||||
o = s.taboption('subscription', form.Flag, 'auto_update', _('Auto update'),
|
o = s.taboption('subscription', form.Flag, 'auto_update', _('Auto update'),
|
||||||
@@ -1383,7 +1299,6 @@ return view.extend({
|
|||||||
|
|
||||||
return this.map.save(null, true);
|
return this.map.save(null, true);
|
||||||
}
|
}
|
||||||
/* Subscriptions settings end */
|
|
||||||
|
|
||||||
return m.render();
|
return m.render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ function handleGenKey(option) {
|
|||||||
password = hp.generateRand('hex', 16);
|
password = hp.generateRand('hex', 16);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
/* AEAD */
|
|
||||||
((length) => {
|
((length) => {
|
||||||
if (length && length > 0)
|
if (length && length > 0)
|
||||||
password = hp.generateRand('base64', length);
|
password = hp.generateRand('base64', length);
|
||||||
@@ -242,19 +241,13 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
/* AnyTLS config */
|
|
||||||
o = s.option(form.DynamicList, 'anytls_padding_scheme', _('Padding scheme'),
|
o = s.option(form.DynamicList, 'anytls_padding_scheme', _('Padding scheme'),
|
||||||
_('AnyTLS padding scheme in array.'));
|
_('AnyTLS padding scheme in array.'));
|
||||||
o.depends('type', 'anytls');
|
o.depends('type', 'anytls');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
/* Hysteria (2) config start */
|
|
||||||
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
|
o = s.option(form.ListValue, 'hysteria_protocol', _('Protocol'));
|
||||||
o.value('udp');
|
o.value('udp');
|
||||||
/* WeChat-Video / FakeTCP are unsupported by sing-box currently
|
|
||||||
o.value('wechat-video');
|
|
||||||
o.value('faketcp');
|
|
||||||
*/
|
|
||||||
o.default = 'udp';
|
o.default = 'udp';
|
||||||
o.depends('type', 'hysteria');
|
o.depends('type', 'hysteria');
|
||||||
o.rmempty = false;
|
o.rmempty = false;
|
||||||
@@ -323,9 +316,7 @@ return view.extend({
|
|||||||
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
|
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
|
||||||
o.depends('type', 'hysteria2');
|
o.depends('type', 'hysteria2');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Hysteria (2) config end */
|
|
||||||
|
|
||||||
/* Snell config start */
|
|
||||||
o = s.option(form.ListValue, 'snell_version', _('Snell version'),
|
o = s.option(form.ListValue, 'snell_version', _('Snell version'),
|
||||||
_('The pre-shared key (password) above must be 12-255 bytes for v6.'));
|
_('The pre-shared key (password) above must be 12-255 bytes for v6.'));
|
||||||
o.value('5', _('v5'));
|
o.value('5', _('v5'));
|
||||||
@@ -348,9 +339,7 @@ return view.extend({
|
|||||||
o.value('unsafe-raw', _('unsafe-raw'));
|
o.value('unsafe-raw', _('unsafe-raw'));
|
||||||
o.depends({'type': 'snell', 'snell_version': '6'});
|
o.depends({'type': 'snell', 'snell_version': '6'});
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Snell config end */
|
|
||||||
|
|
||||||
/* Shadowsocks config */
|
|
||||||
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
o = s.option(form.ListValue, 'shadowsocks_encrypt_method', _('Encrypt method'));
|
||||||
for (let i of hp.shadowsocks_encrypt_methods)
|
for (let i of hp.shadowsocks_encrypt_methods)
|
||||||
o.value(i);
|
o.value(i);
|
||||||
@@ -358,7 +347,6 @@ return view.extend({
|
|||||||
o.depends('type', 'shadowsocks');
|
o.depends('type', 'shadowsocks');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
|
|
||||||
/* Tuic config start */
|
|
||||||
o = s.option(CBIGenValue, 'uuid', _('UUID'));
|
o = s.option(CBIGenValue, 'uuid', _('UUID'));
|
||||||
o.password = true;
|
o.password = true;
|
||||||
o.depends('type', 'tuic');
|
o.depends('type', 'tuic');
|
||||||
@@ -395,9 +383,7 @@ return view.extend({
|
|||||||
o.default = '10';
|
o.default = '10';
|
||||||
o.depends('type', 'tuic');
|
o.depends('type', 'tuic');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Tuic config end */
|
|
||||||
|
|
||||||
/* VLESS / VMess config start */
|
|
||||||
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
|
o = s.option(form.ListValue, 'vless_flow', _('Flow'));
|
||||||
o.value('', _('None'));
|
o.value('', _('None'));
|
||||||
o.value('xtls-rprx-vision');
|
o.value('xtls-rprx-vision');
|
||||||
@@ -409,13 +395,9 @@ return view.extend({
|
|||||||
o.datatype = 'uinteger';
|
o.datatype = 'uinteger';
|
||||||
o.depends('type', 'vmess');
|
o.depends('type', 'vmess');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* VMess config end */
|
|
||||||
|
|
||||||
/* Transport config start */
|
|
||||||
hp.renderTransportOptions(s, { features: features, side: 'server' });
|
hp.renderTransportOptions(s, { features: features, side: 'server' });
|
||||||
/* Transport config end */
|
|
||||||
|
|
||||||
/* Mux config start */
|
|
||||||
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
|
o = s.option(form.Flag, 'multiplex', _('Multiplex'));
|
||||||
o.depends('type', 'shadowsocks');
|
o.depends('type', 'shadowsocks');
|
||||||
o.depends('type', 'trojan');
|
o.depends('type', 'trojan');
|
||||||
@@ -445,9 +427,7 @@ return view.extend({
|
|||||||
o.depends('multiplex_brutal', '1');
|
o.depends('multiplex_brutal', '1');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
}
|
}
|
||||||
/* Mux config end */
|
|
||||||
|
|
||||||
/* TLS config start */
|
|
||||||
hp.renderTlsOptions(s, {
|
hp.renderTlsOptions(s, {
|
||||||
side: 'server',
|
side: 'server',
|
||||||
type_depends: [ 'anytls', 'http', 'hysteria', 'hysteria2', 'naive', 'trojan', 'tuic', 'vless', 'vmess' ],
|
type_depends: [ 'anytls', 'http', 'hysteria', 'hysteria2', 'naive', 'trojan', 'tuic', 'vless', 'vmess' ],
|
||||||
@@ -725,9 +705,7 @@ return view.extend({
|
|||||||
o.rows = 3;
|
o.rows = 3;
|
||||||
o.depends('tls', '1');
|
o.depends('tls', '1');
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* TLS config end */
|
|
||||||
|
|
||||||
/* Extra settings start */
|
|
||||||
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
|
o = s.option(form.Flag, 'tcp_fast_open', _('TCP fast open'),
|
||||||
_('Enable tcp fast open for listener.'));
|
_('Enable tcp fast open for listener.'));
|
||||||
o.depends({'network': 'udp', '!reverse': true});
|
o.depends({'network': 'udp', '!reverse': true});
|
||||||
@@ -766,7 +744,6 @@ return view.extend({
|
|||||||
o = s.option(form.Flag, 'reuse_addr', _('Reuse address'),
|
o = s.option(form.Flag, 'reuse_addr', _('Reuse address'),
|
||||||
_('Reuse listener address.'));
|
_('Reuse listener address.'));
|
||||||
o.modalonly = true;
|
o.modalonly = true;
|
||||||
/* Extra settings end */
|
|
||||||
|
|
||||||
return m.render();
|
return m.render();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,10 @@
|
|||||||
'require ui';
|
'require ui';
|
||||||
'require view';
|
'require view';
|
||||||
|
|
||||||
/* Thanks to luci-app-aria2 */
|
|
||||||
const css = ' \
|
const css = ' \
|
||||||
#log_textarea { \
|
|
||||||
padding: 10px; \
|
padding: 10px; \
|
||||||
text-align: left; \
|
text-align: left; \
|
||||||
} \
|
} \
|
||||||
#log_textarea pre { \
|
|
||||||
padding: .5rem; \
|
padding: .5rem; \
|
||||||
word-break: break-all; \
|
word-break: break-all; \
|
||||||
margin: 0; \
|
margin: 0; \
|
||||||
@@ -257,7 +254,6 @@ return view.extend({
|
|||||||
o.cfgvalue = L.bind(getResVersion, this, o, 'gfw_list');
|
o.cfgvalue = L.bind(getResVersion, this, o, 'gfw_list');
|
||||||
o.rawhtml = true;
|
o.rawhtml = true;
|
||||||
|
|
||||||
/* local rule-sets (used by bypass_mainland_china when present) */
|
|
||||||
o = s.option(form.DummyValue, '_geoip_cn_version', _('GeoIP CN rule-set version'));
|
o = s.option(form.DummyValue, '_geoip_cn_version', _('GeoIP CN rule-set version'));
|
||||||
o.cfgvalue = L.bind(getResVersion, this, o, 'geoip_cn');
|
o.cfgvalue = L.bind(getResVersion, this, o, 'geoip_cn');
|
||||||
o.rawhtml = true;
|
o.rawhtml = true;
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ if (match(proxy_mode, /tun/)) {
|
|||||||
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
||||||
|
|
||||||
if (outbound_node !== 'nil') {
|
if (outbound_node !== 'nil') {
|
||||||
/* the name is interpolated into an nftables rule: keep it to what an
|
|
||||||
interface name can actually be */
|
|
||||||
const raw_tun = trim(uci.get(cfgname, 'infra', 'tun_name') ?? '');
|
const raw_tun = trim(uci.get(cfgname, 'infra', 'tun_name') ?? '');
|
||||||
tun_name = match(raw_tun, /^[A-Za-z0-9._-]{1,15}$/) ? raw_tun : 'singtun0';
|
tun_name = match(raw_tun, /^[A-Za-z0-9._-]{1,15}$/) ? raw_tun : 'singtun0';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,6 @@
|
|||||||
# SPDX-License-Identifier: GPL-2.0-only
|
# SPDX-License-Identifier: GPL-2.0-only
|
||||||
#
|
#
|
||||||
# Copyright (C) 2025 ImmortalWrt.org
|
# 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="
|
HP_FW4_CHAINS="
|
||||||
homeproxy_output_redir
|
homeproxy_output_redir
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ import {
|
|||||||
|
|
||||||
const ubus = connect();
|
const ubus = connect();
|
||||||
|
|
||||||
/* const features = ubus.call('luci.homeproxy', 'singbox_get_features') || {}; */
|
|
||||||
|
|
||||||
/* UCI config start */
|
|
||||||
const uci = cursor();
|
const uci = cursor();
|
||||||
|
|
||||||
const uciconfig = 'homeproxy';
|
const uciconfig = 'homeproxy';
|
||||||
@@ -45,17 +43,6 @@ const uciruleset = 'ruleset';
|
|||||||
|
|
||||||
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
|
const routing_mode = uci.get(uciconfig, ucimain, 'routing_mode') || 'bypass_mainland_china';
|
||||||
|
|
||||||
/*
|
|
||||||
* Single source of truth for "which config branch are we generating".
|
|
||||||
*
|
|
||||||
* The three generation chains below (DNS, outbounds, route) must be selected by
|
|
||||||
* the routing mode, NOT by whether main_node / default_outbound happen to be
|
|
||||||
* set. Those two answer a different question: in custom mode default_outbound
|
|
||||||
* is legitimately 'nil' (every routing rule picks its own outbound, there is no
|
|
||||||
* default) and main_node is never read. Deriving the mode from them made a
|
|
||||||
* perfectly normal custom config fall through every branch and emit a config
|
|
||||||
* containing none of the user's DNS servers, nodes, rules or rule-sets.
|
|
||||||
*/
|
|
||||||
const is_custom_mode = (routing_mode === 'custom');
|
const is_custom_mode = (routing_mode === 'custom');
|
||||||
|
|
||||||
let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']?.[0];
|
let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']?.[0];
|
||||||
@@ -96,14 +83,12 @@ if (routing_mode !== 'custom') {
|
|||||||
proxy_domain_list = proxy_list_raw ? split(trim(proxy_list_raw), /[\r\n]/) : [];
|
proxy_domain_list = proxy_list_raw ? split(trim(proxy_list_raw), /[\r\n]/) : [];
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
/* DNS settings */
|
|
||||||
dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy');
|
dns_default_strategy = uci.get(uciconfig, ucidnssetting, 'default_strategy');
|
||||||
dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server');
|
dns_default_server = uci.get(uciconfig, ucidnssetting, 'default_server');
|
||||||
dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache');
|
dns_disable_cache = uci.get(uciconfig, ucidnssetting, 'disable_cache');
|
||||||
dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
|
dns_disable_cache_expire = uci.get(uciconfig, ucidnssetting, 'disable_cache_expire');
|
||||||
dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
|
dns_client_subnet = uci.get(uciconfig, ucidnssetting, 'client_subnet');
|
||||||
|
|
||||||
/* Routing settings */
|
|
||||||
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
|
default_outbound = uci.get(uciconfig, uciroutingsetting, 'default_outbound') || 'nil';
|
||||||
default_outbound_dns = uci.get(uciconfig, uciroutingsetting, 'default_outbound_dns') || 'default-dns';
|
default_outbound_dns = uci.get(uciconfig, uciroutingsetting, 'default_outbound_dns') || 'default-dns';
|
||||||
domain_strategy = uci.get(uciconfig, uciroutingsetting, 'domain_strategy');
|
domain_strategy = uci.get(uciconfig, uciroutingsetting, 'domain_strategy');
|
||||||
@@ -148,7 +133,6 @@ if (match(proxy_mode, /tun/)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn';
|
const log_level = uci.get(uciconfig, ucimain, 'log_level') || 'warn';
|
||||||
/* UCI config end */
|
|
||||||
|
|
||||||
const tun_dns_mode_raw = uci.get(uciconfig, ucimain, 'tun_dns_mode'),
|
const tun_dns_mode_raw = uci.get(uciconfig, ucimain, 'tun_dns_mode'),
|
||||||
tun_dns_address = uci.get(uciconfig, ucimain, 'tun_dns_address'),
|
tun_dns_address = uci.get(uciconfig, ucimain, 'tun_dns_address'),
|
||||||
@@ -160,13 +144,6 @@ const tun_dns_mode = (tun_dns_mode_raw === 'default') ? '' : tun_dns_mode_raw,
|
|||||||
udp_mapping = (udp_mapping_raw === 'default') ? '' : udp_mapping_raw,
|
udp_mapping = (udp_mapping_raw === 'default') ? '' : udp_mapping_raw,
|
||||||
udp_filtering = (udp_filtering_raw === 'default') ? '' : udp_filtering_raw;
|
udp_filtering = (udp_filtering_raw === 'default') ? '' : udp_filtering_raw;
|
||||||
|
|
||||||
/* Config helper start */
|
|
||||||
/*
|
|
||||||
* Direct-node destination override, keyed by node section name. It must be
|
|
||||||
* declared before the helpers that touch it: ucode resolves let/const
|
|
||||||
* lexically and does not hoist them, so a function defined earlier would
|
|
||||||
* resolve the name as an undeclared global and throw under strict mode.
|
|
||||||
*/
|
|
||||||
const direct_overrides = {};
|
const direct_overrides = {};
|
||||||
|
|
||||||
function parse_port(strport) {
|
function parse_port(strport) {
|
||||||
@@ -256,28 +233,22 @@ function generate_outbound(node) {
|
|||||||
|
|
||||||
server: node.address,
|
server: node.address,
|
||||||
server_port: strToInt(node.port),
|
server_port: strToInt(node.port),
|
||||||
/* Hysteria(2) */
|
|
||||||
server_ports: node.hysteria_hopping_port,
|
server_ports: node.hysteria_hopping_port,
|
||||||
|
|
||||||
username: (node.type !== 'ssh') ? node.username : null,
|
username: (node.type !== 'ssh') ? node.username : null,
|
||||||
user: (node.type === 'ssh') ? node.username : null,
|
user: (node.type === 'ssh') ? node.username : null,
|
||||||
/* Snell authenticates with psk instead of password */
|
|
||||||
password: (node.type !== 'snell') ? node.password : null,
|
password: (node.type !== 'snell') ? node.password : null,
|
||||||
psk: (node.type === 'snell') ? node.password : null,
|
psk: (node.type === 'snell') ? node.password : null,
|
||||||
userkey: (node.type === 'snell') ? node.snell_userkey : null,
|
userkey: (node.type === 'snell') ? node.snell_userkey : null,
|
||||||
reuse: (node.type === 'snell') ? strToBool(node.snell_reuse) : null,
|
reuse: (node.type === 'snell') ? strToBool(node.snell_reuse) : null,
|
||||||
/* Snell v4: HTTP obfuscation; v6: traffic shaping mode */
|
|
||||||
obfs_mode: (node.type === 'snell') ? (node.snell_obfs_mode || null) : null,
|
obfs_mode: (node.type === 'snell') ? (node.snell_obfs_mode || null) : null,
|
||||||
obfs_host: (node.type === 'snell') ? (node.snell_obfs_host || null) : null,
|
obfs_host: (node.type === 'snell') ? (node.snell_obfs_host || null) : null,
|
||||||
mode: (node.type === 'snell') ? (node.snell_mode || null) : null,
|
mode: (node.type === 'snell') ? (node.snell_mode || null) : null,
|
||||||
|
|
||||||
/* Direct */
|
|
||||||
proxy_protocol: strToInt(node.proxy_protocol),
|
proxy_protocol: strToInt(node.proxy_protocol),
|
||||||
/* AnyTLS */
|
|
||||||
idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval),
|
idle_session_check_interval: strToTime(node.anytls_idle_session_check_interval),
|
||||||
idle_session_timeout: strToTime(node.anytls_idle_session_timeout),
|
idle_session_timeout: strToTime(node.anytls_idle_session_timeout),
|
||||||
min_idle_session: strToInt(node.anytls_min_idle_session),
|
min_idle_session: strToInt(node.anytls_min_idle_session),
|
||||||
/* Hysteria (2) */
|
|
||||||
hop_interval: strToTime(node.hysteria_hop_interval),
|
hop_interval: strToTime(node.hysteria_hop_interval),
|
||||||
hop_interval_max: strToTime(node.hysteria_hop_interval_max),
|
hop_interval_max: strToTime(node.hysteria_hop_interval_max),
|
||||||
up_mbps: strToInt(node.hysteria_up_mbps),
|
up_mbps: strToInt(node.hysteria_up_mbps),
|
||||||
@@ -290,29 +261,23 @@ function generate_outbound(node) {
|
|||||||
} : node.hysteria_obfs_password,
|
} : node.hysteria_obfs_password,
|
||||||
auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null,
|
auth: (node.hysteria_auth_type === 'base64') ? node.hysteria_auth_payload : null,
|
||||||
auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null,
|
auth_str: (node.hysteria_auth_type === 'string') ? node.hysteria_auth_payload : null,
|
||||||
/* sing-box 1.14: Hysteria2 QUIC params (Hysteria v1 recv-window tuning removed upstream) */
|
|
||||||
bbr_profile: (node.type === 'hysteria2') ? (node.hysteria_bbr_profile || null) : null,
|
bbr_profile: (node.type === 'hysteria2') ? (node.hysteria_bbr_profile || null) : null,
|
||||||
disable_chrome_parrot: (node.type === 'hysteria2' && node.hysteria_disable_chrome_parrot === '1') ? true : null,
|
disable_chrome_parrot: (node.type === 'hysteria2' && node.hysteria_disable_chrome_parrot === '1') ? true : null,
|
||||||
/* Shadowsocks */
|
|
||||||
method: node.shadowsocks_encrypt_method,
|
method: node.shadowsocks_encrypt_method,
|
||||||
plugin: node.shadowsocks_plugin,
|
plugin: node.shadowsocks_plugin,
|
||||||
plugin_opts: node.shadowsocks_plugin_opts,
|
plugin_opts: node.shadowsocks_plugin_opts,
|
||||||
/* ShadowTLS / Socks / Snell */
|
|
||||||
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : ((node.type === 'snell') ? (strToInt(node.snell_version) || 4) : null)),
|
version: (node.type === 'shadowtls') ? strToInt(node.shadowtls_version) : ((node.type === 'socks') ? node.socks_version : ((node.type === 'snell') ? (strToInt(node.snell_version) || 4) : null)),
|
||||||
/* SSH */
|
|
||||||
client_version: node.ssh_client_version,
|
client_version: node.ssh_client_version,
|
||||||
host_key: node.ssh_host_key,
|
host_key: node.ssh_host_key,
|
||||||
host_key_algorithms: node.ssh_host_key_algo,
|
host_key_algorithms: node.ssh_host_key_algo,
|
||||||
private_key: node.ssh_priv_key,
|
private_key: node.ssh_priv_key,
|
||||||
private_key_passphrase: node.ssh_priv_key_pp,
|
private_key_passphrase: node.ssh_priv_key_pp,
|
||||||
/* Tuic */
|
|
||||||
uuid: node.uuid,
|
uuid: node.uuid,
|
||||||
congestion_control: node.tuic_congestion_control,
|
congestion_control: node.tuic_congestion_control,
|
||||||
udp_relay_mode: node.tuic_udp_relay_mode,
|
udp_relay_mode: node.tuic_udp_relay_mode,
|
||||||
udp_over_stream: strToBool(node.tuic_udp_over_stream),
|
udp_over_stream: strToBool(node.tuic_udp_over_stream),
|
||||||
zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt),
|
zero_rtt_handshake: strToBool(node.tuic_enable_zero_rtt),
|
||||||
heartbeat: strToTime(node.tuic_heartbeat),
|
heartbeat: strToTime(node.tuic_heartbeat),
|
||||||
/* VLESS / VMess */
|
|
||||||
flow: node.vless_flow,
|
flow: node.vless_flow,
|
||||||
alter_id: strToInt(node.vmess_alterid),
|
alter_id: strToInt(node.vmess_alterid),
|
||||||
security: node.vmess_encrypt,
|
security: node.vmess_encrypt,
|
||||||
@@ -344,8 +309,6 @@ function generate_outbound(node) {
|
|||||||
udp_fragment: strToBool(node.udp_fragment)
|
udp_fragment: strToBool(node.udp_fragment)
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Direct-node destination override: sing-box removed these options from
|
|
||||||
the direct outbound since 1.13; emit them via the route action instead */
|
|
||||||
if (node.type === 'direct' && (!isEmpty(node.override_address) || !isEmpty(node.override_port)))
|
if (node.type === 'direct' && (!isEmpty(node.override_address) || !isEmpty(node.override_port)))
|
||||||
direct_overrides[node['.name']] = {
|
direct_overrides[node['.name']] = {
|
||||||
override_address: node.override_address,
|
override_address: node.override_address,
|
||||||
@@ -432,11 +395,6 @@ function isDirectOutboundTag(tag) {
|
|||||||
return !isEmpty(node) && node.type === 'direct';
|
return !isEmpty(node) && node.type === 'direct';
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Remote rule-sets are downloaded through a top-level http_client in sing-box
|
|
||||||
* 1.14 (the per-rule-set download_detour was removed). One client is created
|
|
||||||
* per distinct detour and reused by every rule-set that dials through it.
|
|
||||||
*/
|
|
||||||
const http_clients = [];
|
const http_clients = [];
|
||||||
const http_seen = {};
|
const http_seen = {};
|
||||||
|
|
||||||
@@ -450,9 +408,6 @@ function http_client_for(detour) {
|
|||||||
|
|
||||||
http_seen[detour] = true;
|
http_seen[detour] = true;
|
||||||
const client = { tag: tag };
|
const client = { tag: tag };
|
||||||
/* sing-box 1.14 rejects an empty detour on the direct outbound (pure TUN
|
|
||||||
mode has no self_mark on it); omitting detour uses the same system
|
|
||||||
direct dialer, so behaviour is unchanged. */
|
|
||||||
if (!(isEmpty(self_mark) && isDirectOutboundTag(detour)))
|
if (!(isEmpty(self_mark) && isDirectOutboundTag(detour)))
|
||||||
client.detour = detour;
|
client.detour = detour;
|
||||||
|
|
||||||
@@ -461,10 +416,6 @@ function http_client_for(detour) {
|
|||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* sing-box requires dns.servers[].headers to be a map, while the LuCI field is
|
|
||||||
* a dynamic list of "Name: value" lines (and older configs may hold either).
|
|
||||||
*/
|
|
||||||
function parse_headers(headers) {
|
function parse_headers(headers) {
|
||||||
if (isEmpty(headers))
|
if (isEmpty(headers))
|
||||||
return null;
|
return null;
|
||||||
@@ -482,11 +433,9 @@ function parse_headers(headers) {
|
|||||||
|
|
||||||
return isEmpty(out) ? null : out;
|
return isEmpty(out) ? null : out;
|
||||||
}
|
}
|
||||||
/* Config helper end */
|
|
||||||
|
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
/* Log */
|
|
||||||
config.log = {
|
config.log = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
level: log_level,
|
level: log_level,
|
||||||
@@ -494,7 +443,6 @@ config.log = {
|
|||||||
timestamp: true
|
timestamp: true
|
||||||
};
|
};
|
||||||
|
|
||||||
/* NTP */
|
|
||||||
if (!isEmpty(ntp_server))
|
if (!isEmpty(ntp_server))
|
||||||
config.ntp = {
|
config.ntp = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -503,8 +451,6 @@ if (!isEmpty(ntp_server))
|
|||||||
domain_resolver: 'default-dns',
|
domain_resolver: 'default-dns',
|
||||||
};
|
};
|
||||||
|
|
||||||
/* DNS start */
|
|
||||||
/* Default settings */
|
|
||||||
config.dns = {
|
config.dns = {
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
@@ -532,7 +478,6 @@ config.dns = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!isEmpty(main_node)) {
|
if (!isEmpty(main_node)) {
|
||||||
/* Main DNS */
|
|
||||||
push(config.dns.servers, {
|
push(config.dns.servers, {
|
||||||
tag: 'main-dns',
|
tag: 'main-dns',
|
||||||
domain_resolver: {
|
domain_resolver: {
|
||||||
@@ -551,8 +496,6 @@ if (!isEmpty(main_node)) {
|
|||||||
server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns'
|
server: (routing_mode === 'bypass_mainland_china') ? 'china-dns' : 'default-dns'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Reject SVCB/HTTPS queries to avoid proxy DNS timeout on null domains.
|
|
||||||
Opt out with `block_https_rr 0` when clients should use HTTPS RR/ECH. */
|
|
||||||
if (uci.get(uciconfig, ucimain, 'block_https_rr') !== '0')
|
if (uci.get(uciconfig, ucimain, 'block_https_rr') !== '0')
|
||||||
push(config.dns.rules, {
|
push(config.dns.rules, {
|
||||||
query_type: [64, 65],
|
query_type: [64, 65],
|
||||||
@@ -564,18 +507,12 @@ if (!isEmpty(main_node)) {
|
|||||||
tag: 'china-dns',
|
tag: 'china-dns',
|
||||||
domain_resolver: {
|
domain_resolver: {
|
||||||
server: 'default-dns',
|
server: 'default-dns',
|
||||||
/* follow the IPv6 setting like main-dns does, instead of
|
|
||||||
preferring AAAA even on IPv4-only setups */
|
|
||||||
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null
|
strategy: (ipv6_support !== '1') ? 'ipv4_only' : null
|
||||||
},
|
},
|
||||||
detour: self_mark ? 'direct-out' : null,
|
detour: self_mark ? 'direct-out' : null,
|
||||||
...parse_dnsserver(china_dns_server)
|
...parse_dnsserver(china_dns_server)
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Route NAPTR (qtype 35) queries for SIP/ENUM domains to the ISP
|
|
||||||
default-dns directly: china-dns (223.5.5.5) has intermittent
|
|
||||||
multi-second first responses for NAPTR, causing 'context deadline
|
|
||||||
exceeded' (e.g. sipgz12.hbq.r.10086.cn) */
|
|
||||||
push(config.dns.rules, {
|
push(config.dns.rules, {
|
||||||
query_type: [35],
|
query_type: [35],
|
||||||
domain_suffix: ['r.10086.cn', '10086.cn', 'pub.3gppnetwork.org'],
|
domain_suffix: ['r.10086.cn', '10086.cn', 'pub.3gppnetwork.org'],
|
||||||
@@ -596,7 +533,6 @@ if (!isEmpty(main_node)) {
|
|||||||
server: 'china-dns'
|
server: 'china-dns'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* sing-box 1.14: restore CN-IP fallback via evaluate/match_response (opt-in) */
|
|
||||||
if (uci.get(uciconfig, ucimain, 'cn_ip_fallback') === '1') {
|
if (uci.get(uciconfig, ucimain, 'cn_ip_fallback') === '1') {
|
||||||
push(config.dns.rules, {
|
push(config.dns.rules, {
|
||||||
action: 'evaluate',
|
action: 'evaluate',
|
||||||
@@ -612,7 +548,6 @@ if (!isEmpty(main_node)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (is_custom_mode) {
|
} else if (is_custom_mode) {
|
||||||
/* DNS servers */
|
|
||||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||||
if (cfg.enabled !== '1')
|
if (cfg.enabled !== '1')
|
||||||
return;
|
return;
|
||||||
@@ -640,9 +575,6 @@ if (!isEmpty(main_node)) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* DNS rules */
|
|
||||||
/* sing-box >= 1.14: legacy address-filter rules are auto-wrapped with an
|
|
||||||
evaluate action; deprecated strategy/accept_empty fields are dropped. */
|
|
||||||
const builtin_dns_rules = [];
|
const builtin_dns_rules = [];
|
||||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||||
if (cfg.enabled !== '1')
|
if (cfg.enabled !== '1')
|
||||||
@@ -705,10 +637,6 @@ if (!isEmpty(main_node)) {
|
|||||||
|
|
||||||
const legacy_filter = !isEmpty(cfg.ip_cidr) || strToBool(cfg.ip_is_private) === true;
|
const legacy_filter = !isEmpty(cfg.ip_cidr) || strToBool(cfg.ip_is_private) === true;
|
||||||
if (legacy_filter && !rule.match_response && cfg.action === 'route') {
|
if (legacy_filter && !rule.match_response && cfg.action === 'route') {
|
||||||
/* Wrap a legacy address-filter rule into the 1.14 evaluate/match_response
|
|
||||||
paradigm. Carry the original query-matching fields onto the evaluate
|
|
||||||
prefix rule so only queries that would have hit this rule get
|
|
||||||
pre-resolved; an unconditional evaluate would resolve every query. */
|
|
||||||
const eval_tag = '_hp_eval_' + cfg['.name'];
|
const eval_tag = '_hp_eval_' + cfg['.name'];
|
||||||
const eval_rule = {
|
const eval_rule = {
|
||||||
action: 'evaluate',
|
action: 'evaluate',
|
||||||
@@ -730,14 +658,11 @@ if (!isEmpty(main_node)) {
|
|||||||
rule.match_response = eval_tag;
|
rule.match_response = eval_tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ip_cidr / ip_is_private are only valid with match_response in 1.14 */
|
|
||||||
if (rule.match_response) {
|
if (rule.match_response) {
|
||||||
rule.ip_cidr = cfg.ip_cidr;
|
rule.ip_cidr = cfg.ip_cidr;
|
||||||
rule.ip_is_private = strToBool(cfg.ip_is_private);
|
rule.ip_is_private = strToBool(cfg.ip_is_private);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sing-box rejects the whole config when a route/evaluate action has no
|
|
||||||
server, so drop such a rule with a reason instead. */
|
|
||||||
if ((rule.action === 'route' || rule.action === 'evaluate') && isEmpty(rule.server)) {
|
if ((rule.action === 'route' || rule.action === 'evaluate') && isEmpty(rule.server)) {
|
||||||
warn(sprintf("homeproxy: DNS rule '%s' has no server configured, skipping it.", cfg['.name']));
|
warn(sprintf("homeproxy: DNS rule '%s' has no server configured, skipping it.", cfg['.name']));
|
||||||
return;
|
return;
|
||||||
@@ -749,19 +674,9 @@ if (!isEmpty(main_node)) {
|
|||||||
|
|
||||||
config.dns.final = get_resolver(dns_default_server);
|
config.dns.final = get_resolver(dns_default_server);
|
||||||
}
|
}
|
||||||
/* DNS end */
|
|
||||||
|
|
||||||
/* Inbound start */
|
|
||||||
config.inbounds = [];
|
config.inbounds = [];
|
||||||
|
|
||||||
/*
|
|
||||||
* dns-in is the local listener the DNS chain hands queries to: dnsmasq
|
|
||||||
* forwards to 127.0.0.1#<dns_port> and the nft DNS hijack redirects to
|
|
||||||
* dnsmasq, not here. It used to listen on '::', which published a
|
|
||||||
* proxy-backed resolver to every LAN client on the DNS port; loopback is the
|
|
||||||
* whole reachable surface it needs. mixed-in stays on '::' - that one is the
|
|
||||||
* SOCKS/HTTP listener clients are meant to reach.
|
|
||||||
*/
|
|
||||||
push(config.inbounds, {
|
push(config.inbounds, {
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
tag: 'dns-in',
|
tag: 'dns-in',
|
||||||
@@ -817,12 +732,9 @@ if (match(proxy_mode, /tun/))
|
|||||||
udp_nat_max: udp_nat_max,
|
udp_nat_max: udp_nat_max,
|
||||||
stack: tcpip_stack,
|
stack: tcpip_stack,
|
||||||
});
|
});
|
||||||
/* Inbound end */
|
|
||||||
|
|
||||||
/* Outbound start */
|
|
||||||
config.endpoints = [];
|
config.endpoints = [];
|
||||||
|
|
||||||
/* Default outbounds */
|
|
||||||
config.outbounds = [
|
config.outbounds = [
|
||||||
{
|
{
|
||||||
type: 'direct',
|
type: 'direct',
|
||||||
@@ -835,7 +747,6 @@ config.outbounds = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
/* Main outbounds */
|
|
||||||
if (!isEmpty(main_node)) {
|
if (!isEmpty(main_node)) {
|
||||||
let urltest_nodes = [];
|
let urltest_nodes = [];
|
||||||
|
|
||||||
@@ -958,16 +869,7 @@ if (!isEmpty(main_node)) {
|
|||||||
|
|
||||||
if (isEmpty(config.endpoints))
|
if (isEmpty(config.endpoints))
|
||||||
config.endpoints = null;
|
config.endpoints = null;
|
||||||
/* Outbound end */
|
|
||||||
|
|
||||||
/* Routing rules start */
|
|
||||||
/* Default settings */
|
|
||||||
/*
|
|
||||||
* The sniff rule below deliberately carries no `timeout`. Reports from
|
|
||||||
* sing-box 1.14.2 show it rejected with "route.rules[1].timeout: json: unknown
|
|
||||||
* field". The official builds accept it, so the exact trigger is still open,
|
|
||||||
* but the default is already 300ms and omitting the field is behaviour-neutral.
|
|
||||||
*/
|
|
||||||
config.route = {
|
config.route = {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
@@ -983,19 +885,12 @@ config.route = {
|
|||||||
default_interface: default_interface
|
default_interface: default_interface
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Routing rules */
|
|
||||||
if (!isEmpty(main_node)) {
|
if (!isEmpty(main_node)) {
|
||||||
/* Resolve outbound server domains through the WAN default resolver.
|
|
||||||
Do not use china-dns here: china-dns is for resolving mainland China
|
|
||||||
destinations, not the proxy node itself. Coupling node bootstrap to
|
|
||||||
china_dns_server can break dialing when that resolver is polluted or
|
|
||||||
unsuitable for the node domain. */
|
|
||||||
config.route.default_domain_resolver = {
|
config.route.default_domain_resolver = {
|
||||||
server: 'default-dns',
|
server: 'default-dns',
|
||||||
strategy: (ipv6_support !== '1') ? 'prefer_ipv4' : null
|
strategy: (ipv6_support !== '1') ? 'prefer_ipv4' : null
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Direct list */
|
|
||||||
if (length(direct_domain_list))
|
if (length(direct_domain_list))
|
||||||
push(config.route.rules, {
|
push(config.route.rules, {
|
||||||
rule_set: 'direct-domain',
|
rule_set: 'direct-domain',
|
||||||
@@ -1003,14 +898,17 @@ if (!isEmpty(main_node)) {
|
|||||||
outbound: 'direct-out'
|
outbound: 'direct-out'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Bypass CN traffic: resolve the destination first, then route by IP.
|
if (dedicated_udp_node) {
|
||||||
sing-box does not match an IP-based rule set (geoip-cn) against a domain
|
const udp_override = direct_overrides[main_udp_node] || null;
|
||||||
destination unless it is resolved first, so add an explicit resolve
|
push(config.route.rules, {
|
||||||
action; geoip-cn then sends China IPs to direct and everything else falls
|
network: 'udp',
|
||||||
through to main-out (proxy). This avoids relying on the geosite-* domain
|
action: 'route',
|
||||||
lists, which can mis-classify foreign domains (e.g. Google's gvt2.com
|
outbound: 'main-udp-out',
|
||||||
beacons) as "cn" and send them direct to time out. Keep the direct-domain
|
override_address: udp_override ? udp_override.override_address : null,
|
||||||
fast-path above for known direct domains. */
|
override_port: udp_override ? udp_override.override_port : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (routing_mode === 'bypass_mainland_china') {
|
if (routing_mode === 'bypass_mainland_china') {
|
||||||
push(config.route.rules, {
|
push(config.route.rules, {
|
||||||
action: 'resolve',
|
action: 'resolve',
|
||||||
@@ -1023,20 +921,6 @@ if (!isEmpty(main_node)) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main UDP out */
|
|
||||||
if (dedicated_udp_node) {
|
|
||||||
const udp_override = direct_overrides[main_udp_node] || null;
|
|
||||||
push(config.route.rules, {
|
|
||||||
network: 'udp',
|
|
||||||
action: 'route',
|
|
||||||
outbound: 'main-udp-out',
|
|
||||||
override_address: udp_override ? udp_override.override_address : null,
|
|
||||||
override_port: udp_override ? udp_override.override_port : null
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Direct-node destination override, emitted as route-options action
|
|
||||||
(direct outbound options removed since sing-box 1.13) */
|
|
||||||
const main_override = direct_overrides[main_node] || null;
|
const main_override = direct_overrides[main_node] || null;
|
||||||
if (main_override)
|
if (main_override)
|
||||||
push(config.route.rules, {
|
push(config.route.rules, {
|
||||||
@@ -1047,8 +931,6 @@ if (!isEmpty(main_node)) {
|
|||||||
|
|
||||||
config.route.final = 'main-out';
|
config.route.final = 'main-out';
|
||||||
|
|
||||||
/* Rule set */
|
|
||||||
/* Direct list */
|
|
||||||
if (length(direct_domain_list))
|
if (length(direct_domain_list))
|
||||||
push(config.route.rule_set, {
|
push(config.route.rule_set, {
|
||||||
type: 'inline',
|
type: 'inline',
|
||||||
@@ -1060,7 +942,6 @@ if (!isEmpty(main_node)) {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Proxy list */
|
|
||||||
if (length(proxy_domain_list))
|
if (length(proxy_domain_list))
|
||||||
push(config.route.rule_set, {
|
push(config.route.rule_set, {
|
||||||
type: 'inline',
|
type: 'inline',
|
||||||
@@ -1073,17 +954,6 @@ if (!isEmpty(main_node)) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (routing_mode === 'bypass_mainland_china') {
|
if (routing_mode === 'bypass_mainland_china') {
|
||||||
/*
|
|
||||||
* The two rule-sets sing-box actually needs for this mode. Each one is
|
|
||||||
* used from the local .srs file when update_resources.sh has fetched
|
|
||||||
* it, and only falls back to a remote fetch otherwise: a remote
|
|
||||||
* rule-set is initialized at every start (sing-box fails to start when
|
|
||||||
* the fetch fails even if nothing references it), which would make a
|
|
||||||
* cold boot depend on reaching the CDN through the node. The fallback
|
|
||||||
* keeps a fresh install working before the first resource update.
|
|
||||||
* geosite-noncn is intentionally not declared: no rule references it,
|
|
||||||
* and sing-box downloads it anyway.
|
|
||||||
*/
|
|
||||||
const preset_rulesets = [
|
const preset_rulesets = [
|
||||||
{
|
{
|
||||||
tag: 'geoip-cn',
|
tag: 'geoip-cn',
|
||||||
@@ -1147,11 +1017,6 @@ if (!isEmpty(main_node)) {
|
|||||||
rule_override_port = rule_direct_override.override_port;
|
rule_override_port = rule_direct_override.override_port;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* sing-box routing rule: emit match fields plus action-specific fields.
|
|
||||||
client is the sniffed client type (only set with protocol quic/ssh);
|
|
||||||
resolve adds server/strategy/disable_cache/rewrite_ttl/client_subnet;
|
|
||||||
reject adds method/no_drop. Emitted conditionally so a given action
|
|
||||||
never carries fields sing-box rejects for it. */
|
|
||||||
const rule = {
|
const rule = {
|
||||||
ip_version: strToInt(cfg.ip_version),
|
ip_version: strToInt(cfg.ip_version),
|
||||||
protocol: cfg.protocol,
|
protocol: cfg.protocol,
|
||||||
@@ -1207,8 +1072,6 @@ if (!isEmpty(main_node)) {
|
|||||||
push(config.route.rules, rule);
|
push(config.route.rules, rule);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Direct-node destination override, emitted as route-options action
|
|
||||||
(direct outbound options removed since sing-box 1.13) */
|
|
||||||
const final_override = get_direct_override(default_outbound);
|
const final_override = get_direct_override(default_outbound);
|
||||||
if (final_override)
|
if (final_override)
|
||||||
push(config.route.rules, {
|
push(config.route.rules, {
|
||||||
@@ -1219,7 +1082,6 @@ if (!isEmpty(main_node)) {
|
|||||||
|
|
||||||
config.route.final = get_outbound(default_outbound);
|
config.route.final = get_outbound(default_outbound);
|
||||||
|
|
||||||
/* Rule set */
|
|
||||||
uci.foreach(uciconfig, uciruleset, (cfg) => {
|
uci.foreach(uciconfig, uciruleset, (cfg) => {
|
||||||
if (cfg.enabled !== '1')
|
if (cfg.enabled !== '1')
|
||||||
return null;
|
return null;
|
||||||
@@ -1230,8 +1092,6 @@ if (!isEmpty(main_node)) {
|
|||||||
rs_tag = [rs_tag];
|
rs_tag = [rs_tag];
|
||||||
for (let t in extra_tags)
|
for (let t in extra_tags)
|
||||||
push(rs_tag, 'cfg-' + t + '-rule');
|
push(rs_tag, 'cfg-' + t + '-rule');
|
||||||
/* sing-box 1.14: multi-tag requires a {tag} placeholder in the fetch source
|
|
||||||
(remote: url and initial_path, local: path) */
|
|
||||||
const fetch_ref = (cfg.type === 'remote') ? (cfg.url || '') : (cfg.path || '');
|
const fetch_ref = (cfg.type === 'remote') ? (cfg.url || '') : (cfg.path || '');
|
||||||
if (!match(fetch_ref, /\{tag\}/))
|
if (!match(fetch_ref, /\{tag\}/))
|
||||||
warn(sprintf("homeproxy: rule-set '%s' uses extra tags but its %s source lacks a {tag} placeholder.", cfg['.name'], cfg.type));
|
warn(sprintf("homeproxy: rule-set '%s' uses extra tags but its %s source lacks a {tag} placeholder.", cfg['.name'], cfg.type));
|
||||||
@@ -1247,8 +1107,6 @@ if (!isEmpty(main_node)) {
|
|||||||
url: cfg.url,
|
url: cfg.url,
|
||||||
update_interval: cfg.update_interval
|
update_interval: cfg.update_interval
|
||||||
};
|
};
|
||||||
/* sing-box 1.14 downloads remote rule-sets through a top-level
|
|
||||||
http_client; download_detour no longer exists. */
|
|
||||||
if (cfg.type === 'remote')
|
if (cfg.type === 'remote')
|
||||||
ruleset.http_client = http_client_for(get_outbound(cfg.outbound) || get_outbound(default_outbound));
|
ruleset.http_client = http_client_for(get_outbound(cfg.outbound) || get_outbound(default_outbound));
|
||||||
if (cfg.type === 'remote' && !isEmpty(cfg.initial_path))
|
if (cfg.type === 'remote' && !isEmpty(cfg.initial_path))
|
||||||
@@ -1257,28 +1115,12 @@ if (!isEmpty(main_node)) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Belt-and-braces for route.default_domain_resolver.
|
|
||||||
*
|
|
||||||
* sing-box 1.14 removed the legacy outbound DNS rules and refuses to start when
|
|
||||||
* a dial-fields user has a domain to resolve without a resolver. The exemption
|
|
||||||
* for "only one DNS server configured" never applies here: default-dns and
|
|
||||||
* system-dns are emitted unconditionally, so every config has at least two.
|
|
||||||
* That makes this a global requirement, not a per-mode one, so it must not
|
|
||||||
* depend on which branch above ran. The branches already pick the right
|
|
||||||
* resolver (the WAN resolver, or default_outbound_dns in custom mode); this
|
|
||||||
* only covers the degenerate "not custom mode and no main node" case.
|
|
||||||
*/
|
|
||||||
if (isEmpty(config.route.default_domain_resolver))
|
if (isEmpty(config.route.default_domain_resolver))
|
||||||
config.route.default_domain_resolver = { server: 'default-dns' };
|
config.route.default_domain_resolver = { server: 'default-dns' };
|
||||||
|
|
||||||
/* Remote rule-sets download through the http_clients collected while the
|
|
||||||
rule-sets were built (see http_client_for). */
|
|
||||||
if (length(http_clients))
|
if (length(http_clients))
|
||||||
config.http_clients = http_clients;
|
config.http_clients = http_clients;
|
||||||
/* Routing rules end */
|
|
||||||
|
|
||||||
/* Experimental start */
|
|
||||||
if (routing_mode in ['bypass_mainland_china', 'custom']) {
|
if (routing_mode in ['bypass_mainland_china', 'custom']) {
|
||||||
config.experimental = {
|
config.experimental = {
|
||||||
cache_file: {
|
cache_file: {
|
||||||
@@ -1288,7 +1130,6 @@ if (routing_mode in ['bypass_mainland_china', 'custom']) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/* Experimental end */
|
|
||||||
|
|
||||||
config['$schema'] = 'https://sing-box.sagernet.org/schema.json';
|
config['$schema'] = 'https://sing-box.sagernet.org/schema.json';
|
||||||
|
|
||||||
@@ -1300,7 +1141,4 @@ if (system('/usr/bin/sing-box check --config ' + client_tmp) !== 0) {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
system('mv -f ' + client_tmp + ' ' + RUN_DIR + '/sing-box-c.json');
|
system('mv -f ' + client_tmp + ' ' + RUN_DIR + '/sing-box-c.json');
|
||||||
/* The generated config carries every node credential (passwords, UUIDs,
|
|
||||||
private keys) and writefile() has no mode argument, so it lands with the
|
|
||||||
process umask; chmod it to the sing-box user only. */
|
|
||||||
system('chmod 600 ' + RUN_DIR + '/sing-box-c.json');
|
system('chmod 600 ' + RUN_DIR + '/sing-box-c.json');
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
removeBlankAttrs, buildTLSObject, buildTransportObject, HP_DIR, RUN_DIR
|
removeBlankAttrs, buildTLSObject, buildTransportObject, HP_DIR, RUN_DIR
|
||||||
} from 'homeproxy';
|
} from 'homeproxy';
|
||||||
|
|
||||||
/* UCI config start */
|
|
||||||
const uci = cursor();
|
const uci = cursor();
|
||||||
|
|
||||||
const uciconfig = 'homeproxy';
|
const uciconfig = 'homeproxy';
|
||||||
@@ -24,11 +23,9 @@ uci.load(uciconfig);
|
|||||||
const uciserver = 'server';
|
const uciserver = 'server';
|
||||||
|
|
||||||
const log_level = uci.get(uciconfig, uciserver, 'log_level') || 'warn';
|
const log_level = uci.get(uciconfig, uciserver, 'log_level') || 'warn';
|
||||||
/* UCI config end */
|
|
||||||
|
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
/* Log */
|
|
||||||
config.log = {
|
config.log = {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
level: log_level,
|
level: log_level,
|
||||||
@@ -42,7 +39,6 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
|||||||
if (cfg.enabled !== '1')
|
if (cfg.enabled !== '1')
|
||||||
return;
|
return;
|
||||||
|
|
||||||
/* Snell (1.14): single-psk inbound; shape differs from the generic users[] block below */
|
|
||||||
if (cfg.type === 'snell') {
|
if (cfg.type === 'snell') {
|
||||||
push(config.inbounds, {
|
push(config.inbounds, {
|
||||||
type: 'snell',
|
type: 'snell',
|
||||||
@@ -76,10 +72,8 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
|||||||
udp_timeout: strToTime(cfg.udp_timeout),
|
udp_timeout: strToTime(cfg.udp_timeout),
|
||||||
network: cfg.network,
|
network: cfg.network,
|
||||||
|
|
||||||
/* AnyTLS */
|
|
||||||
padding_scheme: cfg.anytls_padding_scheme,
|
padding_scheme: cfg.anytls_padding_scheme,
|
||||||
|
|
||||||
/* Hysteria (2) */
|
|
||||||
up_mbps: strToInt(cfg.hysteria_up_mbps),
|
up_mbps: strToInt(cfg.hysteria_up_mbps),
|
||||||
down_mbps: strToInt(cfg.hysteria_down_mbps),
|
down_mbps: strToInt(cfg.hysteria_down_mbps),
|
||||||
obfs: cfg.hysteria_obfs_type ? {
|
obfs: cfg.hysteria_obfs_type ? {
|
||||||
@@ -91,31 +85,25 @@ uci.foreach(uciconfig, uciserver, (cfg) => {
|
|||||||
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
|
ignore_client_bandwidth: strToBool(cfg.hysteria_ignore_client_bandwidth),
|
||||||
masquerade: cfg.hysteria_masquerade,
|
masquerade: cfg.hysteria_masquerade,
|
||||||
|
|
||||||
/* Shadowsocks */
|
|
||||||
method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
|
method: (cfg.type === 'shadowsocks') ? cfg.shadowsocks_encrypt_method : null,
|
||||||
password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
|
password: (cfg.type in ['shadowsocks', 'shadowtls']) ? cfg.password : null,
|
||||||
|
|
||||||
/* Tuic */
|
|
||||||
congestion_control: cfg.tuic_congestion_control,
|
congestion_control: cfg.tuic_congestion_control,
|
||||||
auth_timeout: strToTime(cfg.tuic_auth_timeout),
|
auth_timeout: strToTime(cfg.tuic_auth_timeout),
|
||||||
zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt),
|
zero_rtt_handshake: strToBool(cfg.tuic_enable_zero_rtt),
|
||||||
heartbeat: strToTime(cfg.tuic_heartbeat),
|
heartbeat: strToTime(cfg.tuic_heartbeat),
|
||||||
|
|
||||||
/* AnyTLS / HTTP / Hysteria (2) / Mixed / Socks / Trojan / Tuic / VLESS / VMess */
|
|
||||||
users: (cfg.type !== 'shadowsocks') ? [
|
users: (cfg.type !== 'shadowsocks') ? [
|
||||||
{
|
{
|
||||||
name: !(cfg.type in ['http', 'mixed', 'naive', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null,
|
name: !(cfg.type in ['http', 'mixed', 'naive', 'socks']) ? 'cfg-' + cfg['.name'] + '-server' : null,
|
||||||
username: cfg.username,
|
username: cfg.username,
|
||||||
password: cfg.password,
|
password: cfg.password,
|
||||||
|
|
||||||
/* Hysteria */
|
|
||||||
auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
|
auth: (cfg.hysteria_auth_type === 'base64') ? cfg.hysteria_auth_payload : null,
|
||||||
auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
|
auth_str: (cfg.hysteria_auth_type === 'string') ? cfg.hysteria_auth_payload : null,
|
||||||
|
|
||||||
/* Tuic */
|
|
||||||
uuid: cfg.uuid,
|
uuid: cfg.uuid,
|
||||||
|
|
||||||
/* VLESS / VMess */
|
|
||||||
flow: cfg.vless_flow,
|
flow: cfg.vless_flow,
|
||||||
alterId: strToInt(cfg.vmess_alterid)
|
alterId: strToInt(cfg.vmess_alterid)
|
||||||
}
|
}
|
||||||
@@ -150,5 +138,4 @@ if (system('/usr/bin/sing-box check --config ' + server_tmp) !== 0) {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
system('mv -f ' + server_tmp + ' ' + RUN_DIR + '/sing-box-s.json');
|
system('mv -f ' + server_tmp + ' ' + RUN_DIR + '/sing-box-s.json');
|
||||||
/* keep the config (key paths, user credentials) out of world-readable mode */
|
|
||||||
system('chmod 600 ' + RUN_DIR + '/sing-box-s.json');
|
system('chmod 600 ' + RUN_DIR + '/sing-box-s.json');
|
||||||
@@ -7,13 +7,9 @@
|
|||||||
import { mkstemp } from 'fs';
|
import { mkstemp } from 'fs';
|
||||||
import { urldecode_params } from 'luci.http';
|
import { urldecode_params } from 'luci.http';
|
||||||
|
|
||||||
/* Global variables start */
|
|
||||||
export const HP_DIR = '/etc/homeproxy';
|
export const HP_DIR = '/etc/homeproxy';
|
||||||
export const RUN_DIR = '/var/run/homeproxy';
|
export const RUN_DIR = '/var/run/homeproxy';
|
||||||
/* Global variables end */
|
|
||||||
|
|
||||||
/* Utilities start */
|
|
||||||
/* Kanged from luci-app-commands */
|
|
||||||
export function shellQuote(s) {
|
export function shellQuote(s) {
|
||||||
return `'${replace(s, "'", "'\\''")}'`;
|
return `'${replace(s, "'", "'\\''")}'`;
|
||||||
};
|
};
|
||||||
@@ -26,26 +22,15 @@ export function isBinary(str) {
|
|||||||
return false;
|
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) {
|
function closeFD(fd) {
|
||||||
if (fd) {
|
if (fd) {
|
||||||
try {
|
try {
|
||||||
fd.close();
|
fd.close();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
/* already closed */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Read a descriptor to EOF. A single read(size) only returns up to `size`
|
|
||||||
* bytes, so the previous one-shot read(512 KiB) silently truncated larger
|
|
||||||
* responses - a subscription list above that size was cut mid-base64/mid-JSON
|
|
||||||
* and only showed up as a parse failure. MAX_OUTPUT bounds what an oversized
|
|
||||||
* or endless response can allocate.
|
|
||||||
*/
|
|
||||||
const MAX_OUTPUT = 16 * 1024 * 1024;
|
const MAX_OUTPUT = 16 * 1024 * 1024;
|
||||||
|
|
||||||
function readAll(fd) {
|
function readAll(fd) {
|
||||||
@@ -79,7 +64,6 @@ export function executeCommand(...args) {
|
|||||||
|
|
||||||
const binary = isBinary(stdout);
|
const binary = isBinary(stdout);
|
||||||
|
|
||||||
/* mkstemp() returns delete-on-close files, so closing is enough */
|
|
||||||
closeFD(outfd);
|
closeFD(outfd);
|
||||||
closeFD(errfd);
|
closeFD(errfd);
|
||||||
|
|
||||||
@@ -91,8 +75,6 @@ export function executeCommand(...args) {
|
|||||||
binary
|
binary
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} 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(outfd);
|
||||||
closeFD(errfd);
|
closeFD(errfd);
|
||||||
|
|
||||||
@@ -114,19 +96,6 @@ export function getTime(epoch) {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* GNU wget and OpenWrt's default /usr/bin/wget do not accept the same flags.
|
|
||||||
* On OpenWrt/ImmortalWrt that path comes from the uclient-fetch package
|
|
||||||
* (`ALTERNATIVES:=200:/usr/bin/wget:/bin/uclient-fetch`, `PROVIDES:=wget` in
|
|
||||||
* package/libs/uclient) and busybox ships no wget applet, so the effective
|
|
||||||
* wget is uclient-fetch unless wget-ssl happens to be installed on top of it.
|
|
||||||
* GNU-only flags break the fetch outright: uclient-fetch's getopt rejects
|
|
||||||
* `-nv` with "unrecognized option: n" before a single request is sent.
|
|
||||||
*
|
|
||||||
* The probe below tells the two apart. `-nv` (--no-verbose) is how GNU wget
|
|
||||||
* mutes the progress meter while still printing the failure reason on stderr;
|
|
||||||
* `-q` is the only meter switch uclient-fetch and busybox wget share.
|
|
||||||
*/
|
|
||||||
export function classifyWgetFlavor(exitcode, stdout) {
|
export function classifyWgetFlavor(exitcode, stdout) {
|
||||||
if (exitcode !== 0 || !stdout)
|
if (exitcode !== 0 || !stdout)
|
||||||
return 'compat';
|
return 'compat';
|
||||||
@@ -146,13 +115,6 @@ export function wgetFlavor() {
|
|||||||
return wget_flavor;
|
return wget_flavor;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* 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.
|
|
||||||
* Note that uclient-fetch's -q hides most failure messages, so on such a
|
|
||||||
* target the reason is often empty and only the exit code is left.
|
|
||||||
*/
|
|
||||||
export function wGETVerbose(url, ua) {
|
export function wGETVerbose(url, ua) {
|
||||||
if (!url || type(url) !== 'string')
|
if (!url || type(url) !== 'string')
|
||||||
return { content: null, error: 'invalid URL' };
|
return { content: null, error: 'invalid URL' };
|
||||||
@@ -160,9 +122,6 @@ export function wGETVerbose(url, ua) {
|
|||||||
if (!ua)
|
if (!ua)
|
||||||
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
|
ua = 'Wget/1.21 (HomeProxy, like v2rayN)';
|
||||||
|
|
||||||
/* -U/-T are understood by GNU wget, uclient-fetch and busybox wget alike;
|
|
||||||
* the long forms --user-agent/--timeout are not, busybox wget in
|
|
||||||
* particular knows neither of them. */
|
|
||||||
const output = executeCommand(`/usr/bin/wget ${wgetFlavor() === 'gnu' ? '-nv' : '-q'} -O- -U ${shellQuote(ua)} -T 10 ${shellQuote(url)}`) || {};
|
const output = executeCommand(`/usr/bin/wget ${wgetFlavor() === 'gnu' ? '-nv' : '-q'} -O- -U ${shellQuote(ua)} -T 10 ${shellQuote(url)}`) || {};
|
||||||
if (output.exitcode !== 0) {
|
if (output.exitcode !== 0) {
|
||||||
let reason = trim(output.stderr || '');
|
let reason = trim(output.stderr || '');
|
||||||
@@ -180,9 +139,7 @@ export function wGETVerbose(url, ua) {
|
|||||||
export function wGET(url, ua) {
|
export function wGET(url, ua) {
|
||||||
return wGETVerbose(url, ua).content;
|
return wGETVerbose(url, ua).content;
|
||||||
};
|
};
|
||||||
/* Utilities end */
|
|
||||||
|
|
||||||
/* String helper start */
|
|
||||||
export function isEmpty(res) {
|
export function isEmpty(res) {
|
||||||
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0);
|
||||||
};
|
};
|
||||||
@@ -199,16 +156,9 @@ export function strToTime(str) {
|
|||||||
if (isEmpty(str))
|
if (isEmpty(str))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
/* Preserve values that already carry a time unit (e.g. "30s", "1m") */
|
|
||||||
return match(str, /[a-zA-Z]$/) ? str : (str + 's');
|
return match(str, /[a-zA-Z]$/) ? str : (str + 's');
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Milliseconds variant of strToTime(). Used by options whose LuCI field is
|
|
||||||
* documented and validated in milliseconds while sing-box expects a duration
|
|
||||||
* (e.g. route rule tls_fragment_fallback_delay, whose default is 500ms);
|
|
||||||
* strToTime() used to turn "500" into "500s".
|
|
||||||
*/
|
|
||||||
export function strToMs(str) {
|
export function strToMs(str) {
|
||||||
if (isEmpty(str))
|
if (isEmpty(str))
|
||||||
return null;
|
return null;
|
||||||
@@ -255,20 +205,14 @@ export function validation(datatype, data) {
|
|||||||
return (ret === 0);
|
return (ret === 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Validate IP/CIDR format to prevent nftables template injection from resource files */
|
|
||||||
export function isValidCIDR(addr, family) {
|
export function isValidCIDR(addr, family) {
|
||||||
if (isEmpty(addr))
|
if (isEmpty(addr))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
/* Strip leading/trailing whitespace */
|
|
||||||
addr = trim(addr);
|
addr = trim(addr);
|
||||||
if (!addr)
|
if (!addr)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
/* Split address and optional prefix. The prefix has to be validated as
|
|
||||||
strictly as the address: `int()` accepts a leading number ("1.2.3.4/0 }
|
|
||||||
..." parses as 0), which would let a poisoned resource file inject
|
|
||||||
statements into the nftables template this feeds. */
|
|
||||||
const parts = split(addr, '/');
|
const parts = split(addr, '/');
|
||||||
const ip = parts[0];
|
const ip = parts[0];
|
||||||
const prefix = parts[1];
|
const prefix = parts[1];
|
||||||
@@ -278,31 +222,24 @@ export function isValidCIDR(addr, family) {
|
|||||||
if (length(parts) === 2 && !match(prefix, /^\d{1,3}$/))
|
if (length(parts) === 2 && !match(prefix, /^\d{1,3}$/))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
/* Validate IP part */
|
|
||||||
if (family === 4) {
|
if (family === 4) {
|
||||||
if (!match(ip, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
|
if (!match(ip, /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
|
||||||
return false;
|
return false;
|
||||||
/* Validate octet ranges */
|
|
||||||
const octets = split(ip, '.');
|
const octets = split(ip, '.');
|
||||||
for (let o in octets)
|
for (let o in octets)
|
||||||
if (int(o) > 255)
|
if (int(o) > 255)
|
||||||
return false;
|
return false;
|
||||||
/* Validate prefix if present */
|
|
||||||
if (prefix && int(prefix) > 32)
|
if (prefix && int(prefix) > 32)
|
||||||
return false;
|
return false;
|
||||||
} else if (family === 6) {
|
} else if (family === 6) {
|
||||||
/* Only hex digits and colons */
|
|
||||||
if (!match(ip, /^[0-9a-fA-F:]+$/))
|
if (!match(ip, /^[0-9a-fA-F:]+$/))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
/* At most one "::" */
|
|
||||||
if (match(ip, /::.*::/))
|
if (match(ip, /::.*::/))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
/* Validate group structure */
|
|
||||||
const dcolon = index(ip, '::');
|
const dcolon = index(ip, '::');
|
||||||
if (dcolon === -1) {
|
if (dcolon === -1) {
|
||||||
/* Uncompressed form: exactly 8 groups of 1-4 hex digits */
|
|
||||||
const groups = split(ip, ':');
|
const groups = split(ip, ':');
|
||||||
if (length(groups) !== 8)
|
if (length(groups) !== 8)
|
||||||
return false;
|
return false;
|
||||||
@@ -310,7 +247,6 @@ export function isValidCIDR(addr, family) {
|
|||||||
if (!match(g, /^[0-9a-fA-F]{1,4}$/))
|
if (!match(g, /^[0-9a-fA-F]{1,4}$/))
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
/* Compressed form: "::" expands to at least one zero group */
|
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (let part in [substr(ip, 0, dcolon), substr(ip, dcolon + 2)]) {
|
for (let part in [substr(ip, 0, dcolon), substr(ip, dcolon + 2)]) {
|
||||||
if (part === '')
|
if (part === '')
|
||||||
@@ -333,9 +269,7 @@ export function isValidCIDR(addr, family) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
/* String helper end */
|
|
||||||
|
|
||||||
/* String parser start */
|
|
||||||
export function decodeBase64Str(str) {
|
export function decodeBase64Str(str) {
|
||||||
if (isEmpty(str))
|
if (isEmpty(str))
|
||||||
return null;
|
return null;
|
||||||
@@ -351,14 +285,6 @@ export function decodeBase64Str(str) {
|
|||||||
return b64dec(str);
|
return b64dec(str);
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Decode %XX escapes without touching '+'. urldecode() maps '+' to a space,
|
|
||||||
* which corrupts base64 userinfo ("+" is part of the alphabet), so share-link
|
|
||||||
* userinfo is percent-decoded with this instead.
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
* ucode has no tolower(), so both hex cases are mapped by hand.
|
|
||||||
*/
|
|
||||||
function hexValue(ch) {
|
function hexValue(ch) {
|
||||||
const lower = index('0123456789abcdef', ch);
|
const lower = index('0123456789abcdef', ch);
|
||||||
if (lower >= 0)
|
if (lower >= 0)
|
||||||
@@ -369,10 +295,6 @@ function hexValue(ch) {
|
|||||||
return upper < 0 ? -1 : 10 + upper;
|
return upper < 0 ? -1 : 10 + upper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* ucode's regex engine rejects \x00 inside a character class ("Missing ']'"),
|
|
||||||
* so control bytes are checked by hand.
|
|
||||||
*/
|
|
||||||
function hasControlChar(str) {
|
function hasControlChar(str) {
|
||||||
for (let i = 0; i < length(str); i++) {
|
for (let i = 0; i < length(str); i++) {
|
||||||
const code = ord(str, i);
|
const code = ord(str, i);
|
||||||
@@ -449,9 +371,6 @@ export function parseURL(url) {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (objurl.userinfo) {
|
if (objurl.userinfo) {
|
||||||
/* Percent-decode first: providers emit base64 userinfo with the
|
|
||||||
padding escaped ("=" as "%3D"), which used to decode to nothing
|
|
||||||
and silently dropped method/password. */
|
|
||||||
let userinfo = percentDecode(objurl.userinfo);
|
let userinfo = percentDecode(objurl.userinfo);
|
||||||
|
|
||||||
userinfo = replace(userinfo, /:(.+)$/, (_, val) => {
|
userinfo = replace(userinfo, /:(.+)$/, (_, val) => {
|
||||||
@@ -459,14 +378,6 @@ export function parseURL(url) {
|
|||||||
return '';
|
return '';
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
* Keep anything without whitespace or control characters: real-world
|
|
||||||
* userinfo carries base64 (with '=' or '+'), percent-encoded tokens
|
|
||||||
* and passwords with punctuation. The previous class was written as
|
|
||||||
* [A-Za-z0-9\+\-\_\.] but ucode's regex reads \+-\_ as the range
|
|
||||||
* 0x2B-0x5F, so it accepted '=@:;[]\^<>,/' and rejected '!' - it
|
|
||||||
* neither did what it documented nor covered real passwords.
|
|
||||||
*/
|
|
||||||
if (!match(userinfo, /\s/) && !hasControlChar(userinfo))
|
if (!match(userinfo, /\s/) && !hasControlChar(userinfo))
|
||||||
objurl.username = userinfo;
|
objurl.username = userinfo;
|
||||||
else
|
else
|
||||||
@@ -483,18 +394,7 @@ export function parseURL(url) {
|
|||||||
|
|
||||||
return objurl;
|
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) {
|
export function buildTLSObject(cfg, is_server) {
|
||||||
if (cfg.tls !== '1')
|
if (cfg.tls !== '1')
|
||||||
return null;
|
return null;
|
||||||
@@ -540,7 +440,6 @@ export function buildTLSObject(cfg, is_server) {
|
|||||||
ech: is_server ? (cfg.tls_ech_key ? {
|
ech: is_server ? (cfg.tls_ech_key ? {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
key: split(cfg.tls_ech_key, '\n')
|
key: split(cfg.tls_ech_key, '\n')
|
||||||
/* config: split(cfg.tls_ech_config, '\n') */
|
|
||||||
} : null) : ((cfg.tls_ech === '1') ? {
|
} : null) : ((cfg.tls_ech === '1') ? {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
config: cfg.tls_ech_config,
|
config: cfg.tls_ech_config,
|
||||||
@@ -567,8 +466,6 @@ export function buildTLSObject(cfg, is_server) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* 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) {
|
export function buildTransportObject(cfg, is_server) {
|
||||||
if (isEmpty(cfg.transport))
|
if (isEmpty(cfg.transport))
|
||||||
return null;
|
return null;
|
||||||
@@ -589,15 +486,7 @@ export function buildTransportObject(cfg, is_server) {
|
|||||||
permit_without_stream: is_server ? null : strToBool(cfg.grpc_permit_without_stream)
|
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) {
|
export function isValidPEM(content, is_private_key) {
|
||||||
if (isEmpty(content))
|
if (isEmpty(content))
|
||||||
return false;
|
return false;
|
||||||
@@ -619,4 +508,3 @@ export function isValidPEM(content, is_private_key) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
/* PEM validation end */
|
|
||||||
@@ -27,11 +27,9 @@ const uciinfra = 'infra',
|
|||||||
uciroutingrule = 'routing_rule',
|
uciroutingrule = 'routing_rule',
|
||||||
uciserver = 'server';
|
uciserver = 'server';
|
||||||
|
|
||||||
/* chinadns-ng has been removed */
|
|
||||||
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
|
if (uci.get(uciconfig, uciinfra, 'china_dns_port'))
|
||||||
uci.delete(uciconfig, uciinfra, 'china_dns_port');
|
uci.delete(uciconfig, uciinfra, 'china_dns_port');
|
||||||
|
|
||||||
/* chinadns server now only accepts single server */
|
|
||||||
const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
|
const china_dns_server = uci.get(uciconfig, ucimain, 'china_dns_server');
|
||||||
if (type(china_dns_server) === 'array') {
|
if (type(china_dns_server) === 'array') {
|
||||||
uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]);
|
uci.set(uciconfig, ucimain, 'china_dns_server', china_dns_server[0]);
|
||||||
@@ -42,51 +40,41 @@ if (type(china_dns_server) === 'array') {
|
|||||||
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
|
uci.set(uciconfig, ucimain, 'china_dns_server', split(china_dns_server, ',')[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* github_token option has been moved to config section */
|
|
||||||
const github_token = uci.get(uciconfig, uciinfra, 'github_token');
|
const github_token = uci.get(uciconfig, uciinfra, 'github_token');
|
||||||
if (github_token) {
|
if (github_token) {
|
||||||
uci.set(uciconfig, ucimain, 'github_token', github_token);
|
uci.set(uciconfig, ucimain, 'github_token', github_token);
|
||||||
uci.delete(uciconfig, uciinfra, 'github_token')
|
uci.delete(uciconfig, uciinfra, 'github_token')
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ntp_server was introduced */
|
|
||||||
if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
|
if (!uci.get(uciconfig, uciinfra, 'ntp_server'))
|
||||||
uci.set(uciconfig, uciinfra, 'ntp_server', 'nil');
|
uci.set(uciconfig, uciinfra, 'ntp_server', 'nil');
|
||||||
|
|
||||||
/* tun_gso was deprecated in sb 1.11 */
|
|
||||||
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
|
if (!isEmpty(uci.get(uciconfig, uciinfra, 'tun_gso')))
|
||||||
uci.delete(uciconfig, uciinfra, 'tun_gso');
|
uci.delete(uciconfig, uciinfra, 'tun_gso');
|
||||||
|
|
||||||
/* create migration section */
|
|
||||||
if (!uci.get(uciconfig, ucimigration))
|
if (!uci.get(uciconfig, ucimigration))
|
||||||
uci.set(uciconfig, ucimigration, uciconfig);
|
uci.set(uciconfig, ucimigration, uciconfig);
|
||||||
|
|
||||||
/* delete old crontab command */
|
|
||||||
const migration_crontab = uci.get(uciconfig, ucimigration, 'crontab');
|
const migration_crontab = uci.get(uciconfig, ucimigration, 'crontab');
|
||||||
if (!migration_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');
|
uci.set(uciconfig, ucimigration, 'crontab', '1');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* log_level was introduced */
|
|
||||||
if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
|
if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
|
||||||
uci.set(uciconfig, ucimain, 'log_level', 'warn');
|
uci.set(uciconfig, ucimain, 'log_level', 'warn');
|
||||||
|
|
||||||
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
|
if (isEmpty(uci.get(uciconfig, uciserver, 'log_level')))
|
||||||
uci.set(uciconfig, uciserver, 'log_level', 'warn');
|
uci.set(uciconfig, uciserver, 'log_level', 'warn');
|
||||||
|
|
||||||
/* empty value defaults to all ports now */
|
|
||||||
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
|
if (uci.get(uciconfig, ucimain, 'routing_port') === 'all')
|
||||||
uci.delete(uciconfig, ucimain, 'routing_port');
|
uci.delete(uciconfig, ucimain, 'routing_port');
|
||||||
|
|
||||||
/* experimental section was removed */
|
|
||||||
if (uci.get(uciconfig, 'experimental'))
|
if (uci.get(uciconfig, 'experimental'))
|
||||||
uci.delete(uciconfig, 'experimental');
|
uci.delete(uciconfig, 'experimental');
|
||||||
|
|
||||||
/* block-dns was removed from built-in dns servers */
|
|
||||||
const default_dns_server = uci.get(uciconfig, ucidns, 'default_server');
|
const default_dns_server = uci.get(uciconfig, ucidns, 'default_server');
|
||||||
if (default_dns_server === 'block-dns') {
|
if (default_dns_server === 'block-dns') {
|
||||||
/* append a rule at last to block all DNS queries */
|
|
||||||
uci.set(uciconfig, '_migration_dns_final_block', ucidnsrule);
|
uci.set(uciconfig, '_migration_dns_final_block', ucidnsrule);
|
||||||
uci.set(uciconfig, '_migration_dns_final_block', 'label', 'migration_final_block_dns');
|
uci.set(uciconfig, '_migration_dns_final_block', 'label', 'migration_final_block_dns');
|
||||||
uci.set(uciconfig, '_migration_dns_final_block', 'enabled', '1');
|
uci.set(uciconfig, '_migration_dns_final_block', 'enabled', '1');
|
||||||
@@ -98,12 +86,9 @@ else if (default_dns_server === 'local-dns')
|
|||||||
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
|
uci.set(uciconfig, ucidns, 'default_server', 'default-dns');
|
||||||
|
|
||||||
const dns_server_migration = {};
|
const dns_server_migration = {};
|
||||||
/* DNS servers options */
|
|
||||||
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
||||||
/* legacy format was deprecated in sb 1.12 */
|
|
||||||
if (cfg.address) {
|
if (cfg.address) {
|
||||||
const addr = parseURL((!match(cfg.address, /:\/\//) ? 'udp://' : '') + (validation('ip6addr', cfg.address) ? `[${cfg.address}]` : cfg.address));
|
const addr = parseURL((!match(cfg.address, /:\/\//) ? 'udp://' : '') + (validation('ip6addr', cfg.address) ? `[${cfg.address}]` : cfg.address));
|
||||||
/* RCode was moved into DNS rules */
|
|
||||||
if (addr.protocol === 'rcode') {
|
if (addr.protocol === 'rcode') {
|
||||||
dns_server_migration[cfg['.name']] = { action: 'predefined' };
|
dns_server_migration[cfg['.name']] = { action: 'predefined' };
|
||||||
switch (addr.hostname) {
|
switch (addr.hostname) {
|
||||||
@@ -156,9 +141,7 @@ uci.foreach(uciconfig, ucidnsserver, (cfg) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* DNS rules options */
|
|
||||||
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
||||||
/* outbound was removed in sb 1.12 */
|
|
||||||
if (cfg.outbound) {
|
if (cfg.outbound) {
|
||||||
uci.delete(uciconfig, cfg['.name']);
|
uci.delete(uciconfig, cfg['.name']);
|
||||||
if (!cfg.enabled)
|
if (!cfg.enabled)
|
||||||
@@ -181,20 +164,16 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
|
|
||||||
if (cfg.rule_set_ipcidr_match_source === '1')
|
if (cfg.rule_set_ipcidr_match_source === '1')
|
||||||
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
||||||
|
|
||||||
/* block-dns was moved into action in sb 1.11 */
|
|
||||||
if (cfg.server === 'block-dns') {
|
if (cfg.server === 'block-dns') {
|
||||||
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
||||||
uci.delete(uciconfig, cfg['.name'], 'server');
|
uci.delete(uciconfig, cfg['.name'], 'server');
|
||||||
} else if (!cfg.action) {
|
} else if (!cfg.action) {
|
||||||
/* add missing 'action' field */
|
|
||||||
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* strategy and client_subnet were moved into dns rules */
|
|
||||||
if (dns_server_migration[cfg.server]) {
|
if (dns_server_migration[cfg.server]) {
|
||||||
if (dns_server_migration[cfg.server].strategy)
|
if (dns_server_migration[cfg.server].strategy)
|
||||||
uci.set(uciconfig, cfg['.name'], 'strategy', dns_server_migration[cfg.server].strategy);
|
uci.set(uciconfig, cfg['.name'], 'strategy', dns_server_migration[cfg.server].strategy);
|
||||||
@@ -210,26 +189,20 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* nodes options */
|
|
||||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||||
/* tls_ech_tls_disable_drs is useless and deprecated in sb 1.12 */
|
|
||||||
if (!isEmpty(cfg.tls_ech_tls_disable_drs))
|
if (!isEmpty(cfg.tls_ech_tls_disable_drs))
|
||||||
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
|
uci.delete(uciconfig, cfg['.name'], 'tls_ech_tls_disable_drs');
|
||||||
|
|
||||||
/* tls_ech_enable_pqss is useless and deprecated in sb 1.12 */
|
|
||||||
if (!isEmpty(cfg.tls_ech_enable_pqss))
|
if (!isEmpty(cfg.tls_ech_enable_pqss))
|
||||||
uci.delete(uciconfig, cfg['.name'], 'tls_ech_enable_pqss');
|
uci.delete(uciconfig, cfg['.name'], 'tls_ech_enable_pqss');
|
||||||
|
|
||||||
/* wireguard_gso was deprecated in sb 1.11 */
|
|
||||||
if (!isEmpty(cfg.wireguard_gso))
|
if (!isEmpty(cfg.wireguard_gso))
|
||||||
uci.delete(uciconfig, cfg['.name'], '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))
|
if (!isEmpty(cfg.hysteria_revc_window))
|
||||||
uci.rename(uciconfig, cfg['.name'], 'hysteria_revc_window', 'hysteria_recv_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')))
|
if (!isEmpty(uci.get(uciconfig, ucidns, 'independent_cache')))
|
||||||
uci.delete(uciconfig, ucidns, 'independent_cache');
|
uci.delete(uciconfig, ucidns, 'independent_cache');
|
||||||
|
|
||||||
@@ -248,38 +221,29 @@ uci.foreach(uciconfig, ucidnsrule, (cfg) => {
|
|||||||
uci.delete(uciconfig, cfg['.name'], 'rule_set_ip_cidr_accept_empty');
|
uci.delete(uciconfig, cfg['.name'], 'rule_set_ip_cidr_accept_empty');
|
||||||
});
|
});
|
||||||
|
|
||||||
/* routing rules options */
|
|
||||||
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
uci.foreach(uciconfig, uciroutingrule, (cfg) => {
|
||||||
/* rule_set_ipcidr_match_source was renamed in sb 1.10 */
|
|
||||||
if (cfg.rule_set_ipcidr_match_source === '1')
|
if (cfg.rule_set_ipcidr_match_source === '1')
|
||||||
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
uci.rename(uciconfig, cfg['.name'], 'rule_set_ipcidr_match_source', 'rule_set_ip_cidr_match_source');
|
||||||
|
|
||||||
/* block-out was moved into action in sb 1.11 */
|
|
||||||
if (cfg.outbound === 'block-out') {
|
if (cfg.outbound === 'block-out') {
|
||||||
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
uci.set(uciconfig, cfg['.name'], 'action', 'reject');
|
||||||
uci.delete(uciconfig, cfg['.name'], 'outbound');
|
uci.delete(uciconfig, cfg['.name'], 'outbound');
|
||||||
} else if (!cfg.action) {
|
} else if (!cfg.action) {
|
||||||
/* add missing 'action' field */
|
|
||||||
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
uci.set(uciconfig, cfg['.name'], 'action', 'route');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/* server options */
|
|
||||||
/* auto_firewall was moved into server options */
|
|
||||||
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
|
const auto_firewall = uci.get(uciconfig, uciserver, 'auto_firewall');
|
||||||
if (!isEmpty(auto_firewall))
|
if (!isEmpty(auto_firewall))
|
||||||
uci.delete(uciconfig, uciserver, 'auto_firewall');
|
uci.delete(uciconfig, uciserver, 'auto_firewall');
|
||||||
|
|
||||||
uci.foreach(uciconfig, uciserver, (cfg) => {
|
uci.foreach(uciconfig, uciserver, (cfg) => {
|
||||||
/* auto_firewall was moved into server options */
|
|
||||||
if (auto_firewall === '1')
|
if (auto_firewall === '1')
|
||||||
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
|
uci.set(uciconfig, cfg['.name'], 'firewall' , '1');
|
||||||
|
|
||||||
/* sniff_override was deprecated in sb 1.11 */
|
|
||||||
if (!isEmpty(cfg.sniff_override))
|
if (!isEmpty(cfg.sniff_override))
|
||||||
uci.delete(uciconfig, cfg['.name'], 'sniff_override');
|
uci.delete(uciconfig, cfg['.name'], 'sniff_override');
|
||||||
|
|
||||||
/* domain_strategy is now pointless without sniff override */
|
|
||||||
if (!isEmpty(cfg.domain_strategy))
|
if (!isEmpty(cfg.domain_strategy))
|
||||||
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
|
uci.delete(uciconfig, cfg['.name'], 'domain_strategy');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,28 +10,13 @@ import { urldecode, urlencode } from 'luci.http';
|
|||||||
|
|
||||||
import { decodeBase64Str, parseURL, percentDecode, isEmpty, validation } from 'homeproxy';
|
import { decodeBase64Str, parseURL, percentDecode, 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.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Transports sing-box accepts on a V2Ray-style outbound. Anything else
|
|
||||||
* (xhttp, splithttp, ...) makes sing-box reject the whole generated config,
|
|
||||||
* so such a node is dropped while importing instead of poisoning every node.
|
|
||||||
*/
|
|
||||||
const SUPPORTED_TRANSPORTS = ['ws', 'http', 'grpc', 'httpupgrade', 'quic'];
|
const SUPPORTED_TRANSPORTS = ['ws', 'http', 'grpc', 'httpupgrade', 'quic'];
|
||||||
|
|
||||||
function unsupported_transport(config) {
|
function unsupported_transport(config) {
|
||||||
return !isEmpty(config.transport) && !(config.transport in SUPPORTED_TRANSPORTS);
|
return !isEmpty(config.transport) && !(config.transport in SUPPORTED_TRANSPORTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* https://shadowsocks.org/guide/sip008.html */
|
|
||||||
export function parse_sip008_uri(uri) {
|
export function parse_sip008_uri(uri) {
|
||||||
return {
|
return {
|
||||||
label: uri.remarks,
|
label: uri.remarks,
|
||||||
@@ -45,7 +30,6 @@ export function parse_sip008_uri(uri) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md */
|
|
||||||
export function parse_anytls_uri(uri) {
|
export function parse_anytls_uri(uri) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
@@ -80,7 +64,6 @@ export function parse_http_uri(uri) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* https://github.com/HyNetwork/hysteria/wiki/URI-Scheme */
|
|
||||||
export function parse_hysteria_uri(uri, features, log) {
|
export function parse_hysteria_uri(uri, features, log) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
@@ -113,7 +96,6 @@ export function parse_hysteria_uri(uri, features, log) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* https://v2.hysteria.network/docs/developers/URI-Scheme/ */
|
|
||||||
export function parse_hysteria2_uri(uri, features, log) {
|
export function parse_hysteria2_uri(uri, features, log) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
@@ -142,7 +124,6 @@ export function parse_hysteria2_uri(uri, features, log) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Surge Snell share link: snell://host:port?psk=..&obfs=http&obfs-host=..#name */
|
|
||||||
export function parse_snell_uri(uri) {
|
export function parse_snell_uri(uri) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
@@ -183,7 +164,6 @@ export function parse_socks_uri(uri) {
|
|||||||
export function parse_ss_uri(uri) {
|
export function parse_ss_uri(uri) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
/* "Lovely" Shadowrocket format */
|
|
||||||
const ss_suri = split(uri[1], '#');
|
const ss_suri = split(uri[1], '#');
|
||||||
let ss_slabel = '';
|
let ss_slabel = '';
|
||||||
if (length(ss_suri) <= 2) {
|
if (length(ss_suri) <= 2) {
|
||||||
@@ -194,18 +174,13 @@ export function parse_ss_uri(uri) {
|
|||||||
uri[1] = ss_decoded + ss_slabel;
|
uri[1] = ss_decoded + 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]) || {};
|
const url = parseURL('http://' + uri[1]) || {};
|
||||||
|
|
||||||
let ss_userinfo = [];
|
let ss_userinfo = [];
|
||||||
if (url.username && url.password)
|
if (url.username && url.password)
|
||||||
/* User info encoded with URIComponent; parseURL() already decoded it */
|
|
||||||
ss_userinfo = [url.username, url.password];
|
ss_userinfo = [url.username, url.password];
|
||||||
else if (url.username)
|
else if (url.username)
|
||||||
/* User info encoded with base64 (padding may have arrived as %3D) */
|
|
||||||
ss_userinfo = split(decodeBase64Str(url.username) ?? '', ':', 2);
|
ss_userinfo = split(decodeBase64Str(url.username) ?? '', ':', 2);
|
||||||
|
|
||||||
let ss_plugin, ss_plugin_opts;
|
let ss_plugin, ss_plugin_opts;
|
||||||
@@ -213,7 +188,6 @@ export function parse_ss_uri(uri) {
|
|||||||
const ss_plugin_info = split(url.searchParams.plugin, ';', 2);
|
const ss_plugin_info = split(url.searchParams.plugin, ';', 2);
|
||||||
ss_plugin = ss_plugin_info[0];
|
ss_plugin = ss_plugin_info[0];
|
||||||
if (ss_plugin === 'simple-obfs')
|
if (ss_plugin === 'simple-obfs')
|
||||||
/* Fix non-standard plugin name */
|
|
||||||
ss_plugin = 'obfs-local';
|
ss_plugin = 'obfs-local';
|
||||||
ss_plugin_opts = ss_plugin_info[1];
|
ss_plugin_opts = ss_plugin_info[1];
|
||||||
}
|
}
|
||||||
@@ -230,7 +204,6 @@ export function parse_ss_uri(uri) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* https://p4gefau1t.github.io/trojan-go/developer/url/ */
|
|
||||||
export function parse_trojan_uri(uri) {
|
export function parse_trojan_uri(uri) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
@@ -270,7 +243,6 @@ export function parse_trojan_uri(uri) {
|
|||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* https://github.com/daeuniverse/dae/discussions/182 */
|
|
||||||
export function parse_tuic_uri(uri, features, log) {
|
export function parse_tuic_uri(uri, features, log) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
@@ -299,14 +271,12 @@ export function parse_tuic_uri(uri, features, log) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/* https://github.com/XTLS/Xray-core/discussions/716 */
|
|
||||||
export function parse_vless_uri(uri, features, log) {
|
export function parse_vless_uri(uri, features, log) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
const url = parseURL('http://' + uri[1]) || {};
|
const url = parseURL('http://' + uri[1]) || {};
|
||||||
const params = url.searchParams || {};
|
const params = url.searchParams || {};
|
||||||
|
|
||||||
/* Unsupported protocol */
|
|
||||||
if (params.type === 'kcp') {
|
if (params.type === 'kcp') {
|
||||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], urldecode(url.hash) || url.hostname));
|
||||||
return null;
|
return null;
|
||||||
@@ -332,10 +302,6 @@ export function parse_vless_uri(uri, features, log) {
|
|||||||
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
|
tls_reality_public_key: params.pbk ? urldecode(params.pbk) : null,
|
||||||
tls_reality_short_id: params.sid,
|
tls_reality_short_id: params.sid,
|
||||||
tls_utls: features.with_utls ? params.fp : null,
|
tls_utls: features.with_utls ? params.fp : null,
|
||||||
/* sing-box rejects every other value ("unsupported flow") and a single
|
|
||||||
rejected outbound makes the whole generated config fail, so only the
|
|
||||||
supported value is imported (the common
|
|
||||||
"xtls-rprx-vision-udp443" becomes "no flow" instead of breaking). */
|
|
||||||
vless_flow: (params.security in ['tls', 'reality'] && params.flow === 'xtls-rprx-vision') ? params.flow : null
|
vless_flow: (params.security in ['tls', 'reality'] && params.flow === 'xtls-rprx-vision') ? params.flow : null
|
||||||
};
|
};
|
||||||
switch (params.type) {
|
switch (params.type) {
|
||||||
@@ -372,11 +338,9 @@ export function parse_vless_uri(uri, features, log) {
|
|||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
/* https://github.com/2dust/v2rayN/wiki/Description-of-VMess-share-link */
|
|
||||||
export function parse_vmess_uri(uri, features, log) {
|
export function parse_vmess_uri(uri, features, log) {
|
||||||
uri = split(trim(uri), '://');
|
uri = split(trim(uri), '://');
|
||||||
|
|
||||||
/* "Lovely" shadowrocket format */
|
|
||||||
if (match(uri, /&/)) {
|
if (match(uri, /&/)) {
|
||||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||||
return null;
|
return null;
|
||||||
@@ -393,7 +357,6 @@ export function parse_vmess_uri(uri, features, log) {
|
|||||||
if (payload.v != '2') {
|
if (payload.v != '2') {
|
||||||
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
log(sprintf('Skipping unsupported %s format.', uri[0]));
|
||||||
return null;
|
return null;
|
||||||
/* Unsupported protocol */
|
|
||||||
} else if (payload.net === 'kcp') {
|
} else if (payload.net === 'kcp') {
|
||||||
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
log(sprintf('Skipping unsupported %s node: %s.', uri[0], payload.ps || payload.add));
|
||||||
return null;
|
return null;
|
||||||
@@ -404,13 +367,6 @@ export function parse_vmess_uri(uri, features, log) {
|
|||||||
|
|
||||||
return null;
|
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 = {
|
const config = {
|
||||||
label: payload.ps ? urldecode(payload.ps) : null,
|
label: payload.ps ? urldecode(payload.ps) : null,
|
||||||
|
|||||||
@@ -5,21 +5,12 @@
|
|||||||
|
|
||||||
SCRIPTS_DIR="/etc/homeproxy/scripts"
|
SCRIPTS_DIR="/etc/homeproxy/scripts"
|
||||||
|
|
||||||
# update_resources.sh exit codes: 0 updated, 1 failed, 2 another run holds the
|
|
||||||
# lock, 3 already up to date.
|
|
||||||
changed=0
|
changed=0
|
||||||
for i in "china_ip4" "china_ip6" "gfw_list" "china_list" "geoip_cn" "geosite_cn"; do
|
for i in "china_ip4" "china_ip6" "gfw_list" "china_list" "geoip_cn" "geosite_cn"; do
|
||||||
"$SCRIPTS_DIR"/update_resources.sh "$i"
|
"$SCRIPTS_DIR"/update_resources.sh "$i"
|
||||||
[ "$?" -eq 0 ] && changed=1
|
[ "$?" -eq 0 ] && changed=1
|
||||||
done
|
done
|
||||||
|
|
||||||
# The nft sets (china_ip4/ip6.txt) and the dnsmasq snippets (gfw_list.txt,
|
|
||||||
# china_list.txt) are rendered from those files, and the client rule-sets are
|
|
||||||
# loaded from the two .srs files, so a downloaded list only takes effect after
|
|
||||||
# they have been regenerated. Without this the daily cron update rewrote the
|
|
||||||
# files on disk and nothing else.
|
|
||||||
[ "$changed" -eq 0 ] || /etc/init.d/homeproxy refresh_lists
|
[ "$changed" -eq 0 ] || /etc/init.d/homeproxy refresh_lists
|
||||||
|
|
||||||
# update_subscriptions.uc restarts the service itself when it changed the UCI
|
|
||||||
# configuration.
|
|
||||||
"$SCRIPTS_DIR"/update_subscriptions.uc
|
"$SCRIPTS_DIR"/update_subscriptions.uc
|
||||||
@@ -35,12 +35,6 @@ check_list_update() {
|
|||||||
return 2
|
return 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --header= is the only header switch understood by all three wget
|
|
||||||
# implementations this runs on (uclient-fetch, GNU wget, busybox wget);
|
|
||||||
# the previous --header-file= is rejected by uclient-fetch and GNU wget
|
|
||||||
# alike, which made every version probe fail whenever a GitHub token was
|
|
||||||
# configured. The token is therefore visible in `ps` for the duration of
|
|
||||||
# the request; that is the trade-off for a single code path.
|
|
||||||
local api_url="https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1"
|
local api_url="https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname&per_page=1"
|
||||||
local list_info
|
local list_info
|
||||||
if [ -n "$github_token" ]; then
|
if [ -n "$github_token" ]; then
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import {
|
|||||||
|
|
||||||
import { parse_uri } from 'parse_uri';
|
import { parse_uri } from 'parse_uri';
|
||||||
|
|
||||||
/* UCI config start */
|
|
||||||
const uci = cursor();
|
const uci = cursor();
|
||||||
|
|
||||||
const uciconfig = 'homeproxy';
|
const uciconfig = 'homeproxy';
|
||||||
@@ -44,9 +43,7 @@ if (routing_mode !== 'custom') {
|
|||||||
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
main_node = uci.get(uciconfig, ucimain, 'main_node') || 'nil';
|
||||||
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
main_udp_node = uci.get(uciconfig, ucimain, 'main_udp_node') || 'nil';
|
||||||
}
|
}
|
||||||
/* UCI config end */
|
|
||||||
|
|
||||||
/* String helper start */
|
|
||||||
function filter_check(name) {
|
function filter_check(name) {
|
||||||
if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
|
if (isEmpty(name) || filter_mode === 'disabled' || isEmpty(filter_keywords))
|
||||||
return false;
|
return false;
|
||||||
@@ -68,9 +65,7 @@ function filter_check(name) {
|
|||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
/* String helper end */
|
|
||||||
|
|
||||||
/* Common var start */
|
|
||||||
const node_cache = {},
|
const node_cache = {},
|
||||||
node_result = [];
|
node_result = [];
|
||||||
|
|
||||||
@@ -78,9 +73,7 @@ const ubus = connect();
|
|||||||
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
|
const sing_features = ubus.call('luci.homeproxy', 'singbox_get_features', {}) || {};
|
||||||
if (isEmpty(sing_features))
|
if (isEmpty(sing_features))
|
||||||
log('Warning: Failed to query sing-box features via ubus, assuming defaults.');
|
log('Warning: Failed to query sing-box features via ubus, assuming defaults.');
|
||||||
/* Common var end */
|
|
||||||
|
|
||||||
/* Log */
|
|
||||||
system(`mkdir -p ${RUN_DIR}`);
|
system(`mkdir -p ${RUN_DIR}`);
|
||||||
function log(...args) {
|
function log(...args) {
|
||||||
const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
|
const logfile = open(`${RUN_DIR}/homeproxy.log`, 'a');
|
||||||
@@ -88,20 +81,10 @@ function log(...args) {
|
|||||||
logfile.close();
|
logfile.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Subscription URLs usually carry the provider token in the query string;
|
|
||||||
* the log is surfaced in LuCI, so keep it out.
|
|
||||||
*/
|
|
||||||
function redact(url) {
|
function redact(url) {
|
||||||
return replace(url, /[?#].*$/, '');
|
return replace(url, /[?#].*$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* mkdir is the lock: atomic everywhere this runs and no flock binding needed.
|
|
||||||
* A lock older than LOCK_STALE is broken, so a killed run cannot block every
|
|
||||||
* later update. Two concurrent runs (cron + the LuCI button) used to
|
|
||||||
* interleave commits and service restarts.
|
|
||||||
*/
|
|
||||||
const LOCK_DIR = RUN_DIR + '/update_subscriptions.lock';
|
const LOCK_DIR = RUN_DIR + '/update_subscriptions.lock';
|
||||||
const LOCK_STALE = 600;
|
const LOCK_STALE = 600;
|
||||||
|
|
||||||
@@ -126,11 +109,6 @@ function release_lock() {
|
|||||||
system(sprintf('rmdir %s 2>/dev/null', shellQuote(LOCK_DIR)));
|
system(sprintf('rmdir %s 2>/dev/null', shellQuote(LOCK_DIR)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* sing-box rejects the whole generated config over one malformed node, and the
|
|
||||||
* generator keeps the old config when it fails, so a node that cannot possibly
|
|
||||||
* work must not reach UCI. Returns the reason, or null when the node is usable.
|
|
||||||
*/
|
|
||||||
function node_problem(cfg) {
|
function node_problem(cfg) {
|
||||||
if (isEmpty(cfg.address) || isEmpty(cfg.port))
|
if (isEmpty(cfg.address) || isEmpty(cfg.port))
|
||||||
return 'missing address or port';
|
return 'missing address or port';
|
||||||
@@ -199,12 +177,9 @@ function main() {
|
|||||||
try {
|
try {
|
||||||
nodes = json(res).servers || json(res);
|
nodes = json(res).servers || json(res);
|
||||||
|
|
||||||
/* Shadowsocks SIP008 format */
|
|
||||||
if (nodes[0].server && nodes[0].method)
|
if (nodes[0].server && nodes[0].method)
|
||||||
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
|
map(nodes, (_, i) => nodes[i].nodetype = 'sip008');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
/* Clash/mihomo subscriptions are YAML, not base64; say so instead
|
|
||||||
of reporting a base64/JSON failure followed by "no valid node". */
|
|
||||||
if (match(res, /(^|\n)[ \t]*(proxies|proxy-groups)[ \t]*:/)) {
|
if (match(res, /(^|\n)[ \t]*(proxies|proxy-groups)[ \t]*:/)) {
|
||||||
log(sprintf('Unsupported subscription format (Clash/mihomo YAML) at %s.', shown_url));
|
log(sprintf('Unsupported subscription format (Clash/mihomo YAML) at %s.', shown_url));
|
||||||
continue;
|
continue;
|
||||||
@@ -222,9 +197,6 @@ function main() {
|
|||||||
|
|
||||||
const config = parse_uri(node, sing_features, log);
|
const config = parse_uri(node, sing_features, log);
|
||||||
if (isEmpty(config)) {
|
if (isEmpty(config)) {
|
||||||
/* No credentials in the log: only the scheme says whether a
|
|
||||||
parser is missing or the entry is simply not a node. Only
|
|
||||||
the first few are printed, the rest is summarized below. */
|
|
||||||
if (skipped < 5)
|
if (skipped < 5)
|
||||||
log(sprintf('Skipping unparsable entry (%s) from %s.',
|
log(sprintf('Skipping unparsable entry (%s) from %s.',
|
||||||
type(node) === 'string' ? replace(node, /:.*$/, '') : type(node), shown_url));
|
type(node) === 'string' ? replace(node, /:.*$/, '') : type(node), shown_url));
|
||||||
@@ -288,11 +260,9 @@ function main() {
|
|||||||
|
|
||||||
let added = 0, removed = 0, changed = false;
|
let added = 0, removed = 0, changed = false;
|
||||||
uci.foreach(uciconfig, ucinode, (cfg) => {
|
uci.foreach(uciconfig, ucinode, (cfg) => {
|
||||||
/* Nodes created by the user */
|
|
||||||
if (!cfg.grouphash)
|
if (!cfg.grouphash)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
/* Empty object - failed to fetch nodes, or subscription URL not yet processed */
|
|
||||||
if (!node_cache[cfg.grouphash] || length(node_cache[cfg.grouphash]) === 0)
|
if (!node_cache[cfg.grouphash] || length(node_cache[cfg.grouphash]) === 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
@@ -304,14 +274,10 @@ function main() {
|
|||||||
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
|
log(sprintf('Removing node: %s.', cfg.label || cfg['name']));
|
||||||
} else {
|
} else {
|
||||||
map(keys(cfg), (v) => {
|
map(keys(cfg), (v) => {
|
||||||
/* skip the cursor's own metadata (.name/.type/...): deleting
|
|
||||||
those is a no-op and would mark every run as changed */
|
|
||||||
if (substr(v, 0, 1) === '.')
|
if (substr(v, 0, 1) === '.')
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (v in node_cache[cfg.grouphash][cfg['.name']]) {
|
if (v in node_cache[cfg.grouphash][cfg['.name']]) {
|
||||||
/* only write what actually differs, so an unchanged
|
|
||||||
subscription does not fake a change below */
|
|
||||||
const fresh = node_cache[cfg.grouphash][cfg['.name']][v];
|
const fresh = node_cache[cfg.grouphash][cfg['.name']][v];
|
||||||
const current = uci.get(uciconfig, cfg['.name'], v);
|
const current = uci.get(uciconfig, cfg['.name'], v);
|
||||||
if (type(fresh) === 'array' ? (sprintf('%J', uci.get_all(uciconfig, cfg['.name'], v) || []) !== sprintf('%J', fresh))
|
if (type(fresh) === 'array' ? (sprintf('%J', uci.get_all(uciconfig, cfg['.name'], v) || []) !== sprintf('%J', fresh))
|
||||||
@@ -342,9 +308,6 @@ function main() {
|
|||||||
});
|
});
|
||||||
uci.commit(uciconfig);
|
uci.commit(uciconfig);
|
||||||
|
|
||||||
/* via_proxy keeps the service up during the fetch, but a configuration
|
|
||||||
change still has to be applied - it used to be committed and then
|
|
||||||
ignored until the next restart. */
|
|
||||||
let need_restart = (via_proxy !== '1') || changed;
|
let need_restart = (via_proxy !== '1') || changed;
|
||||||
if (!isEmpty(main_node)) {
|
if (!isEmpty(main_node)) {
|
||||||
const first_server = uci.get_first(uciconfig, ucinode);
|
const first_server = uci.get_first(uciconfig, ucinode);
|
||||||
@@ -410,7 +373,6 @@ function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrub stale urltest member references in custom routing nodes */
|
|
||||||
uci.foreach(uciconfig, 'routing_node', (cfg) => {
|
uci.foreach(uciconfig, 'routing_node', (cfg) => {
|
||||||
if (cfg.node !== 'urltest' || isEmpty(cfg.urltest_nodes))
|
if (cfg.node !== 'urltest' || isEmpty(cfg.urltest_nodes))
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -3,14 +3,6 @@
|
|||||||
* SPDX-License-Identifier: GPL-2.0-only
|
* SPDX-License-Identifier: GPL-2.0-only
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 ImmortalWrt.org
|
* 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|client|server]
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -18,7 +10,6 @@
|
|||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
/* --- LuCI runtime mock start ------------------------------------------- */
|
|
||||||
|
|
||||||
String.prototype.format = function (...args) {
|
String.prototype.format = function (...args) {
|
||||||
const values = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
const values = args.length === 1 && Array.isArray(args[0]) ? args[0] : args;
|
||||||
@@ -171,8 +162,6 @@ const rpc = { declare: () => () => Promise.resolve({}) };
|
|||||||
const fsMock = { exec_direct: () => Promise.resolve(''), read: () => Promise.resolve(''), write: () => Promise.resolve() };
|
const fsMock = { exec_direct: () => Promise.resolve(''), read: () => Promise.resolve(''), write: () => Promise.resolve() };
|
||||||
const widgets = { DeviceSelect: class {} };
|
const widgets = { DeviceSelect: class {} };
|
||||||
|
|
||||||
/* tools.firewall is used by the client view for the LAN ACL lists; the mock
|
|
||||||
has to return an option object like the real addIPOption/addMACOption. */
|
|
||||||
const fwtool = {
|
const fwtool = {
|
||||||
addIPOption: (section, _policy, name, title, _desc, _family, _hosts, _optional) => section.option(form.DynamicList, name, title),
|
addIPOption: (section, _policy, name, title, _desc, _family, _hosts, _optional) => section.option(form.DynamicList, name, title),
|
||||||
addMACOption: (section, _policy, name, title, _desc, _hosts) => section.option(form.DynamicList, name, title)
|
addMACOption: (section, _policy, name, title, _desc, _hosts) => section.option(form.DynamicList, name, title)
|
||||||
@@ -186,7 +175,6 @@ const deps = {
|
|||||||
'tools.firewall': fwtool
|
'tools.firewall': fwtool
|
||||||
};
|
};
|
||||||
|
|
||||||
/* --- LuCI runtime mock end --------------------------------------------- */
|
|
||||||
|
|
||||||
function loadLuciModule(file, extraDeps) {
|
function loadLuciModule(file, extraDeps) {
|
||||||
let src = fs.readFileSync(file, 'utf8');
|
let src = fs.readFileSync(file, 'utf8');
|
||||||
@@ -194,7 +182,6 @@ function loadLuciModule(file, extraDeps) {
|
|||||||
(_m, mod, alias) => `const ${alias} = __deps[${JSON.stringify(mod)}];`);
|
(_m, mod, alias) => `const ${alias} = __deps[${JSON.stringify(mod)}];`);
|
||||||
src = src.replace(/^'require ([^']+)';$/gm,
|
src = src.replace(/^'require ([^']+)';$/gm,
|
||||||
(_m, mod) => `const ${mod.split('.').pop().replace(/[^\w]/g, '_')} = __deps[${JSON.stringify(mod)}];`);
|
(_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);
|
const factory = new Function('__deps', '_', 'E', 'L', src);
|
||||||
return factory(Object.assign({}, deps, extraDeps), _, E, L);
|
return factory(Object.assign({}, deps, extraDeps), _, E, L);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# SPDX-License-Identifier: GPL-2.0-only
|
# 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)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
HOST="${HP_TEST_HOST:-root@192.168.1.1}"
|
HOST="${HP_TEST_HOST:-root@192.168.1.1}"
|
||||||
|
|||||||
@@ -2,14 +2,6 @@
|
|||||||
* SPDX-License-Identifier: GPL-2.0-only
|
* SPDX-License-Identifier: GPL-2.0-only
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 ImmortalWrt.org
|
* 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';
|
import { urldecode_params } from 'luci.http';
|
||||||
@@ -33,7 +25,6 @@ export function decodeBase64Str(str) {
|
|||||||
return b64dec(str);
|
return b64dec(str);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Pure-ucode stand-in for /sbin/validate_data (host/port/ip4/ip6/hostname). */
|
|
||||||
export function validation(datatype, data) {
|
export function validation(datatype, data) {
|
||||||
if (!datatype || !data)
|
if (!datatype || !data)
|
||||||
return null;
|
return null;
|
||||||
@@ -62,14 +53,6 @@ export function validation(datatype, data) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
* Decode %XX escapes without touching '+'. urldecode() maps '+' to a space,
|
|
||||||
* which corrupts base64 userinfo ("+" is part of the alphabet), so share-link
|
|
||||||
* userinfo is percent-decoded with this instead.
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
* ucode has no tolower(), so both hex cases are mapped by hand.
|
|
||||||
*/
|
|
||||||
function hexValue(ch) {
|
function hexValue(ch) {
|
||||||
const lower = index('0123456789abcdef', ch);
|
const lower = index('0123456789abcdef', ch);
|
||||||
if (lower >= 0)
|
if (lower >= 0)
|
||||||
@@ -80,10 +63,6 @@ function hexValue(ch) {
|
|||||||
return upper < 0 ? -1 : 10 + upper;
|
return upper < 0 ? -1 : 10 + upper;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* ucode's regex engine rejects \x00 inside a character class ("Missing ']'"),
|
|
||||||
* so control bytes are checked by hand.
|
|
||||||
*/
|
|
||||||
function hasControlChar(str) {
|
function hasControlChar(str) {
|
||||||
for (let i = 0; i < length(str); i++) {
|
for (let i = 0; i < length(str); i++) {
|
||||||
const code = ord(str, i);
|
const code = ord(str, i);
|
||||||
@@ -160,7 +139,6 @@ export function parseURL(url) {
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (objurl.userinfo) {
|
if (objurl.userinfo) {
|
||||||
/* keep in sync with the production parser (homeproxy.uc) */
|
|
||||||
let userinfo = percentDecode(objurl.userinfo);
|
let userinfo = percentDecode(objurl.userinfo);
|
||||||
|
|
||||||
userinfo = replace(userinfo, /:(.+)$/, (_, val) => {
|
userinfo = replace(userinfo, /:(.+)$/, (_, val) => {
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# SPDX-License-Identifier: GPL-2.0-only
|
# 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:-.}"
|
ROOT="${1:-.}"
|
||||||
WORK="${2:-/tmp/hp-ucode-tests}"
|
WORK="${2:-/tmp/hp-ucode-tests}"
|
||||||
@@ -16,8 +10,6 @@ FAILED=0
|
|||||||
echo "== ucode syntax check =="
|
echo "== ucode syntax check =="
|
||||||
for file in "$ROOT"/root/etc/homeproxy/scripts/*.uc "$ROOT"/root/usr/share/rpcd/ucode/*; do
|
for file in "$ROOT"/root/etc/homeproxy/scripts/*.uc "$ROOT"/root/usr/share/rpcd/ucode/*; do
|
||||||
[ -f "$file" ] || continue
|
[ -f "$file" ] || continue
|
||||||
# Modules (with export statements) cannot be compiled as a program; they
|
|
||||||
# are loaded through `import` below instead.
|
|
||||||
case "$file" in
|
case "$file" in
|
||||||
*homeproxy.uc|*parse_uri.uc) continue ;;
|
*homeproxy.uc|*parse_uri.uc) continue ;;
|
||||||
esac
|
esac
|
||||||
@@ -82,10 +74,6 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== wget flavour selection =="
|
echo "== wget flavour selection =="
|
||||||
# A stub /usr/bin/wget whose --version answer comes from the environment, so
|
|
||||||
# both flavours can be exercised on a single host. The empty-answer/non-zero
|
|
||||||
# case is uclient-fetch, whose getopt rejects --version just like it rejects
|
|
||||||
# -nv before sending anything.
|
|
||||||
rm -rf "$WORK/wget_flavor"
|
rm -rf "$WORK/wget_flavor"
|
||||||
mkdir -p "$WORK/wget_flavor"
|
mkdir -p "$WORK/wget_flavor"
|
||||||
cat > "$WORK/wget_flavor/wget-stub" <<'STUB'
|
cat > "$WORK/wget_flavor/wget-stub" <<'STUB'
|
||||||
@@ -125,8 +113,6 @@ for flavor in gnu compat; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
echo "== real uclient-fetch compatibility =="
|
echo "== real uclient-fetch compatibility =="
|
||||||
# uclient-fetch is the default /usr/bin/wget on OpenWrt/ImmortalWrt, so a
|
|
||||||
# GNU-only option anywhere in the generated command line has to fail here.
|
|
||||||
if [ -x /bin/uclient-fetch ]; then
|
if [ -x /bin/uclient-fetch ]; then
|
||||||
rm -rf "$WORK/uclient"
|
rm -rf "$WORK/uclient"
|
||||||
mkdir -p "$WORK/uclient"
|
mkdir -p "$WORK/uclient"
|
||||||
|
|||||||
@@ -1,15 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# SPDX-License-Identifier: GPL-2.0-only
|
# 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="${1:-.}"
|
||||||
ROOT="$(cd "$ROOT" && pwd)"
|
ROOT="$(cd "$ROOT" && pwd)"
|
||||||
@@ -23,8 +13,6 @@ if ! utpl -S "$ROOT/root/etc/homeproxy/scripts/firewall_post.ut" > "$OUT" 2>"/tm
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
for name in homeproxy_local_addr_v4 homeproxy_wan_proxy_addr_v4; do
|
||||||
if ! grep -q "^set $name {" "$OUT"; then
|
if ! grep -q "^set $name {" "$OUT"; then
|
||||||
echo "FAIL: 'set $name {' is missing from the rendered firewall template"
|
echo "FAIL: 'set $name {' is missing from the rendered firewall template"
|
||||||
@@ -33,8 +21,6 @@ for name in homeproxy_local_addr_v4 homeproxy_wan_proxy_addr_v4; do
|
|||||||
fi
|
fi
|
||||||
done
|
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
|
if grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" > "/dev/null"; then
|
||||||
echo "FAIL: an nft statement is glued onto a comment line"
|
echo "FAIL: an nft statement is glued onto a comment line"
|
||||||
grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" | head -2
|
grep -nE "^#[^!].*homeproxy_[a-z0-9_]+ \{" "$OUT" | head -2
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# SPDX-License-Identifier: GPL-2.0-only
|
# 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="${1:-.}"
|
||||||
ROOT="$(cd "$ROOT" && pwd)"
|
ROOT="$(cd "$ROOT" && pwd)"
|
||||||
@@ -18,7 +10,6 @@ PRE="$ROOT/root/etc/homeproxy/scripts/firewall_pre.uc"
|
|||||||
|
|
||||||
FAILED=0
|
FAILED=0
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
. "$NAMES"
|
. "$NAMES"
|
||||||
|
|
||||||
check_group() {
|
check_group() {
|
||||||
@@ -57,8 +48,6 @@ post_sets="$(grep -oE '^set homeproxy_[a-z0-9_]+' "$POST" | awk '{print $2}' | s
|
|||||||
check_group "chain" "$HP_FW4_CHAINS" "$post_chains"
|
check_group "chain" "$HP_FW4_CHAINS" "$post_chains"
|
||||||
check_group "set" "$HP_FW4_SETS" "$post_sets"
|
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)"
|
pre_refs="$(grep -oE 'homeproxy_[a-z0-9_]+' "$PRE" | sort -u)"
|
||||||
if [ -n "$pre_refs" ]; then
|
if [ -n "$pre_refs" ]; then
|
||||||
echo "NOTE: firewall_pre.uc references fw4 objects:$pre_refs"
|
echo "NOTE: firewall_pre.uc references fw4 objects:$pre_refs"
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# SPDX-License-Identifier: GPL-2.0-only
|
# 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:-.}"
|
ROOT="${1:-.}"
|
||||||
WORK="${2:-/tmp/hp-generator-test}"
|
WORK="${2:-/tmp/hp-generator-test}"
|
||||||
@@ -29,8 +21,6 @@ run_case() {
|
|||||||
: > "$dir/resources/proxy_list.txt"
|
: > "$dir/resources/proxy_list.txt"
|
||||||
|
|
||||||
if [ "$local_rs" = "1" ]; then
|
if [ "$local_rs" = "1" ]; then
|
||||||
# with the .srs files that update_resources.sh downloads in place, the
|
|
||||||
# generator has to load them instead of the remote URLs
|
|
||||||
printf '%s' '{"version":1,"rules":[{"domain_suffix":["example.com"]}]}' > "$dir/rs-src.json"
|
printf '%s' '{"version":1,"rules":[{"domain_suffix":["example.com"]}]}' > "$dir/rs-src.json"
|
||||||
if ! sing-box rule-set compile "$dir/rs-src.json" -o "$dir/resources/geoip_cn.srs" \
|
if ! sing-box rule-set compile "$dir/rs-src.json" -o "$dir/resources/geoip_cn.srs" \
|
||||||
|| ! sing-box rule-set compile "$dir/rs-src.json" -o "$dir/resources/geosite_cn.srs"; then
|
|| ! sing-box rule-set compile "$dir/rs-src.json" -o "$dir/resources/geosite_cn.srs"; then
|
||||||
@@ -41,7 +31,6 @@ run_case() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if grep -q "__RULESET_DIR__" "$fixture"; then
|
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"
|
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
|
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"
|
echo "FAIL: $name: could not compile the local rule-set fixture"
|
||||||
@@ -83,10 +72,6 @@ run_case() {
|
|||||||
|
|
||||||
run_case client "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
|
run_case client "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json
|
||||||
|
|
||||||
# Golden comparison for the client generator. `sing-box check` only proves the
|
|
||||||
# config parses; this proves the generated config did not change silently (rule
|
|
||||||
# order, DNS routing decisions, whether a field is emitted at all). The work
|
|
||||||
# directory is normalized out of the paths before comparing.
|
|
||||||
CLIENT_GOLDEN="$ROOT/tests/snapshots/client.generated.json"
|
CLIENT_GOLDEN="$ROOT/tests/snapshots/client.generated.json"
|
||||||
CLIENT_OUT="$WORK/client/run/sing-box-c.json"
|
CLIENT_OUT="$WORK/client/run/sing-box-c.json"
|
||||||
if [ ! -f "$CLIENT_GOLDEN" ]; then
|
if [ ! -f "$CLIENT_GOLDEN" ]; then
|
||||||
@@ -103,10 +88,6 @@ else
|
|||||||
FAILED=1
|
FAILED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 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
|
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"
|
echo "FAIL: client: a remote rule-set would still be downloaded directly"
|
||||||
FAILED=1
|
FAILED=1
|
||||||
@@ -116,9 +97,6 @@ if ! grep -q '"detour": "main-out"' "$WORK/client/run/sing-box-c.json"; then
|
|||||||
FAILED=1
|
FAILED=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Local rule-sets (downloaded by update_resources.sh) must be preferred over
|
|
||||||
# the remote URLs, so a cold start does not depend on reaching the CDN through
|
|
||||||
# the node.
|
|
||||||
run_case client-local "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json 1
|
run_case client-local "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json 1
|
||||||
if grep -q '"type": "local"' "$WORK/client-local/run/sing-box-c.json" \
|
if grep -q '"type": "local"' "$WORK/client-local/run/sing-box-c.json" \
|
||||||
&& ! grep -q '"type": "remote"' "$WORK/client-local/run/sing-box-c.json"; then
|
&& ! grep -q '"type": "remote"' "$WORK/client-local/run/sing-box-c.json"; then
|
||||||
@@ -129,12 +107,6 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
run_case custom "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json
|
run_case custom "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json
|
||||||
# Custom mode without a default outbound: the default shape of the custom UI.
|
|
||||||
# `sing-box check` alone is not enough -- before the fix the generator emitted a
|
|
||||||
# config with none of the user's DNS servers, nodes, rules or rule-sets, and
|
|
||||||
# sing-box 1.14 then refused to start it for a missing
|
|
||||||
# route.default_domain_resolver. Assert on the emitted config so a partial
|
|
||||||
# regression cannot pass as green.
|
|
||||||
run_case custom_no_default "$ROOT/tests/fixtures/generators/custom-no-default-outbound.uci" generate_client.uc sing-box-c.json
|
run_case custom_no_default "$ROOT/tests/fixtures/generators/custom-no-default-outbound.uci" generate_client.uc sing-box-c.json
|
||||||
|
|
||||||
CUSTOM_OUT="$WORK/custom_no_default/run/sing-box-c.json"
|
CUSTOM_OUT="$WORK/custom_no_default/run/sing-box-c.json"
|
||||||
@@ -150,12 +122,8 @@ assert_json() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# sing-box always emits default-dns and system-dns, so more than one DNS server
|
|
||||||
# is always present and 1.14 requires a global domain resolver.
|
|
||||||
assert_json "custom/no-default: a global domain resolver is declared" '"default_domain_resolver"'
|
assert_json "custom/no-default: a global domain resolver is declared" '"default_domain_resolver"'
|
||||||
assert_json "custom/no-default: the user DNS server survived" '"cfg-DNS_Server_Proxy-dns"'
|
assert_json "custom/no-default: the user DNS server survived" '"cfg-DNS_Server_Proxy-dns"'
|
||||||
# A sing-box rule has no label field, so the UCI `option label` never reaches the
|
|
||||||
# JSON. Match the marker the rule body actually carries.
|
|
||||||
assert_json "custom/no-default: the user DNS rule survived" 'dns-rule-marker.example.com'
|
assert_json "custom/no-default: the user DNS rule survived" 'dns-rule-marker.example.com'
|
||||||
assert_json "custom/no-default: the urltest group survived" '"cfg-Routing_Node_Proxy-out"'
|
assert_json "custom/no-default: the urltest group survived" '"cfg-Routing_Node_Proxy-out"'
|
||||||
assert_json "custom/no-default: its member nodes survived" '"cfg-n_hk-out"'
|
assert_json "custom/no-default: its member nodes survived" '"cfg-n_hk-out"'
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
* SPDX-License-Identifier: GPL-2.0-only
|
* SPDX-License-Identifier: GPL-2.0-only
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 ImmortalWrt.org
|
* 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';
|
'use strict';
|
||||||
@@ -32,7 +28,6 @@ function fd_count() {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* successful command */
|
|
||||||
const ok = executeCommand('echo', 'hello');
|
const ok = executeCommand('echo', 'hello');
|
||||||
expect('ok.exitcode', ok.exitcode, 0);
|
expect('ok.exitcode', ok.exitcode, 0);
|
||||||
expect('ok.stdout', ok.stdout, 'hello\n');
|
expect('ok.stdout', ok.stdout, 'hello\n');
|
||||||
@@ -40,29 +35,22 @@ expect('ok.stderr', ok.stderr, '');
|
|||||||
expect('ok.binary', ok.binary, false);
|
expect('ok.binary', ok.binary, false);
|
||||||
expect('ok.command', ok.command, 'echo hello');
|
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'));
|
const bad = executeCommand('sh', '-c', shellQuote('echo oops >&2; exit 3'));
|
||||||
expect('bad.exitcode', bad.exitcode, 3);
|
expect('bad.exitcode', bad.exitcode, 3);
|
||||||
expect('bad.stderr', bad.stderr, 'oops\n');
|
expect('bad.stderr', bad.stderr, 'oops\n');
|
||||||
expect('bad.stdout', bad.stdout, '');
|
expect('bad.stdout', bad.stdout, '');
|
||||||
|
|
||||||
/* nondescript exit code */
|
|
||||||
expect('false.exitcode', executeCommand('false').exitcode, 1);
|
expect('false.exitcode', executeCommand('false').exitcode, 1);
|
||||||
|
|
||||||
/* binary output is detected and withheld */
|
|
||||||
const bin = executeCommand('sh', '-c', shellQuote('printf "\\001\\002\\003"'));
|
const bin = executeCommand('sh', '-c', shellQuote('printf "\\001\\002\\003"'));
|
||||||
expect('bin.binary', bin.binary, true);
|
expect('bin.binary', bin.binary, true);
|
||||||
expect('bin.stdout', bin.stdout, null);
|
expect('bin.stdout', bin.stdout, null);
|
||||||
|
|
||||||
/* output larger than the old one-shot 512 KiB read must not be truncated
|
|
||||||
(subscription lists above that size used to arrive cut in half) */
|
|
||||||
const big_want = 700 * 1024;
|
const big_want = 700 * 1024;
|
||||||
const big = executeCommand('sh', '-c', shellQuote('yes a | head -c ' + big_want));
|
const big = executeCommand('sh', '-c', shellQuote('yes a | head -c ' + big_want));
|
||||||
expect('big.exitcode', big.exitcode, 0);
|
expect('big.exitcode', big.exitcode, 0);
|
||||||
expect('big.length', length(big.stdout), big_want);
|
expect('big.length', length(big.stdout), big_want);
|
||||||
|
|
||||||
/* isValidCIDR(): the prefix needs the same strict validation as the address,
|
|
||||||
otherwise a poisoned resource file reaches the nftables template */
|
|
||||||
expect('cidr.ok', isValidCIDR('1.2.3.4/24', 4), true);
|
expect('cidr.ok', isValidCIDR('1.2.3.4/24', 4), true);
|
||||||
expect('cidr.bare', isValidCIDR('1.2.3.4', 4), true);
|
expect('cidr.bare', isValidCIDR('1.2.3.4', 4), true);
|
||||||
expect('cidr.bad-prefix', isValidCIDR('1.2.3.4/abc', 4), false);
|
expect('cidr.bad-prefix', isValidCIDR('1.2.3.4/abc', 4), false);
|
||||||
@@ -73,7 +61,6 @@ expect('cidr.injection', isValidCIDR('1.2.3.4/0 } flush ruleset; table inet x {'
|
|||||||
expect('cidr.v6-ok', isValidCIDR('2001:db8::/32', 6), true);
|
expect('cidr.v6-ok', isValidCIDR('2001:db8::/32', 6), true);
|
||||||
expect('cidr.v6-injection', isValidCIDR('2001:db8::/32 } flush ruleset;', 6), false);
|
expect('cidr.v6-injection', isValidCIDR('2001:db8::/32 } flush ruleset;', 6), false);
|
||||||
|
|
||||||
/* isValidPEM(): certificate vs private key, boundaries and body */
|
|
||||||
const pem_cert = '-----BEGIN CERTIFICATE-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END CERTIFICATE-----';
|
const pem_cert = '-----BEGIN CERTIFICATE-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END CERTIFICATE-----';
|
||||||
const pem_key = '-----BEGIN RSA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END RSA PRIVATE KEY-----';
|
const pem_key = '-----BEGIN RSA PRIVATE KEY-----\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\n-----END RSA PRIVATE KEY-----';
|
||||||
expect('pem.cert', isValidPEM(pem_cert, false), true);
|
expect('pem.cert', isValidPEM(pem_cert, false), true);
|
||||||
@@ -83,7 +70,6 @@ expect('pem.key-as-cert', isValidPEM(pem_key, false), false);
|
|||||||
expect('pem.garbage', isValidPEM('not a pem at all', false), false);
|
expect('pem.garbage', isValidPEM('not a pem at all', false), false);
|
||||||
expect('pem.empty', isValidPEM('', false), false);
|
expect('pem.empty', isValidPEM('', false), false);
|
||||||
|
|
||||||
/* descriptors must not leak across calls */
|
|
||||||
const before = fd_count();
|
const before = fd_count();
|
||||||
for (let i = 0; i < 200; i++)
|
for (let i = 0; i < 200; i++)
|
||||||
executeCommand('true');
|
executeCommand('true');
|
||||||
|
|||||||
@@ -3,11 +3,6 @@
|
|||||||
* SPDX-License-Identifier: GPL-2.0-only
|
* SPDX-License-Identifier: GPL-2.0-only
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 ImmortalWrt.org
|
* 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';
|
'use strict';
|
||||||
|
|||||||
@@ -3,10 +3,6 @@
|
|||||||
* SPDX-License-Identifier: GPL-2.0-only
|
* SPDX-License-Identifier: GPL-2.0-only
|
||||||
*
|
*
|
||||||
* Copyright (C) 2025 ImmortalWrt.org
|
* 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';
|
'use strict';
|
||||||
@@ -44,13 +40,11 @@ function expect_null(name, cfg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* anytls */
|
|
||||||
expect_fields('anytls', parse_uri('anytls://secret@a.example.com:443?sni=a.example.com&insecure=1#AnyTLS-Node', FEATURES, LOG), {
|
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',
|
label: 'AnyTLS-Node', type: 'anytls', address: 'a.example.com', port: '443',
|
||||||
password: 'secret', tls: '1', tls_sni: 'a.example.com', tls_insecure: '1'
|
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), {
|
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',
|
label: 'HTTPS-Node', type: 'http', address: 'b.example.com', port: '8443',
|
||||||
username: 'user', password: 'pass', tls: '1'
|
username: 'user', password: 'pass', tls: '1'
|
||||||
@@ -59,7 +53,6 @@ expect_fields('http', parse_uri('http://c.example.com:8080#HTTP-Node', FEATURES,
|
|||||||
label: 'HTTP-Node', type: 'http', address: 'c.example.com', port: '8080', tls: '0'
|
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), {
|
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',
|
label: 'Hy1', type: 'hysteria', address: 'd.example.com', port: '36712',
|
||||||
hysteria_protocol: 'udp', hysteria_auth_type: 'string', hysteria_auth_payload: 'pass',
|
hysteria_protocol: 'udp', hysteria_auth_type: 'string', hysteria_auth_payload: 'pass',
|
||||||
@@ -67,7 +60,6 @@ expect_fields('hysteria', parse_uri('hysteria://d.example.com:36712?auth=pass&pe
|
|||||||
tls: '1', tls_insecure: '1', tls_sni: 'd.example.com', tls_alpn: 'h3'
|
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), {
|
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',
|
label: 'Hy2', type: 'hysteria2', address: 'e.example.com', port: '443',
|
||||||
password: 'pass', hysteria_obfs_type: 'salamander', hysteria_obfs_password: 'op',
|
password: 'pass', hysteria_obfs_type: 'salamander', hysteria_obfs_password: 'op',
|
||||||
@@ -77,14 +69,12 @@ expect_fields('hy2-alias', parse_uri('hy2://pass@e.example.com:443#Hy2Alias', FE
|
|||||||
label: 'Hy2Alias', type: 'hysteria2', address: 'e.example.com', port: '443'
|
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), {
|
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',
|
label: 'Snell', type: 'snell', address: 'f.example.com', port: '443',
|
||||||
password: 'pskvalue', snell_version: '4', snell_obfs_mode: 'http',
|
password: 'pskvalue', snell_version: '4', snell_obfs_mode: 'http',
|
||||||
snell_obfs_host: 'bing.com', snell_reuse: '1'
|
snell_obfs_host: 'bing.com', snell_reuse: '1'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* socks */
|
|
||||||
expect_fields('socks5', parse_uri('socks5://user:pass@g.example.com:1080#Socks', FEATURES, LOG), {
|
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',
|
label: 'Socks', type: 'socks', address: 'g.example.com', port: '1080',
|
||||||
username: 'user', password: 'pass', socks_version: '5'
|
username: 'user', password: 'pass', socks_version: '5'
|
||||||
@@ -93,25 +83,21 @@ expect_fields('socks4', parse_uri('socks4://h.example.com:1080#Socks4', FEATURES
|
|||||||
socks_version: '4'
|
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), {
|
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',
|
label: 'SS-Node', type: 'shadowsocks', address: 'i.example.com', port: '8388',
|
||||||
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
|
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), {
|
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',
|
label: 'SS-Plugin', type: 'shadowsocks', address: 'j.example.com', port: '8388',
|
||||||
shadowsocks_plugin: 'obfs-local', shadowsocks_plugin_opts: 'obfs=http'
|
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), {
|
expect_fields('ss-shadowrocket', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmRAay5leGFtcGxlLmNvbTo4Mzg4#SS-SR', FEATURES, LOG), {
|
||||||
label: 'SS-SR', type: 'shadowsocks', address: 'k.example.com', port: '8388',
|
label: 'SS-SR', type: 'shadowsocks', address: 'k.example.com', port: '8388',
|
||||||
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
|
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), {
|
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',
|
label: 'TrojanWs', type: 'trojan', address: 'l.example.com', port: '443', password: 'pass',
|
||||||
transport: 'ws', ws_host: 'l.example.com', ws_path: '/path',
|
transport: 'ws', ws_host: 'l.example.com', ws_path: '/path',
|
||||||
@@ -122,14 +108,12 @@ expect_fields('trojan-grpc', parse_uri('trojan://pass@l.example.com:443?type=grp
|
|||||||
transport: 'grpc', grpc_servicename: 'gs'
|
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), {
|
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',
|
label: 'Tuic', type: 'tuic', address: 'm.example.com', port: '443',
|
||||||
uuid: 'tuic-uuid', password: 'pass', tuic_congestion_control: 'bbr',
|
uuid: 'tuic-uuid', password: 'pass', tuic_congestion_control: 'bbr',
|
||||||
tuic_udp_relay_mode: 'native', tls: '1', tls_sni: 'm.example.com', tls_alpn: [ 'h3' ]
|
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), {
|
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',
|
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',
|
transport: 'grpc', grpc_servicename: 'gs', tls: '1', tls_sni: 'n.example.com',
|
||||||
@@ -142,7 +126,6 @@ expect_fields('vless-ws-early-data', parse_uri('vless://vless-uuid@o.example.com
|
|||||||
tls: '1', tls_alpn: [ 'h2', 'http/1.1' ]
|
tls: '1', tls_alpn: [ 'h2', 'http/1.1' ]
|
||||||
});
|
});
|
||||||
|
|
||||||
/* vmess */
|
|
||||||
expect_fields('vmess-ws', parse_uri('vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQiOiJwLmV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiM2FmODg1NjEtOWM2OS00YjE5LThmN2UtZjA4ZDU4MGJjMzM5IiwiYWlkIjoiMCIsIm5ldCI6IndzIiwidHlwZSI6Im5vbmUiLCJob3N0IjoicC5leGFtcGxlLmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJwLmV4YW1wbGUuY29tIn0=', FEATURES, LOG), {
|
expect_fields('vmess-ws', parse_uri('vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQiOiJwLmV4YW1wbGUuY29tIiwicG9ydCI6IjQ0MyIsImlkIjoiM2FmODg1NjEtOWM2OS00YjE5LThmN2UtZjA4ZDU4MGJjMzM5IiwiYWlkIjoiMCIsIm5ldCI6IndzIiwidHlwZSI6Im5vbmUiLCJob3N0IjoicC5leGFtcGxlLmNvbSIsInBhdGgiOiIvd3MiLCJ0bHMiOiJ0bHMiLCJzbmkiOiJwLmV4YW1wbGUuY29tIn0=', FEATURES, LOG), {
|
||||||
label: 'VMess', type: 'vmess', address: 'p.example.com', port: '443',
|
label: 'VMess', type: 'vmess', address: 'p.example.com', port: '443',
|
||||||
uuid: '3af88561-9c69-4b19-8f7e-f08d580bc339', vmess_alterid: '0', vmess_encrypt: 'auto',
|
uuid: '3af88561-9c69-4b19-8f7e-f08d580bc339', vmess_alterid: '0', vmess_encrypt: 'auto',
|
||||||
@@ -150,7 +133,6 @@ expect_fields('vmess-ws', parse_uri('vmess://eyJ2IjoiMiIsInBzIjoiVk1lc3MiLCJhZGQ
|
|||||||
tls: '1', tls_sni: 'p.example.com'
|
tls: '1', tls_sni: 'p.example.com'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* sip008 object */
|
|
||||||
expect_fields('sip008', parse_uri({
|
expect_fields('sip008', parse_uri({
|
||||||
nodetype: 'sip008', remarks: 'SIP008', server: 'q.example.com', server_port: 8388,
|
nodetype: 'sip008', remarks: 'SIP008', server: 'q.example.com', server_port: 8388,
|
||||||
method: 'aes-256-gcm', password: 'pw'
|
method: 'aes-256-gcm', password: 'pw'
|
||||||
@@ -159,30 +141,23 @@ expect_fields('sip008', parse_uri({
|
|||||||
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'pw'
|
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('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('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('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('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));
|
expect_null('unknown-scheme', parse_uri('unknown://u@t.example.com:443', FEATURES, LOG));
|
||||||
/* sing-box has no xhttp transport; importing it used to make the whole
|
|
||||||
generated config fail with "unknown transport type" */
|
|
||||||
expect_null('vless-xhttp-unsupported', parse_uri('vless://u@t.example.com:443?security=tls&type=xhttp#Xhttp', FEATURES, LOG));
|
expect_null('vless-xhttp-unsupported', parse_uri('vless://u@t.example.com:443?security=tls&type=xhttp#Xhttp', FEATURES, LOG));
|
||||||
|
|
||||||
/* percent-encoded base64 userinfo (%3D instead of =): used to lose method and
|
|
||||||
password, which made the node break the generated config */
|
|
||||||
expect_fields('ss-percent-encoded-userinfo', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ%3D@j.example.com:8388#SSEnc', FEATURES, LOG), {
|
expect_fields('ss-percent-encoded-userinfo', parse_uri('ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ%3D@j.example.com:8388#SSEnc', FEATURES, LOG), {
|
||||||
label: 'SSEnc', type: 'shadowsocks', address: 'j.example.com', port: '8388',
|
label: 'SSEnc', type: 'shadowsocks', address: 'j.example.com', port: '8388',
|
||||||
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
|
shadowsocks_encrypt_method: 'aes-256-gcm', password: 'password'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* unsupported flow must import as "no flow" instead of breaking sing-box */
|
|
||||||
expect_fields('vless-flow-udp443-dropped', parse_uri('vless://vless-uuid@n.example.com:443?security=tls&flow=xtls-rprx-vision-udp443&sni=n.example.com#FlowDrop', FEATURES, LOG), {
|
expect_fields('vless-flow-udp443-dropped', parse_uri('vless://vless-uuid@n.example.com:443?security=tls&flow=xtls-rprx-vision-udp443&sni=n.example.com#FlowDrop', FEATURES, LOG), {
|
||||||
label: 'FlowDrop', type: 'vless', address: 'n.example.com', port: '443',
|
label: 'FlowDrop', type: 'vless', address: 'n.example.com', port: '443',
|
||||||
uuid: 'vless-uuid', tls: '1', tls_sni: 'n.example.com', vless_flow: null
|
uuid: 'vless-uuid', tls: '1', tls_sni: 'n.example.com', vless_flow: null
|
||||||
});
|
});
|
||||||
|
|
||||||
/* punctuation in a password survives now that userinfo is not whitelisted */
|
|
||||||
expect_fields('trojan-punctuation-password', parse_uri('trojan://pa!ss~1@m.example.com:443?sni=m.example.com#Punct', FEATURES, LOG), {
|
expect_fields('trojan-punctuation-password', parse_uri('trojan://pa!ss~1@m.example.com:443?sni=m.example.com#Punct', FEATURES, LOG), {
|
||||||
label: 'Punct', type: 'trojan', address: 'm.example.com', port: '443',
|
label: 'Punct', type: 'trojan', address: 'm.example.com', port: '443',
|
||||||
password: 'pa!ss~1', tls: '1', tls_sni: 'm.example.com'
|
password: 'pa!ss~1', tls: '1', tls_sni: 'm.example.com'
|
||||||
|
|||||||
@@ -3,17 +3,6 @@
|
|||||||
* SPDX-License-Identifier: GPL-2.0-only
|
* SPDX-License-Identifier: GPL-2.0-only
|
||||||
*
|
*
|
||||||
* Copyright (C) 2026 ImmortalWrt.org
|
* Copyright (C) 2026 ImmortalWrt.org
|
||||||
*
|
|
||||||
* Regression tests for the wget flavour probe and for the command line
|
|
||||||
* wGETVerbose() builds from it.
|
|
||||||
*
|
|
||||||
* OpenWrt/ImmortalWrt's default /usr/bin/wget is uclient-fetch, which rejects
|
|
||||||
* GNU-only flags such as -nv with "unrecognized option: n" *before* it opens a
|
|
||||||
* connection, so a single GNU-ism turns every subscription update into
|
|
||||||
* "no valid node found". run.sh stages a copy of homeproxy.uc whose
|
|
||||||
* /usr/bin/wget points at a stub; HP_WGET_STUB_VERSION steers the answer of
|
|
||||||
* the `--version` probe (empty output plus a non-zero exit mimics
|
|
||||||
* uclient-fetch, whose getopt rejects the option and returns 1).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -39,7 +28,6 @@ function has(list, value) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The value following the first occurrence of `key` */
|
|
||||||
function after(list, key) {
|
function after(list, key) {
|
||||||
let prev = null;
|
let prev = null;
|
||||||
|
|
||||||
@@ -53,7 +41,6 @@ function after(list, key) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Classification of the `wget --version` probe. */
|
|
||||||
expect('gnu.version', classifyWgetFlavor(0, 'GNU Wget 1.25.0 built on linux-gnu.\n'), 'gnu');
|
expect('gnu.version', classifyWgetFlavor(0, 'GNU Wget 1.25.0 built on linux-gnu.\n'), 'gnu');
|
||||||
expect('gnu.multiline', classifyWgetFlavor(0, "GNU Wget 1.21.4 built on linux-gnu.\n\nCopyright (C) 2015 Free Software Foundation.\n"), 'gnu');
|
expect('gnu.multiline', classifyWgetFlavor(0, "GNU Wget 1.21.4 built on linux-gnu.\n\nCopyright (C) 2015 Free Software Foundation.\n"), 'gnu');
|
||||||
expect('uclient.rejects-version', classifyWgetFlavor(1, null), 'compat');
|
expect('uclient.rejects-version', classifyWgetFlavor(1, null), 'compat');
|
||||||
@@ -61,7 +48,6 @@ expect('busybox.version', classifyWgetFlavor(0, 'BusyBox v1.36.1 (2024-01-01) mu
|
|||||||
expect('empty.stdout', classifyWgetFlavor(0, ''), 'compat');
|
expect('empty.stdout', classifyWgetFlavor(0, ''), 'compat');
|
||||||
expect('missing.binary', classifyWgetFlavor(127, null), 'compat');
|
expect('missing.binary', classifyWgetFlavor(127, null), 'compat');
|
||||||
|
|
||||||
/* wgetFlavor() probes the stub and caches the answer. */
|
|
||||||
const want_flavor = getenv('HP_WGET_EXPECT_FLAVOR');
|
const want_flavor = getenv('HP_WGET_EXPECT_FLAVOR');
|
||||||
expect('flavor', wgetFlavor(), want_flavor);
|
expect('flavor', wgetFlavor(), want_flavor);
|
||||||
expect('flavor.cached', wgetFlavor(), want_flavor);
|
expect('flavor.cached', wgetFlavor(), want_flavor);
|
||||||
@@ -79,8 +65,6 @@ expect('timeout flag', after(args, '-T'), '10');
|
|||||||
expect('output flag', has(args, '-O-'), true);
|
expect('output flag', has(args, '-O-'), true);
|
||||||
expect('url', has(args, 'https://example.invalid/subs?token=abc'), true);
|
expect('url', has(args, 'https://example.invalid/subs?token=abc'), true);
|
||||||
|
|
||||||
/* -nv is GNU wget's --no-verbose; every other wget on the target rejects it,
|
|
||||||
* and -q is the meter switch they all share. */
|
|
||||||
expect('meter flag', has(args, want_flavor === 'gnu' ? '-nv' : '-q'), true);
|
expect('meter flag', has(args, want_flavor === 'gnu' ? '-nv' : '-q'), true);
|
||||||
expect('no gnu-only flag', has(args, '-nv'), want_flavor === 'gnu');
|
expect('no gnu-only flag', has(args, '-nv'), want_flavor === 'gnu');
|
||||||
expect('no long options', has(args, '--user-agent') || has(args, '--timeout=10'), false);
|
expect('no long options', has(args, '--user-agent') || has(args, '--timeout=10'), false);
|
||||||
|
|||||||
@@ -3,17 +3,6 @@
|
|||||||
* SPDX-License-Identifier: GPL-2.0-only
|
* SPDX-License-Identifier: GPL-2.0-only
|
||||||
*
|
*
|
||||||
* Copyright (C) 2026 ImmortalWrt.org
|
* Copyright (C) 2026 ImmortalWrt.org
|
||||||
*
|
|
||||||
* Feeds the command line wGETVerbose() builds to the target's real
|
|
||||||
* uclient-fetch - the default /usr/bin/wget on OpenWrt/ImmortalWrt. run.sh
|
|
||||||
* rewrites the module's wget path to /bin/uclient-fetch and skips this file
|
|
||||||
* where that binary does not exist.
|
|
||||||
*
|
|
||||||
* A closed loopback port is the entire request: what matters is that
|
|
||||||
* uclient-fetch gets past getopt. Before the flavour probe existed it did not -
|
|
||||||
* `-nv` made it print "unrecognized option: n" and exit before the request was
|
|
||||||
* sent, so every subscription fetch came back empty. Any failure observed here
|
|
||||||
* must therefore come from the network, never from the option parser.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -30,7 +19,6 @@ function expect(name, actual, want) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* uclient-fetch has no --version and rejects it, so it is classified compat. */
|
|
||||||
expect('flavor', wgetFlavor(), 'compat');
|
expect('flavor', wgetFlavor(), 'compat');
|
||||||
|
|
||||||
const res = wGETVerbose('http://127.0.0.1:1/subs', 'HomeProxy test UA');
|
const res = wGETVerbose('http://127.0.0.1:1/subs', 'HomeProxy test UA');
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-honk
|
PKG_NAME:=luci-app-honk
|
||||||
PKG_VERSION:=1.1.4
|
PKG_VERSION:=1.1.4
|
||||||
PKG_RELEASE:=21
|
PKG_RELEASE:=22
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI support for honk
|
LUCI_TITLE:=LuCI support for honk
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
|
|||||||
@@ -10,6 +10,14 @@ if [ "$dash" = "none" ]; then
|
|||||||
: > /etc/honk/config.d/api.dae
|
: > /etc/honk/config.d/api.dae
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Drop options of the auto-subscribe feature removed from this package
|
||||||
|
uci -q show honk 2>/dev/null | sed -n 's/^\(.*\)\.subscribe_[a-z_]*=.*$/\1/p' | sort -u | while read -r s; do
|
||||||
|
for opt in subscribe_auto_update subscribe_update_week_time subscribe_update_day_time; do
|
||||||
|
uci -q delete "$s.$opt"
|
||||||
|
done
|
||||||
|
uci -q commit honk
|
||||||
|
done
|
||||||
|
|
||||||
# Migrate legacy /etc/honk/dashboard directory
|
# Migrate legacy /etc/honk/dashboard directory
|
||||||
if [ -d /etc/honk/dashboard ] && [ ! -d /etc/honk/doona ]; then
|
if [ -d /etc/honk/dashboard ] && [ ! -d /etc/honk/doona ]; then
|
||||||
mv /etc/honk/dashboard /etc/honk/doona
|
mv /etc/honk/dashboard /etc/honk/doona
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-open-box
|
PKG_NAME:=luci-app-open-box
|
||||||
PKG_VERSION:=0.1.311
|
PKG_VERSION:=0.1.313
|
||||||
PKG_RELEASE:=23
|
PKG_RELEASE:=24
|
||||||
|
|
||||||
PKG_MAINTAINER:=liandu2024
|
PKG_MAINTAINER:=liandu2024
|
||||||
PKG_LICENSE:=GPL-3.0-or-later
|
PKG_LICENSE:=GPL-3.0-or-later
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-passwall
|
PKG_NAME:=luci-app-passwall
|
||||||
PKG_VERSION:=26.10.4
|
PKG_VERSION:=26.10.4
|
||||||
PKG_RELEASE:=326
|
PKG_RELEASE:=327
|
||||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
|
|||||||
@@ -315,9 +315,6 @@ o:depends({ _acl_node_bool = "1", _diff_global_node = "1" })
|
|||||||
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
|
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
|
||||||
o:depends({ node = "__always__" })
|
o:depends({ node = "__always__" })
|
||||||
|
|
||||||
o = s:taboption("Main", HideValue, "_node_sel_other", "")
|
|
||||||
o:depends({ _node_sel_shunt = "1", ['!reverse'] = true })
|
|
||||||
|
|
||||||
---- DNS
|
---- DNS
|
||||||
o = s:taboption("DNS", ListValue, "dns_shunt", "DNS " .. translate("Shunt"))
|
o = s:taboption("DNS", ListValue, "dns_shunt", "DNS " .. translate("Shunt"))
|
||||||
o.default = "chinadns-ng"
|
o.default = "chinadns-ng"
|
||||||
@@ -347,7 +344,7 @@ end
|
|||||||
if has_xray then
|
if has_xray then
|
||||||
o:value("xray", "Xray")
|
o:value("xray", "Xray")
|
||||||
end
|
end
|
||||||
o:depends({ _acl_node_bool = "1", _node_sel_other = "1", _diff_global_node = "1" })
|
o:depends({ _acl_node_bool = "1", _node_sel_shunt = "", _diff_global_node = "1" })
|
||||||
o.write = function(self, section, value)
|
o.write = function(self, section, value)
|
||||||
if value == "dns2socks" then
|
if value == "dns2socks" then
|
||||||
m:del(section, "v2ray_dns_mode")
|
m:del(section, "v2ray_dns_mode")
|
||||||
|
|||||||
@@ -136,13 +136,7 @@ if (has_singbox or has_xray) and #nodes_table > 0 then
|
|||||||
tips.cfgvalue = function(t, n)
|
tips.cfgvalue = function(t, n)
|
||||||
return string.format('<a style="color: red">%s</a>', translate("There are no available nodes, please add or subscribe nodes first."))
|
return string.format('<a style="color: red">%s</a>', translate("There are no available nodes, please add or subscribe nodes first."))
|
||||||
end
|
end
|
||||||
tips:depends("_node", "1")
|
tips:depends({ node = "", ['!reverse'] = true })
|
||||||
for k, v in pairs(shunt_list) do
|
|
||||||
tips:depends("node", v.id)
|
|
||||||
end
|
|
||||||
for k, v in pairs(balancing_list) do
|
|
||||||
tips:depends("node", v.id)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -150,7 +144,7 @@ o = s:taboption("Main", Value, "node_socks_port", translate("Node") .. " Socks "
|
|||||||
o.default = 1070
|
o.default = 1070
|
||||||
o.placeholder = 1070
|
o.placeholder = 1070
|
||||||
o.datatype = "range(1,65535)"
|
o.datatype = "range(1,65535)"
|
||||||
o:depends("_node", "1")
|
o:depends({ node = "", ['!reverse'] = true })
|
||||||
--[[
|
--[[
|
||||||
if has_singbox or has_xray then
|
if has_singbox or has_xray then
|
||||||
o = s:taboption("Main", Value, "node_http_port", translate("Node") .. " HTTP " .. translate("Listen Port") .. " " .. translate("0 is not use"))
|
o = s:taboption("Main", Value, "node_http_port", translate("Node") .. " HTTP " .. translate("Listen Port") .. " " .. translate("0 is not use"))
|
||||||
@@ -160,22 +154,16 @@ end
|
|||||||
]]--
|
]]--
|
||||||
o = s:taboption("Main", Flag, "node_socks_bind_local", translate("Node") .. " Socks " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
|
o = s:taboption("Main", Flag, "node_socks_bind_local", translate("Node") .. " Socks " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
|
||||||
o.default = "1"
|
o.default = "1"
|
||||||
o:depends("_node", "1")
|
o:depends({ node = "", ['!reverse'] = true })
|
||||||
|
|
||||||
o = s:taboption("Main", HideValue, "node_save_before", "")
|
o = s:taboption("Main", HideValue, "node_save_before", "")
|
||||||
o.value = current_node[".name"]
|
o.value = current_node[".name"]
|
||||||
o.cbid = function(self, section) return "node_save_before" end
|
o.cbid = function(self, section) return "node_save_before" end
|
||||||
|
|
||||||
o = s:taboption("Main", HideValue, "_node", "")
|
|
||||||
o:depends({ node = "", ['!reverse'] = true })
|
|
||||||
|
|
||||||
-- Node → DNS Depends Settings
|
-- Node → DNS Depends Settings
|
||||||
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
|
o = s:taboption("Main", HideValue, "_node_sel_shunt", "")
|
||||||
o:depends({ node = "__always__" })
|
o:depends({ node = "__always__" })
|
||||||
|
|
||||||
o = s:taboption("Main", HideValue, "_node_sel_other", "")
|
|
||||||
o:depends({ _node_sel_shunt = "1", ['!reverse'] = true })
|
|
||||||
|
|
||||||
-- [[ DNS Settings ]]--
|
-- [[ DNS Settings ]]--
|
||||||
s:tab("DNS", translate("DNS"))
|
s:tab("DNS", translate("DNS"))
|
||||||
|
|
||||||
@@ -279,8 +267,8 @@ end
|
|||||||
if has_xray then
|
if has_xray then
|
||||||
o:value("xray", "Xray")
|
o:value("xray", "Xray")
|
||||||
end
|
end
|
||||||
o:depends({ dns_shunt = "chinadns-ng", _node_sel_other = "1" })
|
o:depends({ dns_shunt = "chinadns-ng", _node_sel_shunt = "" })
|
||||||
o:depends({ dns_shunt = "dnsmasq", _node_sel_other = "1" })
|
o:depends({ dns_shunt = "dnsmasq", _node_sel_shunt = "" })
|
||||||
o.write = function(self, section, value)
|
o.write = function(self, section, value)
|
||||||
if value ~= "sing-box" and value ~= "xray" then
|
if value ~= "sing-box" and value ~= "xray" then
|
||||||
m:del(section, "v2ray_dns_mode")
|
m:del(section, "v2ray_dns_mode")
|
||||||
@@ -305,7 +293,7 @@ if api.is_finded("smartdns") then
|
|||||||
if has_xray then
|
if has_xray then
|
||||||
o:value("xray", "Xray")
|
o:value("xray", "Xray")
|
||||||
end
|
end
|
||||||
o:depends({ dns_shunt = "smartdns", _node_sel_other = "1" })
|
o:depends({ dns_shunt = "smartdns", _node_sel_shunt = "" })
|
||||||
o.write = function(self, section, value)
|
o.write = function(self, section, value)
|
||||||
if value == "socks" then
|
if value == "socks" then
|
||||||
m:del(section, "v2ray_dns_mode")
|
m:del(section, "v2ray_dns_mode")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
|||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
PKG_NAME:=luci-app-ssr-plus
|
PKG_NAME:=luci-app-ssr-plus
|
||||||
PKG_VERSION:=196
|
PKG_VERSION:=196
|
||||||
PKG_RELEASE:=87
|
PKG_RELEASE:=88
|
||||||
|
|
||||||
PKG_CONFIG_DEPENDS:= \
|
PKG_CONFIG_DEPENDS:= \
|
||||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ usage() {
|
|||||||
|
|
||||||
loger() {
|
loger() {
|
||||||
# 1.alert 2.crit 3.err 4.warn 5.notice 6.info 7.debug
|
# 1.alert 2.crit 3.err 4.warn 5.notice 6.info 7.debug
|
||||||
logger -st ssr-rules[$$] -p$1 $2
|
logger -st "ssr-rules[$$]" -p "$1" -- "$2"
|
||||||
}
|
}
|
||||||
|
|
||||||
# IP list normalization function (for comparison)
|
# IP list normalization function (for comparison)
|
||||||
|
|||||||
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
|||||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||||
|
|
||||||
PKG_NAME:=mise
|
PKG_NAME:=mise
|
||||||
PKG_VERSION:=2026.10.7
|
PKG_VERSION:=2026.10.8
|
||||||
PKG_RELEASE:=32
|
PKG_RELEASE:=33
|
||||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
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_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||||
PKG_HASH:=skip
|
PKG_HASH:=skip
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=nexttrace
|
PKG_NAME:=nexttrace
|
||||||
PKG_VERSION:=1.7.3
|
PKG_VERSION:=1.7.4
|
||||||
PKG_RELEASE:=16
|
PKG_RELEASE:=17
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||||
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)?
|
PKG_SOURCE_URL:=https://codeload.github.com/nxtrace/NTrace-core/tar.gz/v$(PKG_VERSION)?
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ PKG_NAME:=natmapt
|
|||||||
PKG_UPSTREAM_VERSION:=20260214
|
PKG_UPSTREAM_VERSION:=20260214
|
||||||
PKG_UPSTREAM_GITHASH:=
|
PKG_UPSTREAM_GITHASH:=
|
||||||
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)$(if $(PKG_UPSTREAM_GITHASH),~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH)))
|
PKG_VERSION:=$(PKG_UPSTREAM_VERSION)$(if $(PKG_UPSTREAM_GITHASH),~$(call version_abbrev,$(PKG_UPSTREAM_GITHASH)))
|
||||||
PKG_RELEASE:=11
|
PKG_RELEASE:=12
|
||||||
SCRIPTS_VERSION:=0.2026.01.24
|
SCRIPTS_VERSION:=0.2026.01.24
|
||||||
|
|
||||||
PKG_SOURCE_SUBDIR:=$(PKG_UPSTREAM_NAME)-$(PKG_UPSTREAM_VERSION)
|
PKG_SOURCE_SUBDIR:=$(PKG_UPSTREAM_NAME)-$(PKG_UPSTREAM_VERSION)
|
||||||
@@ -23,7 +23,7 @@ PKG_SOURCE:=$(PKG_SOURCE_SUBDIR).tar.xz
|
|||||||
else
|
else
|
||||||
PKG_SOURCE_PROTO:=git
|
PKG_SOURCE_PROTO:=git
|
||||||
PKG_SOURCE_URL:=https://github.com/heiher/natmap.git
|
PKG_SOURCE_URL:=https://github.com/heiher/natmap.git
|
||||||
PKG_SOURCE_VERSION:=f3b9b2e5d2027eb1c2bd95c0afb582c865d16805
|
PKG_SOURCE_VERSION:=ed69834e8139744abe1b70c341dbd2c3b40ea53e
|
||||||
PKG_MIRROR_HASH:=skip
|
PKG_MIRROR_HASH:=skip
|
||||||
|
|
||||||
PKG_SOURCE:=$(PKG_SOURCE_SUBDIR)-$(PKG_SOURCE_VERSION).tar.gz
|
PKG_SOURCE:=$(PKG_SOURCE_SUBDIR)-$(PKG_SOURCE_VERSION).tar.gz
|
||||||
|
|||||||
Reference in new issue
Block a user