💐 Sync 2026-03-10 20:23:29

This commit is contained in:
github-actions[bot] committed 2026-03-10 20:23:29 +08:00
1 parent 62c1cd7501
commit 52bf5fc663
26 files changed
+3712 -3272

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@ LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +curl +bandix
PKG_MAINTAINER:=timsaya PKG_MAINTAINER:=timsaya
PKG_VERSION:=0.12.6 PKG_VERSION:=0.12.6
PKG_RELEASE:=3 PKG_RELEASE:=4
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
@@ -3118,8 +3118,8 @@ return view.extend({
}; };
var daysText = days.length > 0 ? days.map(function (d) { return dayNames[d] || d; }).join(', ') : '-'; var daysText = days.length > 0 ? days.map(function (d) { return dayNames[d] || d; }).join(', ') : '-';
var uploadLimit = rule.w_tx_r_limit || 0; var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0; var downloadLimit = rule.wan_rx_rate_limit || 0;
// 使用 isRuleActive 函数检查规则是否激活 // 使用 isRuleActive 函数检查规则是否激活
var isActive = isRuleActive(rule); var isActive = isRuleActive(rule);
@@ -3746,8 +3746,8 @@ return view.extend({
}); });
// 设置限速值 // 设置限速值
var uploadLimit = editingRule.w_tx_r_limit || 0; var uploadLimit = editingRule.wan_tx_rate_limit || 0;
var downloadLimit = editingRule.w_rx_r_limit || 0; var downloadLimit = editingRule.wan_rx_rate_limit || 0;
// 根据限速值选择合适的单位 // 根据限速值选择合适的单位
var uploadUnit, uploadValue, downloadUnit, downloadValue; var uploadUnit, uploadValue, downloadUnit, downloadValue;
@@ -4064,8 +4064,8 @@ return view.extend({
var startTime = rule.time_slot && rule.time_slot.start ? rule.time_slot.start : ''; var startTime = rule.time_slot && rule.time_slot.start ? rule.time_slot.start : '';
var endTime = rule.time_slot && rule.time_slot.end ? rule.time_slot.end : ''; var endTime = rule.time_slot && rule.time_slot.end ? rule.time_slot.end : '';
var uploadLimit = rule.w_tx_r_limit || 0; var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0; var downloadLimit = rule.wan_rx_rate_limit || 0;
var ruleItem = E('div', { 'class': 'schedule-rule-item' }, [ var ruleItem = E('div', { 'class': 'schedule-rule-item' }, [
E('div', { 'class': 'schedule-rule-info' }, [ E('div', { 'class': 'schedule-rule-info' }, [
@@ -4262,8 +4262,8 @@ return view.extend({
var downloadLimits = []; var downloadLimits = [];
activeRules.forEach(function (rule) { activeRules.forEach(function (rule) {
var uploadLimit = rule.w_tx_r_limit || 0; var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0; var downloadLimit = rule.wan_rx_rate_limit || 0;
// 只收集非零的限制值 // 只收集非零的限制值
if (uploadLimit > 0) { if (uploadLimit > 0) {
+1 -1
View File
@@ -18,7 +18,7 @@ LUCI_DEPENDS:=+ip-full +tc +nftables +bc +kmod-ifb
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_VERSION:=1.3.0 PKG_VERSION:=1.3.0
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com> PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com>
define Build/Compile define Build/Compile
@@ -1,56 +0,0 @@
#!/bin/sh
# Copyright (C) 2006 OpenWrt.org
# Copyright 2022-2023 sirpdboy <herboy2008@gmail.com>
NAME=eqosplus
IDLIST="/var/$NAME.idlist"
TMPID="/var/$NAME.tmpid"
idlist=`uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' `
check_list() {
i=$1
checki='0'
start_time=$(uci -q get $NAME.@device[$i].timestart 2>/dev/null)
end_time=$(uci -q get $NAME.@device[$i].timeend 2>/dev/null)
wweek=`uci -q get $NAME.@device[$i].week `
current_time=$(date +%H:%M)
current_weekday=$(date +%u)
[ "$start_time" = "$end_time" ] || {
[[ "$start_time" < "$end_time" ]] && { [[ "$current_time" > "$start_time" ]] && [[ "$current_time" < "$end_time" ]] || return ; }
[[ "$start_time" > "$end_time" ]] && { [[ "$current_time" < "$start_time" ]] && [[ "$current_time" > "$end_time" ]] || return ; }
}
for ww in `echo $wweek | sed 's/,/ /g' `; do
if [ $current_weekday = $ww ] || [ 'x0' = x$ww ] ;then
checki='1'
fi
done
return $re
}
idlistusr(){
checki='0'
[ -s $IDLIST ] || return
for list in $idlist ;do
check_list $list
if [ $checki == '1' ] ; then
[ `cat $IDLIST 2>/dev/null | grep "!${list}!" | wc -l ` -gt 0 ] || {
eqosplus add $list
echo "!${list}!" >> $IDLIST ; cat $IDLIST | sort | uniq > $TMPID ;cat $TMPID >$IDLIST ;rm -rf $TMPID
}
else
[ `cat $IDLIST 2>/dev/null | grep "!${list}!" | wc -l ` -gt 0 ] && {
eqosplus del $list
sed -i "/!$list!/d" $IDLIST >/dev/null 2>&1
}
fi
done
}
while :;do
sleep 30
idlistusr
sleep 30
done
@@ -124,6 +124,10 @@ const glossary = {
prefmt: 'rule_%s', prefmt: 'rule_%s',
field: 'rule-providers', field: 'rule-providers',
}, },
inbound: {
prefmt: 'inbound_%s',
field: 'listeners',
},
server: { server: {
prefmt: 'server_%s', prefmt: 'server_%s',
field: 'listeners', field: 'listeners',
@@ -148,7 +152,8 @@ const inbound_type = [
['anytls', _('AnyTLS') + ' - ' + _('TCP')], ['anytls', _('AnyTLS') + ' - ' + _('TCP')],
['tuic', _('TUIC') + ' - ' + _('UDP')], ['tuic', _('TUIC') + ' - ' + _('UDP')],
['hysteria2', _('Hysteria2') + ' - ' + _('UDP')], ['hysteria2', _('Hysteria2') + ' - ' + _('UDP')],
//['tunnel', _('Tunnel') + ' - ' + _('TCP/UDP')] ['trusttunnel', _('TrustTunnel') + ' - ' + _('TCP/UDP')],
['tunnel', _('Tunnel') + ' - ' + _('TCP/UDP')]
]; ];
const ip_version = [ const ip_version = [
@@ -183,6 +188,7 @@ const outbound_type = [
['hysteria2', _('Hysteria2') + ' - ' + _('UDP')], ['hysteria2', _('Hysteria2') + ' - ' + _('UDP')],
['tuic', _('TUIC') + ' - ' + _('UDP')], ['tuic', _('TUIC') + ' - ' + _('UDP')],
['masque', _('Masque') + ' - ' + _('UDP')], // https://blog.cloudflare.com/post-quantum-warp/ ['masque', _('Masque') + ' - ' + _('UDP')], // https://blog.cloudflare.com/post-quantum-warp/
['trusttunnel', _('TrustTunnel') + ' - ' + _('TCP/UDP')],
['wireguard', _('WireGuard') + ' - ' + _('UDP')], ['wireguard', _('WireGuard') + ' - ' + _('UDP')],
['ssh', _('SSH') + ' - ' + _('TCP')] ['ssh', _('SSH') + ' - ' + _('TCP')]
]; ];
File diff suppressed because it is too large. Load diff
@@ -795,7 +795,7 @@ function renderRules(s, uciconfig) {
UIEl.setValue(rule.toString('json')); UIEl.setValue(rule.toString('json'));
} }
o.write = function() {}; o.write = function() {};
//o.depends('SUB-RULE', ''); //o.depends('SUB-RULE', ''); // work on subrules not rules
o.editable = true; o.editable = true;
o = s.option(form.Flag, 'src', _('src'), o = s.option(form.Flag, 'src', _('src'),
@@ -0,0 +1,38 @@
'use strict';
'require form';
'require uci';
'require ui';
'require view';
'require fchomo as hm';
'require fchomo.listeners as lsnr'
return view.extend({
load() {
return Promise.all([
uci.load('fchomo')
]);
},
render(data) {
let m, s, o;
m = new form.Map('fchomo', _('Edit inbound'));
/* Inbound settings START */
s = m.section(hm.GridSection, 'inbound', null);
s.addremove = true;
s.rowcolors = true;
s.sortable = true;
s.nodescriptions = true;
s.hm_modaltitle = [ _('Inbound'), _('Add a inbound') ];
s.hm_prefmt = hm.glossary[s.sectiontype].prefmt;
s.hm_field = hm.glossary[s.sectiontype].field;
s.hm_lowcase_only = false;
lsnr.renderListeners(s, data[0], true);
/* Inbound settings END */
return m.render();
}
});
@@ -255,13 +255,13 @@ return view.extend({
/* hm.validateAuth */ /* hm.validateAuth */
so = ss.taboption('field_general', form.Value, 'username', _('Username')); so = ss.taboption('field_general', form.Value, 'username', _('Username'));
so.validate = hm.validateAuthUsername; so.validate = hm.validateAuthUsername;
so.depends({type: /^(http|socks5|mieru|ssh)$/}); so.depends({type: /^(http|socks5|mieru|trusttunnel|ssh)$/});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'password', _('Password')); so = ss.taboption('field_general', form.Value, 'password', _('Password'));
so.password = true; so.password = true;
so.validate = hm.validateAuthPassword; so.validate = hm.validateAuthPassword;
so.depends({type: /^(http|socks5|mieru|trojan|anytls|hysteria2|tuic|ssh)$/}); so.depends({type: /^(http|socks5|mieru|trojan|anytls|hysteria2|tuic|trusttunnel|ssh)$/});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', hm.TextValue, 'headers', _('HTTP header')); so = ss.taboption('field_general', hm.TextValue, 'headers', _('HTTP header'));
@@ -375,6 +375,12 @@ return view.extend({
so.depends('type', 'mieru'); so.depends('type', 'mieru');
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'mieru_traffic_pattern', _('Traffic pattern'),
_('A base64 string is used to fine-tune network behavior.<br/>Please refer to the <a target="_blank" href="%s" rel="noreferrer noopener">document</a>.')
.format('https://github.com/enfein/mieru/blob/main/docs/traffic-pattern.md'));
so.depends('type', 'mieru');
so.modalonly = true;
/* Sudoku fields */ /* Sudoku fields */
so = ss.taboption('field_general', form.Value, 'sudoku_key', _('Key'), so = ss.taboption('field_general', form.Value, 'sudoku_key', _('Key'),
_('The ED25519 available private key or UUID provided by Sudoku server.')); _('The ED25519 available private key or UUID provided by Sudoku server.'));
@@ -448,18 +454,19 @@ return view.extend({
so.value('stream', _('split-stream') + ' - ' + _('CDN support')); so.value('stream', _('split-stream') + ' - ' + _('CDN support'));
so.value('poll', _('poll') + ' - ' + _('CDN support')); so.value('poll', _('poll') + ' - ' + _('CDN support'));
so.value('auto', _('Auto') + ' - ' + _('CDN support')); so.value('auto', _('Auto') + ' - ' + _('CDN support'));
so.value('ws', _('WebSocket') + ' - ' + _('CDN support'));
so.depends('sudoku_http_mask', '1'); so.depends('sudoku_http_mask', '1');
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'sudoku_http_mask_tls', _('HTTP mask: %s').format(_('TLS'))); so = ss.taboption('field_general', form.Flag, 'sudoku_http_mask_tls', _('HTTP mask: %s').format(_('TLS')));
so.default = so.disabled; so.default = so.disabled;
so.depends({sudoku_http_mask_mode: /^(stream|poll|auto)$/}); so.depends({sudoku_http_mask_mode: /^(stream|poll|auto|ws)$/});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'sudoku_http_mask_host', _('HTTP mask: %s').format(_('Host/SNI override'))); so = ss.taboption('field_general', form.Value, 'sudoku_http_mask_host', _('HTTP mask: %s').format(_('Host/SNI override')));
so.datatype = 'or(hostname, hostport)'; so.datatype = 'or(hostname, hostport)';
so.placeholder = 'example.com[:443]'; so.placeholder = 'example.com[:443]';
so.depends({sudoku_http_mask_mode: /^(stream|poll|auto)$/}); so.depends({sudoku_http_mask_mode: /^(stream|poll|auto|ws)$/});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'sudoku_path_root', _('HTTP root path')); so = ss.taboption('field_general', form.Value, 'sudoku_path_root', _('HTTP root path'));
@@ -475,8 +482,10 @@ return view.extend({
so.validate = function(section_id, value) { so.validate = function(section_id, value) {
const http_mask_mode = this.section.getOption('sudoku_http_mask_mode').formvalue(section_id); const http_mask_mode = this.section.getOption('sudoku_http_mask_mode').formvalue(section_id);
if (http_mask_mode === 'ws' && value !== 'off')
return _('Expecting: %s').format(_('only applies when %s is not %s.').format(_('HTTP mask mode'), _('WebSocket')));
if (value === 'on' && !['stream', 'poll', 'auto'].includes(http_mask_mode)) if (value === 'on' && !['stream', 'poll', 'auto'].includes(http_mask_mode))
return _('Expecting: %s').format(_('only applies when %s is stream/poll/auto.').format(_('HTTP mask mode'))); return _('Expecting: %s').format(_('only applies when %s is %s.').format(_('HTTP mask mode'), _('stream/poll/auto')));
return true; return true;
} }
@@ -512,15 +521,6 @@ return view.extend({
so.depends('type', 'tuic'); so.depends('type', 'tuic');
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.ListValue, 'tuic_congestion_controller', _('Congestion controller'),
_('QUIC congestion controller.'));
so.default = hm.congestion_controller[0][0];
hm.congestion_controller.forEach((res) => {
so.value.apply(so, res);
})
so.depends('type', 'tuic');
so.modalonly = true;
so = ss.taboption('field_general', form.ListValue, 'tuic_udp_relay_mode', _('UDP relay mode'), so = ss.taboption('field_general', form.ListValue, 'tuic_udp_relay_mode', _('UDP relay mode'),
_('UDP packet relay mode.')); _('UDP packet relay mode.'));
so.default = 'native'; so.default = 'native';
@@ -714,12 +714,15 @@ return view.extend({
so.depends('masque_remote_dns_resolve', '1'); so.depends('masque_remote_dns_resolve', '1');
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.ListValue, 'masque_congestion_controller', _('Congestion controller')); /* TrustTunnel fields */
so.default = hm.congestion_controller[0][0]; so = ss.taboption('field_general', form.Flag, 'trusttunnel_health_check', _('Health check'));
hm.congestion_controller.forEach((res) => { so.default = so.enabled;
so.value.apply(so, res); so.depends('type', 'trusttunnel');
}) so.modalonly = true;
so.depends('type', 'masque');
so = ss.taboption('field_general', form.Flag, 'trusttunnel_quic', _('QUIC'));
so.default = so.disabled;
so.depends('type', 'trusttunnel');
so.modalonly = true; so.modalonly = true;
/* WireGuard fields */ /* WireGuard fields */
@@ -848,9 +851,17 @@ return view.extend({
so.modalonly = true; so.modalonly = true;
/* Extra fields */ /* Extra fields */
so = ss.taboption('field_general', form.ListValue, 'congestion_controller', _('Congestion controller'));
so.default = hm.congestion_controller[0][0];
hm.congestion_controller.forEach((res) => {
so.value.apply(so, res);
})
so.depends({type: /^(tuic|masque|trusttunnel)$/});
so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'udp', _('UDP')); so = ss.taboption('field_general', form.Flag, 'udp', _('UDP'));
so.default = so.disabled; so.default = so.disabled;
so.depends({type: /^(direct|socks5|ss|mieru|vmess|vless|trojan|anytls|masque|wireguard)$/}); so.depends({type: /^(direct|socks5|ss|mieru|vmess|vless|trojan|anytls|trusttunnel|masque|wireguard)$/});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'uot', _('UoT'), so = ss.taboption('field_general', form.Flag, 'uot', _('UoT'),
@@ -941,7 +952,7 @@ return view.extend({
let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input'); let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input');
// Force enabled // Force enabled
if (['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic'].includes(type)) { if (['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic', 'trusttunnel'].includes(type)) {
tls.checked = true; tls.checked = true;
tls.disabled = true; tls.disabled = true;
} else { } else {
@@ -950,7 +961,7 @@ return view.extend({
return true; return true;
} }
so.depends({type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic)$/}); so.depends({type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|trusttunnel)$/});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_tls', form.Flag, 'tls_disable_sni', _('Disable SNI'), so = ss.taboption('field_tls', form.Flag, 'tls_disable_sni', _('Disable SNI'),
@@ -961,7 +972,7 @@ return view.extend({
so = ss.taboption('field_tls', form.Value, 'tls_sni', _('TLS SNI'), so = ss.taboption('field_tls', form.Value, 'tls_sni', _('TLS SNI'),
_('Used to verify the hostname on the returned certificates.')); _('Used to verify the hostname on the returned certificates.'));
so.depends({tls: '1', type: /^(http|vmess|vless|trojan|anytls|hysteria|hysteria2)$/}); so.depends({tls: '1', type: /^(http|vmess|vless|trojan|anytls|hysteria|hysteria2|trusttunnel)$/});
so.depends({tls: '1', tls_disable_sni: '0', type: /^(tuic)$/}); so.depends({tls: '1', tls_disable_sni: '0', type: /^(tuic)$/});
so.modalonly = true; so.modalonly = true;
@@ -991,6 +1002,9 @@ return view.extend({
case 'anytls': case 'anytls':
def_alpn = ['h2', 'http/1.1']; def_alpn = ['h2', 'http/1.1'];
break; break;
case 'trusttunnel':
def_alpn = ['h3', 'h2'];
break;
default: default:
def_alpn = []; def_alpn = [];
} }
@@ -1000,7 +1014,7 @@ return view.extend({
return true; return true;
} }
so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls|hysteria|hysteria2|tuic)$/}); so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|trusttunnel)$/});
so.depends({type: 'ss', plugin: 'shadow-tls'}); so.depends({type: 'ss', plugin: 'shadow-tls'});
so.modalonly = true; so.modalonly = true;
@@ -1022,7 +1036,7 @@ return view.extend({
'<br/>' + '<br/>' +
_('This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!')); _('This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!'));
so.default = so.disabled; so.default = so.disabled;
so.depends({tls: '1', type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic)$/}); so.depends({tls: '1', type: /^(http|socks5|vmess|vless|trojan|anytls|hysteria|hysteria2|tuic|trusttunnel)$/});
so.modalonly = true; so.modalonly = true;
so = ss.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path') + _(' (mTLS)'), so = ss.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path') + _(' (mTLS)'),
@@ -1076,7 +1090,7 @@ return view.extend({
hm.tls_client_fingerprints.forEach((res) => { hm.tls_client_fingerprints.forEach((res) => {
so.value.apply(so, res); so.value.apply(so, res);
}) })
so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls)$/}); so.depends({tls: '1', type: /^(vmess|vless|trojan|anytls|trusttunnel)$/});
so.depends({type: 'ss', plugin: /^(shadow-tls|restls)$/}); so.depends({type: 'ss', plugin: /^(shadow-tls|restls)$/});
so.modalonly = true; so.modalonly = true;
@@ -6,125 +6,17 @@
'require view'; 'require view';
'require fchomo as hm'; 'require fchomo as hm';
'require fchomo.listeners as lsnr'
const CBIDummyCopyValue = hm.CopyValue.extend({
__name__: 'CBI.DummyCopyValue',
renderWidget(/* ... */) {
let node = hm.CopyValue.prototype.renderWidget.apply(this, arguments);
node.firstChild.style.width = '30em';
return node;
},
write: function() {}
});
class VlessEncryption {
// origin:
// https://github.com/XTLS/Xray-core/pull/5067
// server:
// https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L64
// https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/server.go#L42
// client:
// https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/factory.go#L12
// https://github.com/muink/mihomo/blob/7917f24f428e40ac20b8b8f953b02cf59d1be334/transport/vless/encryption/client.go#L45
/*
{
"method": "mlkem768x25519plus",
"xormode": "native",
"ticket": "600s",
"rtt": "0rtt",
"paddings": [ // Optional
"100-111-1111",
"75-0-111",
"50-0-3333",
...
],
"keypairs": [
{
"type": "vless-x25519",
"server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8",
"client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk"
},
{
"type": "vless-mlkem768",
"server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog",
"client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5Jm0Ge71Fc-A3aLpaS3C3pARbGQoquUDUEVDNwEWjQTvFpGTUV3Nddw_LlRmWN6Wqhguti9cpS6GhEmkBvBayFeHgZosuaQ1FMoAqIeQzSSSoguCZtGLUmdQjEs3zc5rwG1rNanbhtyI3QnooYvr3A0vggIkbmddjtjwYaVQdMAj9Moavc12EAUajOV91QA73RWVuhelbe7pLumsHiW-emIdBgVhEgDDYdGaLq1E8QjB0WbIfufnJp-CJa3Ieu9gmDASTlQBeEREeA9gfoZcTpYD8elhJIJxaPJKXchvUVkFhZarcivlKoqVuaFPzsJM7KQCBC8zfS0t_oiBka-uzg3_Hl153nMTDaCAbZULPZGE-p2EazI2eFBCDktdHtDffJNo7i7ZYSkWkqN9ysr2QZRvYG_PYCzcYSo34Gf5WNvHKuz0Ye3kFkckfuirCmzr3knw2azrSOmpTOX_RSlMlse7HgFYwxHPMJnzPS19ymiwKZPgrAMvCmAUZmsxZGDoKeusNEDGSSFhLcTQys20qGBGYasIgKYGjAKGjK7SCxSOCGBQSU496XBkXQEeOB7k9Sh8jdB0pQGAZw9Ntwvrts2DjIUcsQBv-XEGfnHQXoBmDgzwzYEWxeHd0oNbPIlz7CqvNseoKu6uPZl85xynum6aWd6BDDAtwobbqYkuMUfOUhXf_cH13kWSnuJ6QrOxah94JzAnda3tWRDQ3RajOOjk-OXhbOqi8QMJRFdA_C-xMwQalM_rTSTKOqyCcaNSTkVmMlmyOt90tptk7jKUizDmGhGbsSU8WMY5mhdZ3eUd5O6gQitiMHI1EqnlaRNsXnKFoJ5yHV82Wp1dhFONCG_dlpqunVJD5bFgpxtdFDD-KmXQTymAalFjxeVl_xdc5xd4XYCYmk5dhEiQBE0J_S3Z6x0tmFORpWG9lESK_OBRSul9oKZh9Vet-UZ8FSOVtNFwbeokRwWpFuFL1dL3UpJeININ2cgUfDNWQlwItkokiFf_Kdy12y2O_hqJtoTpNttNxTOiclDzKM1KHNOjYJgTgydcid3mmJl3eA6ezyrDAw1RLCHBucIvYRfwbkmpYMvnfAaA2DIiaTNaSxX8BUl92V49UVKWlQSp8ijfmmTRHrBMmxKjvBIgHqC6dSMhVUEOMzCKXAO3giCS3eZzdrNQGhhqTxpYYnFf6uLoKOIiaGY-ByI1YoIVXxX8aCTOOpesFvHjwOKBEoj4Hoxd3iFMUJQazR7P2drnfmS11kgipM7pSUgB7POKwxEF0NQCedM41wVIuoathAqD6N6qalwQ6iOKlZOBUwwMVAMRDJ3aomG37ZeLYhv6fB0-pUUJSN1q4knjtkLFIJSUrih9FZ0XnOll_aeEgOICqQkb4aOMrovjcJEWvgdjUqGPdyIGgkurfqBRHih3dukUcYxt6Y__4KLQ7acqMx0FOFv0ZxFRTCIRGj_GAlFWUi6fpuPKebXUnEn1PRE0iNXwUV_4jESWb0"
},
...
]
}
*/
constructor(payload) {
this.input = payload || '';
try {
let content = JSON.parse(this.input.trim());
Object.keys(content).forEach(key => this[key] = content[key]);
} catch {}
this.method ||= hm.vless_encryption.methods[0][0];
this.xormode ||= hm.vless_encryption.xormodes[0][0];
this.ticket ||= hm.vless_encryption.tickets[0][0];
this.rtt ||= hm.vless_encryption.rtts[0][0];
this.paddings ||= [];
this.keypairs ||= [];
}
setKey(key, value) {
this[key] = value;
return this
}
_toMihomo(payload, side) {
if (!['server', 'client'].includes(side))
throw new Error('Unknown side: ' + side); // `Unknown side: '${side}'`
let required = [
payload.method,
payload.xormode,
side === 'server' ? payload.ticket : side === 'client' ? payload.rtt : null
].join('.');
return required +
(hm.isEmpty(payload.paddings) ? '' : '.' + payload.paddings.join('.')) + // Optional
(hm.isEmpty(payload.keypairs) ? '' : '.' + payload.keypairs.map(e => e[side]).join('.')); // Required
}
toString(format, side) {
format ||= 'json';
let payload = hm.removeBlankAttrs({
method: this.method,
xormode: this.xormode,
ticket: this.ticket,
rtt: this.rtt,
paddings: this.paddings || [],
keypairs: this.keypairs || []
});
if (format === 'json')
return JSON.stringify(payload);
else if (format === 'mihomo')
return this._toMihomo(payload, side);
else
throw new Error(`Unknown format: '${format}'`);
}
}
return view.extend({ return view.extend({
load() { load() {
return Promise.all([ return Promise.all([
uci.load('fchomo'), uci.load('fchomo')
hm.getFeatures()
]); ]);
}, },
render(data) { render(data) {
const dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo'); const dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo');
const features = data[1];
let m, s, o; let m, s, o;
@@ -168,845 +60,7 @@ return view.extend({
s.hm_field = hm.glossary[s.sectiontype].field; s.hm_field = hm.glossary[s.sectiontype].field;
s.hm_lowcase_only = false; s.hm_lowcase_only = false;
s.tab('field_general', _('General fields')); lsnr.renderListeners(s, data[0], false);
s.tab('field_vless_encryption', _('Vless Encryption fields'));
s.tab('field_tls', _('TLS fields'));
s.tab('field_transport', _('Transport fields'));
s.tab('field_multiplex', _('Multiplex fields'));
s.tab('field_listen', _('Listen fields'));
/* General fields */
o = s.taboption('field_general', form.Value, 'label', _('Label'));
o.load = hm.loadDefaultLabel;
o.validate = hm.validateUniqueValue;
o.modalonly = true;
o = s.taboption('field_general', form.Flag, 'enabled', _('Enable'));
o.default = o.enabled;
o.editable = true;
o = s.taboption('field_general', form.Flag, 'auto_firewall', _('Firewall'),
_('Auto configure firewall'));
o.default = o.enabled;
o.editable = true;
o = s.taboption('field_general', form.ListValue, 'type', _('Type'));
o.default = hm.inbound_type[0][0];
hm.inbound_type.forEach((res) => {
o.value.apply(o, res);
})
o = s.taboption('field_general', form.Value, 'listen', _('Listen address'));
o.datatype = 'ipaddr';
o.placeholder = '::';
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'port', _('Listen port') + ' / ' + _('Ports pool'));
o.datatype = 'or(port, portrange)';
//o.placeholder = '1080,2079-2080,3080'; // @fw4 does not support port lists with commas
o.rmempty = false;
//o.validate = hm.validateCommonPort; // @fw4 does not support port lists with commas
// @dev: Features under development
// @rule
// @proxy
/* HTTP / SOCKS fields */
/* hm.validateAuth */
o = s.taboption('field_general', form.Value, 'username', _('Username'));
o.validate = hm.validateAuthUsername;
o.depends({type: /^(http|socks|mixed|mieru|trojan|anytls|hysteria2)$/});
o.modalonly = true;
o = s.taboption('field_general', hm.GenValue, 'password', _('Password'));
o.password = true;
o.validate = hm.validateAuthPassword;
o.rmempty = false;
o.depends({type: /^(http|socks|mixed|mieru|trojan|anytls|hysteria2)$/, username: /.+/});
o.depends({type: /^(tuic)$/, uuid: /.+/});
o.modalonly = true;
/* Hysteria2 fields */
o = s.taboption('field_general', form.Value, 'hysteria_up_mbps', _('Max upload speed'),
_('In Mbps.'));
o.datatype = 'uinteger';
o.depends('type', 'hysteria2');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'hysteria_down_mbps', _('Max download speed'),
_('In Mbps.'));
o.datatype = 'uinteger';
o.depends('type', 'hysteria2');
o.modalonly = true;
o = s.taboption('field_general', form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'),
_('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.'));
o.default = o.disabled;
o.depends({type: 'hysteria2', hysteria_up_mbps: '', hysteria_down_mbps: ''});
o.modalonly = true;
o = s.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type'));
o.value('', _('Disable'));
o.value('salamander', _('Salamander'));
o.depends('type', 'hysteria2');
o.modalonly = true;
o = s.taboption('field_general', hm.GenValue, 'hysteria_obfs_password', _('Obfuscate password'),
_('Enabling obfuscation will make the server incompatible with standard QUIC connections, losing the ability to masquerade with HTTP/3.'));
o.password = true;
o.rmempty = false;
o.depends('type', 'hysteria');
o.depends({type: 'hysteria2', hysteria_obfs_type: /.+/});
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'hysteria_masquerade', _('Masquerade'),
_('HTTP3 server behavior when authentication fails.<br/>A 404 page will be returned if empty.'));
o.placeholder = 'file:///var/www or http://127.0.0.1:8080'
o.depends('type', 'hysteria2');
o.modalonly = true;
/* Shadowsocks fields */
o = s.taboption('field_general', form.ListValue, 'shadowsocks_chipher', _('Chipher'));
o.default = hm.shadowsocks_cipher_methods[1][0];
hm.shadowsocks_cipher_methods.forEach((res) => {
o.value.apply(o, res);
})
o.depends('type', 'shadowsocks');
o.modalonly = true;
o = s.taboption('field_general', hm.GenValue, 'shadowsocks_password', _('Password'));
o.password = true;
o.validate = function(section_id, value) {
const encmode = this.section.getOption('shadowsocks_chipher').formvalue(section_id);
return hm.validateShadowsocksPassword.call(this, encmode, section_id, value);
}
o.depends({type: 'shadowsocks', shadowsocks_chipher: /.+/});
o.modalonly = true;
/* Mieru fields */
o = s.taboption('field_general', form.ListValue, 'mieru_transport', _('Transport'));
o.default = 'TCP';
o.value('TCP');
o.value('UDP');
o.depends('type', 'mieru');
o.modalonly = true;
/* Sudoku fields */
const sudoku_keytypes = [
['sudoku-keypair', _('sudoku-keypair')],
['uuid', _('UUID')]
]
o = s.taboption('field_general', hm.GenValue, 'sudoku_key', _('Key'),
_('The ED25519 master public key or UUID generated by Sudoku.'));
o.hm_options = {
type: sudoku_keytypes[0][0],
callback: function(result) {
if (result.uuid)
return [
[this.option, result.uuid],
['sudoku_client_key', result.uuid]
]
else
return [
[this.option, result.public_key],
['sudoku_client_key', result.private_key]
]
}
}
o.renderWidget = function(section_id, option_index, cfgvalue) {
let node = form.Value.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
const cbid = this.cbid(section_id) + '._keytype_select';
const selected = this.hm_options.type;
let selectEl = E('select', {
id: cbid,
class: 'cbi-input-select',
style: 'width: 10em',
});
sudoku_keytypes.forEach(([k, v]) => {
selectEl.appendChild(E('option', {
'value': k,
'selected': (k === selected) ? '' : null
}, [ v ]));
});
node.appendChild(E('div', { 'class': 'control-group' }, [
selectEl,
E('button', {
class: 'cbi-button cbi-button-add',
click: ui.createHandlerFn(this, () => {
this.hm_options.type = document.getElementById(cbid).value;
return hm.handleGenKey.call(this, this.hm_options);
})
}, [ _('Generate') ])
]));
return node;
}
o.rmempty = false;
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', hm.CopyValue, 'sudoku_client_key', _('Client key'));
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', form.ListValue, 'sudoku_aead_method', _('Chipher'));
o.default = hm.sudoku_cipher_methods[0][0];
hm.sudoku_cipher_methods.forEach((res) => {
o.value.apply(o, res);
})
o.validate = function(section_id, value) {
const pure_downlink = this.section.getUIElement(section_id, 'sudoku_enable_pure_downlink')?.node.querySelector('input').checked;
if (value === 'none' && pure_downlink === false)
return _('Expecting: %s').format(_('Chipher must be enabled if obfuscate downlink is disabled.'));
return true;
}
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', form.ListValue, 'sudoku_table_type', _('Obfuscate type'));
o.value('prefer_ascii', _('Obfuscated as ASCII data stream'));
o.value('prefer_entropy', _('Obfuscated as low-entropy data stream'));
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', form.DynamicList, 'sudoku_custom_tables', _('Custom byte layout'));
o.renderWidget = function(/* ... */) {
let node = form.DynamicList.prototype.renderWidget.apply(this, arguments);
(node.querySelector('.control-group') || node).appendChild(E('button', {
class: 'cbi-button cbi-button-positive',
title: _('Generate'),
click: ui.createHandlerFn(this, hm.handleGenKey, this.hm_options || this.option)
}, [ _('Generate') ]));
return node;
}
o.validate = hm.validateSudokuCustomTable;
o.depends('sudoku_table_type', 'prefer_entropy');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'sudoku_padding_min', _('Minimum padding rate'));
o.datatype = 'and(uinteger, range(0, 100))';
o.default = 1;
o.rmempty = false;
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'sudoku_padding_max', _('Maximum padding rate'));
o.datatype = 'and(uinteger, range(0, 100))';
o.default = 15;
o.rmempty = false;
o.validate = function(section_id, value) {
const padding_min = this.section.getOption('sudoku_padding_min').formvalue(section_id);
if (value < padding_min)
return _('Expecting: %s').format(_('Maximum padding rate must be greater than or equal to the minimum padding rate.'));
return true;
}
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'sudoku_handshake_timeout', _('Handshake timeout'),
_('In seconds.'));
o.datatype = 'uinteger';
o.placeholder = 5;
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', form.Flag, 'sudoku_enable_pure_downlink', _('Enable obfuscate for downlink'),
_('false = bandwidth optimized downlink; true = pure Sudoku downlink.'));
o.default = o.enabled;
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', form.Flag, 'sudoku_http_mask', _('HTTP mask'));
o.default = o.enabled;
o.depends('type', 'sudoku');
o.modalonly = true;
o = s.taboption('field_general', form.ListValue, 'sudoku_http_mask_mode', _('HTTP mask mode'));
o.default = 'legacy';
o.value('legacy', _('Legacy'));
o.value('stream', _('split-stream') + ' - ' + _('CDN support'));
o.value('poll', _('poll') + ' - ' + _('CDN support'));
o.value('auto', _('Auto') + ' - ' + _('CDN support'));
o.depends('sudoku_http_mask', '1');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'sudoku_path_root', _('HTTP root path'));
o.depends('sudoku_http_mask', '1');
o.modalonly = true;
/* Tuic fields */
o = s.taboption('field_general', hm.GenValue, 'uuid', _('UUID'));
o.rmempty = false;
o.validate = hm.validateUUID;
o.depends('type', 'tuic');
o.modalonly = true;
o = s.taboption('field_general', form.ListValue, 'tuic_congestion_controller', _('Congestion controller'),
_('QUIC congestion controller.'));
o.default = hm.congestion_controller[0][0];
hm.congestion_controller.forEach((res) => {
o.value.apply(o, res);
})
o.depends('type', 'tuic');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'tuic_max_udp_relay_packet_size', _('Max UDP relay packet size'));
o.datatype = 'uinteger';
o.default = '1500';
o.depends('type', 'tuic');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'tuic_max_idle_time', _('Idle timeout'),
_('In seconds.'));
o.default = '15000';
o.validate = hm.validateTimeDuration;
o.depends('type', 'tuic');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'tuic_authentication_timeout', _('Auth timeout'),
_('In seconds.'));
o.default = '1000';
o.validate = hm.validateTimeDuration;
o.depends('type', 'tuic');
o.modalonly = true;
/* Trojan fields */
o = s.taboption('field_general', form.Flag, 'trojan_ss_enabled', _('Shadowsocks encrypt'));
o.default = o.disabled;
o.depends('type', 'trojan');
o.modalonly = true;
o = s.taboption('field_general', form.ListValue, 'trojan_ss_chipher', _('Shadowsocks chipher'));
o.default = hm.trojan_cipher_methods[0][0];
hm.trojan_cipher_methods.forEach((res) => {
o.value.apply(o, res);
})
o.depends({type: 'trojan', trojan_ss_enabled: '1'});
o.modalonly = true;
o = s.taboption('field_general', hm.GenValue, 'trojan_ss_password', _('Shadowsocks password'));
o.password = true;
o.validate = function(section_id, value) {
const encmode = this.section.getOption('trojan_ss_chipher').formvalue(section_id);
return hm.validateShadowsocksPassword.call(this, encmode, section_id, value);
}
o.depends({type: 'trojan', trojan_ss_enabled: '1'});
o.modalonly = true;
/* AnyTLS fields */
o = s.taboption('field_general', form.TextValue, 'anytls_padding_scheme', _('Padding scheme'));
o.depends('type', 'anytls');
o.modalonly = true;
/* VMess / VLESS fields */
o = s.taboption('field_general', hm.GenValue, 'vmess_uuid', _('UUID'));
o.rmempty = false;
o.validate = hm.validateUUID;
o.depends({type: /^(vmess|vless)$/});
o.modalonly = true;
o = s.taboption('field_general', form.ListValue, 'vless_flow', _('Flow'));
o.default = hm.vless_flow[0][0];
hm.vless_flow.forEach((res) => {
o.value.apply(o, res);
})
o.depends('type', 'vless');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'vmess_alterid', _('Alter ID'),
_('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.'));
o.datatype = 'uinteger';
o.placeholder = '0';
o.depends('type', 'vmess');
o.modalonly = true;
/* Plugin fields */
o = s.taboption('field_general', form.ListValue, 'plugin', _('Plugin'));
o.value('', _('none'));
o.value('shadow-tls', _('shadow-tls'));
//o.value('kcp-tun', _('kcp-tun'));
o.depends('type', 'shadowsocks');
o.modalonly = true;
o = s.taboption('field_general', form.Value, 'plugin_opts_handshake_dest', _('Plugin: ') + _('Handshake target that supports TLS 1.3'));
o.datatype = 'hostport';
o.placeholder = 'cloud.tencent.com:443';
o.rmempty = false;
o.depends({plugin: 'shadow-tls'});
o.modalonly = true;
o = s.taboption('field_general', hm.GenValue, 'plugin_opts_thetlspassword', _('Plugin: ') + _('Password'));
o.password = true;
o.rmempty = false;
o.depends({plugin: 'shadow-tls'});
o.modalonly = true;
o = s.taboption('field_general', form.ListValue, 'plugin_opts_shadowtls_version', _('Plugin: ') + _('Version'));
o.value('1', _('v1'));
o.value('2', _('v2'));
o.value('3', _('v3'));
o.default = '3';
o.depends({plugin: 'shadow-tls'});
o.modalonly = true;
/* Extra fields */
o = s.taboption('field_general', form.Flag, 'udp', _('UDP'));
o.default = o.disabled;
o.depends({type: /^(socks|mixed|shadowsocks)$/});
o.modalonly = true;
/* Vless Encryption fields */
o = s.taboption('field_general', form.Flag, 'vless_decryption', _('decryption'));
o.default = o.disabled;
o.depends('type', 'vless');
o.modalonly = true;
const initVlessEncryptionOption = function(o, key) {
o.load = function(section_id) {
return new VlessEncryption(uci.get(data[0], section_id, 'vless_encryption_hmpayload'))[key];
}
o.onchange = function(ev, section_id, value) {
let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload');
let newpayload = new VlessEncryption(UIEl.getValue()).setKey(key, value);
UIEl.setValue(newpayload.toString());
[
['server', '_vless_encryption_decryption'],
['client', '_vless_encryption_encryption']
].forEach(([side, option]) => {
UIEl = this.section.getUIElement(section_id, option);
UIEl.setValue(newpayload.toString('mihomo', side));
});
}
o.write = function() {};
}
o = s.taboption('field_vless_encryption', form.Value, 'vless_encryption_hmpayload', _('Payload'));
o.readonly = true;
o.depends('vless_decryption', '1');
o.modalonly = true;
o = s.taboption('field_vless_encryption', CBIDummyCopyValue, '_vless_encryption_decryption', _('decryption'));
o.depends('vless_decryption', '1');
o.modalonly = true;
o = s.taboption('field_vless_encryption', CBIDummyCopyValue, '_vless_encryption_encryption', _('encryption'));
o.depends('vless_decryption', '1');
o.modalonly = true;
o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_method', _('Encryption method'));
o.default = hm.vless_encryption.methods[0][0];
hm.vless_encryption.methods.forEach((res) => {
o.value.apply(o, res);
})
initVlessEncryptionOption(o, 'method');
o.depends('vless_decryption', '1');
o.modalonly = true;
o = s.taboption('field_vless_encryption', form.RichListValue, 'vless_encryption_xormode', _('XOR mode'));
o.default = hm.vless_encryption.xormodes[0][0];
hm.vless_encryption.xormodes.forEach((res) => {
o.value.apply(o, res);
})
initVlessEncryptionOption(o, 'xormode');
o.depends('vless_decryption', '1');
o.modalonly = true;
o = s.taboption('field_vless_encryption', hm.RichValue, 'vless_encryption_ticket', _('Server') +' '+ _('RTT'));
o.default = hm.vless_encryption.tickets[0][0];
hm.vless_encryption.tickets.forEach((res) => {
o.value.apply(o, res);
})
initVlessEncryptionOption(o, 'ticket');
o.validate = function(section_id, value) {
if (!value)
return true;
if (!value.match(/^(\d+-)?\d+s$/))
return _('Expecting: %s').format('^(\\d+-)?\\d+s$');
return true;
}
o.rmempty = false;
o.depends('vless_decryption', '1');
o.modalonly = true;
o = s.taboption('field_vless_encryption', form.ListValue, 'vless_encryption_rtt', _('Client') +' '+ _('RTT'));
o.default = hm.vless_encryption.rtts[0][0];
hm.vless_encryption.rtts.forEach((res) => {
o.value.apply(o, res);
})
initVlessEncryptionOption(o, 'rtt');
o.rmempty = false;
o.depends('vless_decryption', '1');
o.modalonly = true;
o = s.taboption('field_vless_encryption', hm.less_25_12 ? hm.DynamicList : form.DynamicList, 'vless_encryption_paddings', _('Paddings'), // @less_25_12
_('The server and client can set different padding parameters.') + '</br>' +
_('In the order of one <code>Padding-Length</code> and one <code>Padding-Interval</code>, infinite concatenation.') + '</br>' +
_('The first padding must have a probability of 100% and at least 35 bytes.'));
hm.vless_encryption.paddings.forEach((res) => {
o.value.apply(o, res);
})
initVlessEncryptionOption(o, 'paddings');
o.validate = function(section_id, value) {
if (!value)
return true;
if (!value.match(/^\d+(-\d+){2}$/))
return _('Expecting: %s').format('^\\d+(-\\d+){2}$');
return true;
}
o.allowduplicates = true;
o.depends('vless_decryption', '1');
o.modalonly = true;
o = s.taboption('field_vless_encryption', hm.GenText, 'vless_encryption_keypairs', _('Keypairs'));
o.placeholder = '[\n {\n "type": "vless-x25519",\n "server": "cP5Oy9MOpTaBKKE17Pfd56mbb1CIfp5EMpyBYqr2EG8",\n "client": "khEcQMT8j41xWmGYKpZtQ4vd8_9VWyFVmmCDIhRJ-Uk"\n },\n {\n "type": "vless-mlkem768",\n "server": "UHPx3nf-FVxF95byAw0YG025aQNw9HxKej-MiG5AhTcdW_WFpHlTVYQU5NHmXP6tmljSnB2iPmSQ29fisGxEog",\n "client": "h4sdZgCc5-ZefvQ8mZmReOWQdxYb0mwngMdl7pKhYEZZpGWHUPKAmxug87Bgj3GqSHs195QeVpxfrMLNB5J..."\n },\n ...\n]';
o.rows = 10;
o.hm_options = {
type: hm.vless_encryption.keypairs.types[0][0],
params: '',
callback: function(result) {
const section_id = this.section.section;
const key_type = this.hm_options.type;
let keypair = {"type": key_type, "server": "", "client": ""};
switch (key_type) {
case 'vless-x25519':
keypair.server = result.private_key;
keypair.client = result.password;
break;
case 'vless-mlkem768':
keypair.server = result.seed;
keypair.client = result.client;
break;
default:
break;
}
let keypairs = [];
try {
keypairs = JSON.parse(this.formvalue(section_id).trim());
} catch {}
if (!Array.isArray(keypairs))
keypairs = [];
keypairs.push(keypair);
return [
[this.option, JSON.stringify(keypairs, null, 2)]
]
}
}
o.renderWidget = function(section_id, option_index, cfgvalue) {
let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
const cbid = this.cbid(section_id) + '._keytype_select';
const selected = this.hm_options.type;
let selectEl = E('select', {
id: cbid,
class: 'cbi-input-select',
style: 'width: 10em',
});
hm.vless_encryption.keypairs.types.forEach(([k, v]) => {
selectEl.appendChild(E('option', {
'value': k,
'selected': (k === selected) ? '' : null
}, [ v ]));
});
node.appendChild(E('div', { 'class': 'control-group' }, [
selectEl,
E('button', {
class: 'cbi-button cbi-button-add',
click: ui.createHandlerFn(this, () => {
this.hm_options.type = document.getElementById(cbid).value;
return hm.handleGenKey.call(this, this.hm_options);
})
}, [ _('Generate') ])
]));
return node;
}
o.load = function(section_id) {
return JSON.stringify(new VlessEncryption(uci.get(data[0], section_id, 'vless_encryption_hmpayload'))['keypairs'], null, 2);
}
o.validate = function(section_id, value) {
let result = hm.validateJson.call(this, section_id, value);
if (result === true) {
let keypairs = JSON.parse(value.trim());
if (Array.isArray(keypairs) && keypairs.length >= 1) {
let UIEl = this.section.getUIElement(section_id, 'vless_encryption_hmpayload');
let newpayload = new VlessEncryption(UIEl.getValue()).setKey('keypairs', keypairs);
UIEl.setValue(newpayload.toString());
[
['server', '_vless_encryption_decryption'],
['client', '_vless_encryption_encryption']
].forEach(([side, option]) => {
UIEl = this.section.getUIElement(section_id, option);
UIEl.setValue(newpayload.toString('mihomo', side));
});
} else
return _('Expecting: %s').format(_('least one keypair required'));
return true;
} else
return result;
}
o.rmempty = false;
o.depends('vless_decryption', '1');
o.modalonly = true;
/* TLS fields */
o = s.taboption('field_general', form.Flag, 'tls', _('TLS'));
o.default = o.disabled;
o.validate = function(section_id, value) {
const type = this.section.getOption('type').formvalue(section_id);
let tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input');
let tls_alpn = this.section.getUIElement(section_id, 'tls_alpn');
let tls_reality = this.section.getUIElement(section_id, 'tls_reality').node.querySelector('input');
// Force enabled
if (['trojan', 'anytls', 'tuic', 'hysteria2'].includes(type)) {
tls.checked = true;
tls.disabled = true;
if (['tuic', 'hysteria2'].includes(type) && !`${tls_alpn.getValue()}`)
tls_alpn.setValue('h3');
} else {
tls.removeAttribute('disabled');
}
// Force disabled
if (!['vmess', 'vless', 'trojan'].includes(type)) {
tls_reality.checked = false;
tls_reality.disabled = true;
} else {
tls_reality.removeAttribute('disabled');
}
return true;
}
o.depends({type: /^(http|socks|mixed|vmess|vless|trojan|anytls|tuic|hysteria2)$/});
o.modalonly = true;
o = s.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'),
_('List of supported application level protocols, in order of preference.'));
o.depends('tls', '1');
o.modalonly = true;
o = s.taboption('field_tls', form.Value, 'tls_cert_path', _('Certificate path'),
_('The %s public key, in PEM format.').format(_('Server')));
o.value('/etc/fchomo/certs/server_publickey.pem');
o.depends({tls: '1', tls_reality: '0'});
o.rmempty = false;
o.modalonly = true;
o = s.taboption('field_tls', form.Button, '_upload_cert', _('Upload certificate'),
_('<strong>Save your configuration before uploading files!</strong>'));
o.inputstyle = 'action';
o.inputtitle = _('Upload...');
o.depends({tls: '1', tls_cert_path: '/etc/fchomo/certs/server_publickey.pem'});
o.onclick = L.bind(hm.uploadCertificate, o, _('certificate'), 'server_publickey');
o.modalonly = true;
o = s.taboption('field_tls', form.Value, 'tls_key_path', _('Key path'),
_('The %s private key, in PEM format.').format(_('Server')));
o.value('/etc/fchomo/certs/server_privatekey.pem');
o.rmempty = false;
o.depends({tls: '1', tls_cert_path: /.+/});
o.modalonly = true;
o = s.taboption('field_tls', form.Button, '_upload_key', _('Upload key'),
_('<strong>Save your configuration before uploading files!</strong>'));
o.inputstyle = 'action';
o.inputtitle = _('Upload...');
o.depends({tls: '1', tls_key_path: '/etc/fchomo/certs/server_privatekey.pem'});
o.onclick = L.bind(hm.uploadCertificate, o, _('private key'), 'server_privatekey');
o.modalonly = true;
o = s.taboption('field_tls', form.ListValue, 'tls_client_auth_type', _('Client Auth type') + _(' (mTLS)'));
o.default = hm.tls_client_auth_types[0][0];
hm.tls_client_auth_types.forEach((res) => {
o.value.apply(o, res);
})
o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/});
o.modalonly = true;
o = s.taboption('field_tls', form.Value, 'tls_client_auth_cert_path', _('Client Auth Certificate path') + _(' (mTLS)'),
_('The %s public key, in PEM format.').format(_('Client')));
o.value('/etc/fchomo/certs/client_publickey.pem');
o.validate = function(/* ... */) {
return hm.validateMTLSClientAuth.call(this, 'tls_client_auth_type', ...arguments);
}
o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/});
o.modalonly = true;
o = s.taboption('field_tls', form.Button, '_upload_client_auth_cert', _('Upload certificate') + _(' (mTLS)'),
_('<strong>Save your configuration before uploading files!</strong>'));
o.inputstyle = 'action';
o.inputtitle = _('Upload...');
o.depends({tls: '1', tls_client_auth_cert_path: '/etc/fchomo/certs/client_publickey.pem'});
o.onclick = L.bind(hm.uploadCertificate, o, _('certificate'), 'client_publickey');
o.modalonly = true;
o = s.taboption('field_tls', hm.GenText, 'tls_ech_key', _('ECH key'));
o.placeholder = '-----BEGIN ECH KEYS-----\nACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK\nmadSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz\ndC5jb20AAA==\n-----END ECH KEYS-----';
o.hm_placeholder = 'outer-sni.any.domain';
o.cols = 30;
o.rows = 2;
o.hm_options = {
type: 'ech-keypair',
params: '',
callback: function(result) {
return [
[this.option, result.ech_key],
['tls_ech_config', result.ech_cfg]
]
}
}
o.renderWidget = function(section_id, option_index, cfgvalue) {
let node = hm.TextValue.prototype.renderWidget.call(this, section_id, option_index, cfgvalue);
const cbid = this.cbid(section_id) + '._outer_sni';
node.appendChild(E('div', { 'class': 'control-group' }, [
E('input', {
id: cbid,
class: 'cbi-input-text',
style: 'width: 10em',
placeholder: this.hm_placeholder
}),
E('button', {
class: 'cbi-button cbi-button-add',
click: ui.createHandlerFn(this, () => {
this.hm_options.params = document.getElementById(cbid).value;
return hm.handleGenKey.call(this, this.hm_options);
})
}, [ _('Generate') ])
]));
return node;
}
o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/});
o.modalonly = true;
o = s.taboption('field_tls', hm.CopyValue, 'tls_ech_config', _('ECH config'),
_('This ECH parameter needs to be added to the HTTPS record of the domain.'));
o.placeholder = 'AEn+DQBFKwAgACABWIHUGj4u+PIggYXcR5JF0gYk3dCRioBW8uJq9H4mKAAIAAEAAQABAANAEnB1YmxpYy50bHMtZWNoLmRldgAA';
o.depends({tls: '1', type: /^(http|socks|mixed|vmess|vless|trojan|anytls|hysteria2|tuic)$/});
o.modalonly = true;
// uTLS fields
o = s.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY'));
o.default = o.disabled;
o.depends('tls', '1');
o.modalonly = true;
o = s.taboption('field_tls', form.Value, 'tls_reality_dest', _('REALITY handshake server'));
o.datatype = 'hostport';
o.placeholder = 'cloud.tencent.com:443';
o.rmempty = false;
o.depends('tls_reality', '1');
o.modalonly = true;
o = s.taboption('field_tls', hm.GenValue, 'tls_reality_private_key', _('REALITY private key'));
o.hm_options = {
type: 'reality-keypair',
callback: function(result) {
return [
[this.option, result.private_key],
['tls_reality_public_key', result.public_key]
]
}
}
o.password = true;
o.rmempty = false;
o.depends('tls_reality', '1');
o.modalonly = true;
o = s.taboption('field_tls', hm.CopyValue, 'tls_reality_public_key', _('REALITY public key'));
o.depends('tls_reality', '1');
o.modalonly = true;
o = s.taboption('field_tls', form.DynamicList, 'tls_reality_short_id', _('REALITY short ID'));
//o.value('', '""');
o.rmempty = false;
o.depends('tls_reality', '1');
o.modalonly = true;
o = s.taboption('field_tls', form.DynamicList, 'tls_reality_server_names', _('REALITY certificate issued to'));
o.datatype = 'list(hostname)';
o.placeholder = 'cloud.tencent.com';
o.rmempty = false;
o.depends('tls_reality', '1');
o.modalonly = true;
/* Transport fields */
o = s.taboption('field_general', form.Flag, 'transport_enabled', _('Transport'));
o.default = o.disabled;
o.depends({type: /^(vmess|vless|trojan)$/});
o.modalonly = true;
o = s.taboption('field_transport', form.ListValue, 'transport_type', _('Transport type'));
o.value('grpc', _('gRPC'));
o.value('ws', _('WebSocket'));
o.validate = function(section_id, value) {
const type = this.section.getOption('type').formvalue(section_id);
switch (type) {
case 'vmess':
case 'vless':
if (!['http', 'h2', 'grpc', 'ws'].includes(value))
return _('Expecting: only support %s.').format(_('HTTP') +
' / ' + _('HTTPUpgrade') +
' / ' + _('gRPC') +
' / ' + _('WebSocket'));
break;
case 'trojan':
if (!['grpc', 'ws'].includes(value))
return _('Expecting: only support %s.').format(_('gRPC') +
' / ' + _('WebSocket'));
break;
default:
break;
}
return true;
}
o.depends('transport_enabled', '1');
o.modalonly = true;
o = s.taboption('field_transport', form.Value, 'transport_path', _('Request path'));
o.placeholder = '/';
o.default = '/';
o.rmempty = false;
o.depends({transport_enabled: '1', transport_type: 'ws'});
o.modalonly = true;
o = s.taboption('field_transport', form.Value, 'transport_grpc_servicename', _('gRPC service name'));
o.placeholder = 'GunService';
o.rmempty = false;
o.depends({transport_enabled: '1', transport_type: 'grpc'});
o.modalonly = true;
/* Server settings END */ /* Server settings END */
return m.render(); return m.render();
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+34 -24
View File
@@ -45,6 +45,31 @@ opmc() { # @less_25_12
$OPM $action "$@" $OPM $action "$@"
} }
# add_firewall
add_firewall() {
local enabled auto_firewall listen port
config_get_bool enabled "$1" "enabled" "1"
config_get_bool auto_firewall "$1" "auto_firewall" "1"
config_get listen "$1" "listen" "::"
config_get port "$1" "port"
[ "$enabled" = "0" ] && return 0
[ "$auto_firewall" = "0" ] && return 0
json_add_object ''
json_add_string type rule
json_add_string target ACCEPT
json_add_string name "$1"
#json_add_string family '' # '' = IPv4 and IPv6
json_add_string proto 'tcp udp'
json_add_string direction in
json_add_string src "*"
#json_add_string dest '' # '' = input
json_add_string dest_ip "$(echo "$listen" | grep -vE '^(0\.\d+\.\d+\.\d+|::)$')"
json_add_string dest_port "$port"
json_close_object
}
config_load "$CONF" config_load "$CONF"
# define global var: DEF_WAN DEF_WAN6 NIC_* NIC6_* # define global var: DEF_WAN DEF_WAN6 NIC_* NIC6_*
@@ -333,6 +358,15 @@ start_service() {
procd_set_param stderr 1 procd_set_param stderr 1
procd_set_param respawn procd_set_param respawn
# add_firewall
procd_open_data
# configure firewall
json_add_array firewall
# meta l4proto %s th dport %s counter accept comment "!%s: accept server instance [%s]"
config_foreach add_firewall "inbound"
json_close_array
procd_close_data
procd_close_instance procd_close_instance
fi fi
fi fi
@@ -380,30 +414,6 @@ start_service() {
procd_set_param respawn procd_set_param respawn
# add_firewall # add_firewall
add_firewall() {
local enabled auto_firewall listen port
config_get_bool enabled "$1" "enabled" "1"
config_get_bool auto_firewall "$1" "auto_firewall" "1"
config_get listen "$1" "listen" "::"
config_get port "$1" "port"
[ "$enabled" = "0" ] && return 0
[ "$auto_firewall" = "0" ] && return 0
json_add_object ''
json_add_string type rule
json_add_string target ACCEPT
json_add_string name "$1"
#json_add_string family '' # '' = IPv4 and IPv6
json_add_string proto 'tcp udp'
json_add_string direction in
json_add_string src "*"
#json_add_string dest '' # '' = input
json_add_string dest_ip "$(echo "$listen" | grep -vE '^(0\.\d+\.\d+\.\d+|::)$')"
json_add_string dest_port "$port"
json_close_object
}
#
procd_open_data procd_open_data
# configure firewall # configure firewall
json_add_array firewall json_add_array firewall
@@ -34,6 +34,22 @@ migrate() {
uci_set "$CONF" "$1" vless_encryption_encryption "$vless_encryption" uci_set "$CONF" "$1" vless_encryption_encryption "$vless_encryption"
fi fi
fi fi
# tuic_congestion_controller -> congestion_controller
if isDefined "$1" tuic_congestion_controller; then
local tuic_congestion_controller
config_get tuic_congestion_controller "$1" tuic_congestion_controller ""
uci_remove "$CONF" "$1" tuic_congestion_controller
uci_set "$CONF" "$1" congestion_controller "$tuic_congestion_controller"
fi
# masque_congestion_controller -> congestion_controller
if isDefined "$1" masque_congestion_controller; then
local masque_congestion_controller
config_get masque_congestion_controller "$1" masque_congestion_controller ""
uci_remove "$CONF" "$1" masque_congestion_controller
uci_set "$CONF" "$1" congestion_controller "$masque_congestion_controller"
fi
} }
config_foreach migrate node config_foreach migrate node
@@ -0,0 +1,33 @@
#!/bin/sh
# Migration script for fchomo server
# Used to migrate LuCI application server option.
. /lib/functions.sh
. /usr/share/libubox/jshn.sh
CONF=fchomo
config_load "$CONF"
# isDefined <section> <option>
isDefined() {
local opt=CONFIG_${1}_${2};
eval "[ -n \"\${$opt+x}\" ] && return 0 || return 1"
}
migrate() {
# tuic_congestion_controller -> congestion_controller
if isDefined "$1" tuic_congestion_controller; then
local tuic_congestion_controller
config_get tuic_congestion_controller "$1" tuic_congestion_controller ""
uci_remove "$CONF" "$1" tuic_congestion_controller
uci_set "$CONF" "$1" congestion_controller "$tuic_congestion_controller"
fi
}
config_foreach migrate server
uci_commit "$CONF"
exit 0
@@ -11,6 +11,7 @@ import { urldecode, urlencode } from 'luci.http';
import { import {
isEmpty, strToBool, strToInt, bytesizeToByte, durationToSecond, isEmpty, strToBool, strToInt, bytesizeToByte, durationToSecond,
arrToObj, removeBlankAttrs, arrToObj, removeBlankAttrs,
parseListener,
HM_DIR, RUN_DIR, PRESET_OUTBOUND, RULES_LOGICAL_TYPE HM_DIR, RUN_DIR, PRESET_OUTBOUND, RULES_LOGICAL_TYPE
} from 'fchomo'; } from 'fchomo';
@@ -39,6 +40,7 @@ const ucisniff = 'sniff',
ucidnspoli = 'dns_policy', ucidnspoli = 'dns_policy',
ucidnsnpoli = 'dns_node_policy', ucidnsnpoli = 'dns_node_policy',
ucipgrp = 'proxy_group', ucipgrp = 'proxy_group',
uciinbd = 'inbound',
ucinode = 'node', ucinode = 'node',
uciprov = 'provider', uciprov = 'provider',
ucichain = 'dialer_proxy', ucichain = 'dialer_proxy',
@@ -326,29 +328,14 @@ uci.foreach(uciconf, ucisniff, (cfg) => {
/* Inbound START */ /* Inbound START */
const proxy_mode = uci.get(uciconf, uciinbound, 'proxy_mode') || 'redir_tproxy'; const proxy_mode = uci.get(uciconf, uciinbound, 'proxy_mode') || 'redir_tproxy';
/* Listen ports */ /* Listen ports */
config.listeners = []; config["allow-lan"] = true;
push(config.listeners, { config["bind-address"] = "*";
name: 'mixed-in', config["mixed-port"] = strToInt(uci.get(uciconf, uciinbound, 'mixed_port')) || 7890;
type: 'mixed',
port: strToInt(uci.get(uciconf, uciinbound, 'mixed_port')) || 7890,
listen: '::',
udp: true
});
if (match(proxy_mode, /redir/)) if (match(proxy_mode, /redir/))
push(config.listeners, { config["redir-port"] = strToInt(uci.get(uciconf, uciinbound, 'redir_port')) || 7891;
name: 'redir-in',
type: 'redir',
port: strToInt(uci.get(uciconf, uciinbound, 'redir_port')) || 7891,
listen: '::'
});
if (match(proxy_mode, /tproxy/)) if (match(proxy_mode, /tproxy/))
push(config.listeners, { config["tproxy-port"] = strToInt(uci.get(uciconf, uciinbound, 'tproxy_port')) || 7892;
name: 'tproxy-in', config.listeners = [];
type: 'tproxy',
port: strToInt(uci.get(uciconf, uciinbound, 'tproxy_port')) || 7892,
listen: '::',
udp: true
});
push(config.listeners, { push(config.listeners, {
name: 'dns-in', name: 'dns-in',
type: 'tunnel', type: 'tunnel',
@@ -357,6 +344,13 @@ push(config.listeners, {
network: ['tcp', 'udp'], network: ['tcp', 'udp'],
target: '1.1.1.1:53' target: '1.1.1.1:53'
}); // @Not required for v1.19.2+ }); // @Not required for v1.19.2+
/* Custom Inbound settings */
uci.foreach(uciconf, uciinbd, (cfg) => {
if (cfg.enabled === '0')
return;
push(config.listeners, parseListener(cfg, true, get_proxygroup(cfg.proxy)));
});
/* Tun settings */ /* Tun settings */
if (match(proxy_mode, /tun/)) if (match(proxy_mode, /tun/))
config.tun = { config.tun = {
@@ -502,7 +496,6 @@ uci.foreach(uciconf, ucinode, (cfg) => {
cipher: cfg.vmess_chipher || cfg.shadowsocks_chipher, cipher: cfg.vmess_chipher || cfg.shadowsocks_chipher,
password: cfg.shadowsocks_password || cfg.password, password: cfg.shadowsocks_password || cfg.password,
headers: cfg.headers ? json(cfg.headers) : null, headers: cfg.headers ? json(cfg.headers) : null,
"congestion-controller": cfg.tuic_congestion_controller || cfg.masque_congestion_controller,
"private-key": cfg.masque_private_key || cfg.wireguard_private_key || cfg.ssh_priv_key, "private-key": cfg.masque_private_key || cfg.wireguard_private_key || cfg.ssh_priv_key,
"public-key": cfg.masque_endpoint_public_key || cfg.wireguard_peer_public_key, "public-key": cfg.masque_endpoint_public_key || cfg.wireguard_peer_public_key,
ip: cfg.masque_ip || cfg.wireguard_ip, ip: cfg.masque_ip || cfg.wireguard_ip,
@@ -531,6 +524,7 @@ uci.foreach(uciconf, ucinode, (cfg) => {
transport: cfg.mieru_transport, transport: cfg.mieru_transport,
multiplexing: cfg.mieru_multiplexing, multiplexing: cfg.mieru_multiplexing,
"handshake-mode": cfg.mieru_handshake_mode, "handshake-mode": cfg.mieru_handshake_mode,
"traffic-pattern": cfg.mieru_traffic_pattern,
/* Sudoku */ /* Sudoku */
key: cfg.sudoku_key, key: cfg.sudoku_key,
@@ -540,12 +534,16 @@ uci.foreach(uciconf, ucinode, (cfg) => {
"table-type": cfg.sudoku_table_type, "table-type": cfg.sudoku_table_type,
"custom-tables": cfg.sudoku_custom_tables, "custom-tables": cfg.sudoku_custom_tables,
"enable-pure-downlink": (cfg.sudoku_enable_pure_downlink === '0') ? false : null, "enable-pure-downlink": (cfg.sudoku_enable_pure_downlink === '0') ? false : null,
"http-mask": (cfg.sudoku_http_mask === '0') ? false : true, ...(cfg.type === 'sudoku' ? {
"http-mask-mode": cfg.sudoku_http_mask_mode, httpmask: (cfg.sudoku_http_mask === '0') ? { disable: true } : {
"http-mask-tls": strToBool(cfg.sudoku_http_mask_tls), disable: false,
"http-mask-host": cfg.sudoku_http_mask_host, mode: cfg.sudoku_http_mask_mode,
"path-root": cfg.sudoku_path_root, tls: strToBool(cfg.sudoku_http_mask_tls) || false,
"http-mask-multiplex": cfg.sudoku_http_mask_multiplex, host: cfg.sudoku_http_mask_host,
path_root: cfg.sudoku_path_root,
multiplex: cfg.sudoku_http_mask_multiplex,
}
} : {}),
/* Snell */ /* Snell */
psk: cfg.snell_psk, psk: cfg.snell_psk,
@@ -587,6 +585,10 @@ uci.foreach(uciconf, ucinode, (cfg) => {
"packet-encoding": cfg.vmess_packet_encoding, "packet-encoding": cfg.vmess_packet_encoding,
encryption: cfg.vless_encryption === '1' ? cfg.vless_encryption_encryption : null, encryption: cfg.vless_encryption === '1' ? cfg.vless_encryption_encryption : null,
/* TrustTunnel */
"health-check": strToBool(cfg.trusttunnel_health_check === '0' ? false : true),
quic: strToBool(cfg.trusttunnel_quic),
/* WireGuard */ /* WireGuard */
"pre-shared-key": cfg.wireguard_pre_shared_key, "pre-shared-key": cfg.wireguard_pre_shared_key,
"allowed-ips": cfg.wireguard_allowed_ips, "allowed-ips": cfg.wireguard_allowed_ips,
@@ -605,12 +607,13 @@ uci.foreach(uciconf, ucinode, (cfg) => {
} : null, } : null,
/* Extra fields */ /* Extra fields */
"congestion-controller": cfg.congestion_controller,
udp: strToBool(cfg.udp), udp: strToBool(cfg.udp),
"udp-over-tcp": strToBool(cfg.uot), "udp-over-tcp": strToBool(cfg.uot),
"udp-over-tcp-version": cfg.uot_version, "udp-over-tcp-version": cfg.uot_version,
/* TLS fields */ /* TLS fields */
tls: (cfg.type in ['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic']) ? null : strToBool(cfg.tls), tls: (cfg.type in ['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic', 'trusttunnel']) ? null : strToBool(cfg.tls),
"disable-sni": strToBool(cfg.tls_disable_sni), "disable-sni": strToBool(cfg.tls_disable_sni),
...arrToObj([[(cfg.type in ['vmess', 'vless']) ? 'servername' : 'sni', cfg.tls_sni]]), ...arrToObj([[(cfg.type in ['vmess', 'vless']) ? 'servername' : 'sni', cfg.tls_sni]]),
fingerprint: cfg.tls_fingerprint, fingerprint: cfg.tls_fingerprint,
@@ -7,6 +7,7 @@ import { cursor } from 'uci';
import { import {
isEmpty, strToBool, strToInt, durationToSecond, isEmpty, strToBool, strToInt, durationToSecond,
arrToObj, removeBlankAttrs, arrToObj, removeBlankAttrs,
parseListener,
HM_DIR, RUN_DIR, PRESET_OUTBOUND HM_DIR, RUN_DIR, PRESET_OUTBOUND
} from 'fchomo'; } from 'fchomo';
@@ -33,23 +34,6 @@ function parse_users(cfg) {
return users; return users;
} }
function parse_vless_encryption(payload, side) {
if (isEmpty(payload))
return null;
let content = json(trim(payload));
let required = join('.', [
content.method,
content.xormode,
side === 'server' ? content.ticket : side === 'client' ? content.rtt : null
]);
return required +
(isEmpty(content.paddings) ? '' : '.' + join('.', content.paddings)) + // Optional
(isEmpty(content.keypairs) ? '' : '.' + join('.', map(content.keypairs, e => e[side]))); // Required
}
/* Config helper END */ /* Config helper END */
/* Main */ /* Main */
@@ -61,131 +45,7 @@ uci.foreach(uciconf, uciserver, (cfg) => {
if (cfg.enabled === '0') if (cfg.enabled === '0')
return; return;
push(config.listeners, { push(config.listeners, parseListener(cfg, false));
name: cfg['.name'],
type: cfg.type,
listen: cfg.listen || '::',
port: cfg.port,
proxy: 'DIRECT',
/* HTTP / SOCKS / VMess / VLESS / Trojan / AnyTLS / Tuic / Hysteria2 */
users: (cfg.type in ['http', 'socks', 'mixed', 'vmess', 'vless', 'trojan']) ? [
{
/* HTTP / SOCKS */
username: cfg.username,
password: cfg.password,
/* VMess / VLESS */
uuid: cfg.vmess_uuid,
flow: cfg.vless_flow,
alterId: strToInt(cfg.vmess_alterid)
}
/*{
}*/
] : ((cfg.type in ['mieru', 'anytls', 'tuic', 'hysteria2']) ? {
/* Mieru / AnyTLS / Hysteria2 */
...arrToObj([[cfg.username, cfg.password]]),
/* Tuic */
...arrToObj([[cfg.uuid, cfg.password]])
} : null),
/* Hysteria2 */
up: strToInt(cfg.hysteria_up_mbps),
down: strToInt(cfg.hysteria_down_mbps),
"ignore-client-bandwidth": strToBool(cfg.hysteria_ignore_client_bandwidth),
obfs: cfg.hysteria_obfs_type,
"obfs-password": cfg.hysteria_obfs_password,
masquerade: cfg.hysteria_masquerade,
/* Shadowsocks */
cipher: cfg.shadowsocks_chipher,
password: cfg.shadowsocks_password,
/* Mieru */
transport: cfg.mieru_transport,
/* Sudoku */
key: cfg.sudoku_key,
"aead-method": replace(cfg.sudoku_aead_method || '', 'chacha20-ietf-poly1305', 'chacha20-poly1305') || null,
"padding-min": strToInt(cfg.sudoku_padding_min),
"padding-max": strToInt(cfg.sudoku_padding_max),
"table-type": cfg.sudoku_table_type,
"custom-tables": cfg.sudoku_custom_tables,
"handshake-timeout": strToInt(cfg.sudoku_handshake_timeout) ?? null,
"enable-pure-downlink": (cfg.sudoku_enable_pure_downlink === '0') ? false : null,
"disable-http-mask": (cfg.sudoku_http_mask === '0') ? true : null,
"http-mask-mode": cfg.sudoku_http_mask_mode,
"path-root": cfg.sudoku_path_root,
/* Tuic */
"congestion-controller": cfg.tuic_congestion_controller,
"max-idle-time": durationToSecond(cfg.tuic_max_idle_time),
"authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout),
"max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size),
/* Trojan */
"ss-option": cfg.trojan_ss_enabled === '1' ? {
enabled: true,
method: cfg.trojan_ss_chipher,
password: cfg.trojan_ss_password
} : null,
/* AnyTLS */
"padding-scheme": cfg.anytls_padding_scheme,
/* VMess / VLESS */
decryption: cfg.vless_decryption === '1' ? parse_vless_encryption(cfg.vless_encryption_hmpayload, 'server') : null,
/* Plugin fields */
...(cfg.plugin ? {
// shadow-tls
"shadow-tls": cfg.plugin === 'shadow-tls' ? {
enable: true,
version: strToInt(cfg.plugin_opts_shadowtls_version),
...(strToInt(cfg.plugin_opts_shadowtls_version) >= 3 ? {
users: [
{
name: 1,
password: cfg.plugin_opts_thetlspassword
}
],
} : { password: cfg.plugin_opts_thetlspassword }),
handshake: {
dest: cfg.plugin_opts_handshake_dest
},
} : null
} : {}),
/* Extra fields */
udp: strToBool(cfg.udp),
/* TLS fields */
...(cfg.tls === '1' ? {
alpn: cfg.tls_alpn,
...(cfg.tls_reality === '1' ? {
"reality-config": {
dest: cfg.tls_reality_dest,
"private-key": cfg.tls_reality_private_key,
"short-id": cfg.tls_reality_short_id,
"server-names": cfg.tls_reality_server_names
}
} : {
certificate: cfg.tls_cert_path,
"private-key": cfg.tls_key_path
}),
"client-auth-type": cfg.tls_client_auth_type,
"client-auth-cert": cfg.tls_client_auth_cert_path,
"ech-key": cfg.tls_ech_key,
} : {}),
/* Transport fields */
...(cfg.transport_enabled === '1' ? {
"grpc-service-name": cfg.transport_grpc_servicename,
"ws-path": cfg.transport_path
} : {})
});
}); });
/* Inbound END */ /* Inbound END */
@@ -34,6 +34,14 @@
"path": "fchomo/hosts" "path": "fchomo/hosts"
} }
}, },
"admin/services/fchomo/inbound": {
"title": "Inbound",
"order": 27,
"action": {
"type": "view",
"path": "fchomo/inbound"
}
},
"admin/services/fchomo/node": { "admin/services/fchomo/node": {
"title": "Node", "title": "Node",
"order": 30, "order": 30,
@@ -191,3 +191,163 @@ export function removeBlankAttrs(res) {
return content; return content;
}; };
/* String helper end */ /* String helper end */
/* String universal start */
export function parseVlessEncryption(payload, side) {
if (isEmpty(payload))
return null;
let content = json(trim(payload));
let required = join('.', [
content.method,
content.xormode,
side === 'server' ? content.ticket : side === 'client' ? content.rtt : null
]);
return required +
(isEmpty(content.paddings) ? '' : '.' + join('.', content.paddings)) + // Optional
(isEmpty(content.keypairs) ? '' : '.' + join('.', map(content.keypairs, e => e[side]))); // Required
};
export function parseListener(cfg, isClient, label) {
return {
name: cfg['.name'],
type: cfg.type,
listen: cfg.listen || '::',
port: cfg.port,
...(isClient ? {
rule: cfg.rule,
proxy: label,
} : {}),
/* HTTP / SOCKS / VMess / VLESS / Trojan / AnyTLS / Tuic / Hysteria2 */
users: (cfg.type in ['http', 'socks', 'mixed', 'vmess', 'vless', 'trojan', 'trusttunnel']) ? [
{
/* HTTP / SOCKS */
username: cfg.username,
password: cfg.password,
/* VMess / VLESS */
uuid: cfg.vmess_uuid,
flow: cfg.vless_flow,
alterId: strToInt(cfg.vmess_alterid)
}
/*{
}*/
] : ((cfg.type in ['mieru', 'anytls', 'tuic', 'hysteria2']) ? {
/* Mieru / AnyTLS / Hysteria2 */
...arrToObj([[cfg.username, cfg.password]]),
/* Tuic */
...arrToObj([[cfg.uuid, cfg.password]])
} : null),
/* Hysteria2 */
up: strToInt(cfg.hysteria_up_mbps),
down: strToInt(cfg.hysteria_down_mbps),
"ignore-client-bandwidth": strToBool(cfg.hysteria_ignore_client_bandwidth),
obfs: cfg.hysteria_obfs_type,
"obfs-password": cfg.hysteria_obfs_password,
masquerade: cfg.hysteria_masquerade,
/* Shadowsocks */
cipher: cfg.shadowsocks_chipher,
password: cfg.shadowsocks_password,
/* Mieru */
transport: cfg.mieru_transport,
"traffic-pattern": cfg.mieru_traffic_pattern,
/* Sudoku */
key: cfg.sudoku_key,
"aead-method": replace(cfg.sudoku_aead_method || '', 'chacha20-ietf-poly1305', 'chacha20-poly1305') || null,
"padding-min": strToInt(cfg.sudoku_padding_min),
"padding-max": strToInt(cfg.sudoku_padding_max),
"table-type": cfg.sudoku_table_type,
"custom-tables": cfg.sudoku_custom_tables,
"handshake-timeout": strToInt(cfg.sudoku_handshake_timeout) ?? null,
"enable-pure-downlink": (cfg.sudoku_enable_pure_downlink === '0') ? false : null,
...(cfg.type === 'sudoku' ? {
httpmask: (cfg.sudoku_http_mask === '0') ? { disable: true } : {
disable: false,
mode: cfg.sudoku_http_mask_mode,
path_root: cfg.sudoku_path_root,
}
} : {}),
fallback: (cfg.sudoku_http_mask === '0') ? null : cfg.sudoku_fallback,
/* Tuic */
"max-idle-time": durationToSecond(cfg.tuic_max_idle_time),
"authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout),
"max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size),
/* Trojan */
"ss-option": cfg.trojan_ss_enabled === '1' ? {
enabled: true,
method: cfg.trojan_ss_chipher,
password: cfg.trojan_ss_password
} : null,
/* AnyTLS */
"padding-scheme": cfg.anytls_padding_scheme,
/* VMess / VLESS */
decryption: cfg.vless_decryption === '1' ? parseVlessEncryption(cfg.vless_encryption_hmpayload, 'server') : null,
/* Tunnel */
target: cfg.tunnel_target,
/* Plugin fields */
...(cfg.plugin ? {
// shadow-tls
"shadow-tls": cfg.plugin === 'shadow-tls' ? {
enable: true,
version: strToInt(cfg.plugin_opts_shadowtls_version),
...(strToInt(cfg.plugin_opts_shadowtls_version) >= 3 ? {
users: [
{
name: 1,
password: cfg.plugin_opts_thetlspassword
}
],
} : { password: cfg.plugin_opts_thetlspassword }),
handshake: {
dest: cfg.plugin_opts_handshake_dest
},
} : null
} : {}),
/* Extra fields */
"congestion-controller": cfg.congestion_controller,
network: cfg.network,
udp: strToBool(cfg.udp),
/* TLS fields */
...(cfg.tls === '1' ? {
alpn: cfg.tls_alpn,
...(cfg.tls_reality === '1' ? {
"reality-config": {
dest: cfg.tls_reality_dest,
"private-key": cfg.tls_reality_private_key,
"short-id": cfg.tls_reality_short_id,
"server-names": cfg.tls_reality_server_names
}
} : {
certificate: cfg.tls_cert_path,
"private-key": cfg.tls_key_path
}),
"client-auth-type": cfg.tls_client_auth_type,
"client-auth-cert": cfg.tls_client_auth_cert_path,
"ech-key": cfg.tls_ech_key,
} : {}),
/* Transport fields */
...(cfg.transport_enabled === '1' ? {
"grpc-service-name": cfg.transport_grpc_servicename,
"ws-path": cfg.transport_path
} : {})
}
};
/* String universal end */
+2 -2
View File
@@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
LUCI_TITLE:=kai LUCI_TITLE:=kai
PKG_VERSION:=1.0.0 PKG_VERSION:=1.0.1
PKG_RELEASE:=2 PKG_RELEASE:=3
LUCI_DEPENDS:=+kai +kai_session +luci-compat LUCI_DEPENDS:=+kai +kai_session +luci-compat
LUCI_MINIFY_CSS:=0 LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0 LUCI_MINIFY_JS:=0
+10 -1
View File
@@ -9,11 +9,20 @@ s.anonymous=true
s:option(Flag, "enabled", translate("Enable")).rmempty=false s:option(Flag, "enabled", translate("Enable")).rmempty=false
local kai_model = require "luci.model.kai"
local blocks = kai_model.blocks()
local home = kai_model.home()
local data_dir = s:option(Value, "data_dir", translate("Data directory")) local data_dir = s:option(Value, "data_dir", translate("Data directory"))
data_dir.placeholder = ""
data_dir.rmempty = false data_dir.rmempty = false
data_dir.description = translate("Required. KAI session will store cwd/cache/data/config/state under this directory (subfolders: cwd, cache, data, config, state).") data_dir.description = translate("Required. KAI session will store cwd/cache/data/config/state under this directory (subfolders: cwd, cache, data, config, state).")
local paths, default_path = kai_model.find_paths(blocks, home, "Configs")
for _, val in pairs(paths) do
data_dir:value(val, val)
end
data_dir.default = default_path
local port = s:option(Value, "port", translate("API port")) local port = s:option(Value, "port", translate("API port"))
port.default = "8197" port.default = "8197"
port.rmempty = false port.rmempty = false
+54
View File
@@ -0,0 +1,54 @@
local jsonc = require "luci.jsonc"
local kai = {}
kai.blocks = function()
local f = io.popen("lsblk -s -f -b -o NAME,FSSIZE,MOUNTPOINT --json", "r")
local vals = {}
if f then
local ret = f:read("*all")
f:close()
local obj = jsonc.parse(ret)
for _, val in pairs(obj and obj["blockdevices"] or {}) do
local fsize = val["fssize"]
if fsize ~= nil and string.len(fsize) > 10 and val["mountpoint"] then
vals[#vals + 1] = val["mountpoint"]
end
end
end
return vals
end
kai.home = function()
local uci = require "luci.model.uci".cursor()
local home_dirs = {}
home_dirs["main_dir"] = uci:get_first("quickstart", "main", "main_dir", "/root")
home_dirs["Configs"] = uci:get_first("quickstart", "main", "conf_dir", home_dirs["main_dir"] .. "/Configs")
home_dirs["Public"] = uci:get_first("quickstart", "main", "pub_dir", home_dirs["main_dir"] .. "/Public")
home_dirs["Downloads"] = uci:get_first("quickstart", "main", "dl_dir", home_dirs["Public"] .. "/Downloads")
home_dirs["Caches"] = uci:get_first("quickstart", "main", "tmp_dir", home_dirs["main_dir"] .. "/Caches")
return home_dirs
end
kai.find_paths = function(blocks, home_dirs, path_name)
local default_path = ""
local paths = {}
default_path = home_dirs[path_name] .. "/KAI"
if #blocks == 0 then
table.insert(paths, default_path)
else
for _, val in pairs(blocks) do
table.insert(paths, val .. "/" .. path_name .. "/KAI")
end
local without_conf_dir = "/root/" .. path_name .. "/KAI"
if default_path == without_conf_dir then
default_path = paths[1]
end
end
return paths, default_path
end
return kai
+1 -1
View File
@@ -11,7 +11,7 @@ LUCI_TITLE:=LuCI Support for Automatic Partition Mount
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
LUCI_DEPENDS:=+fdisk +block-mount +bc +blkid +parted +btrfs-progs +losetup +resize2fs +e2fsprogs +f2fs-tools +kmod-loop LUCI_DEPENDS:=+fdisk +block-mount +bc +blkid +parted +btrfs-progs +losetup +resize2fs +e2fsprogs +f2fs-tools +kmod-loop
PKG_VERSION:=2.0.3 PKG_VERSION:=2.0.3
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_LICENSE:=Apache-2.0 PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Sirpdboy <herboy2008@gmail.com> PKG_MAINTAINER:=Sirpdboy <herboy2008@gmail.com>
+3 -9
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
# #
# Copyright (C) 2021-2025 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/luci-app-partexp # Copyright (C) 2021-2026 sirpdboy <herboy2008@gmail.com> https://github.com/sirpdboy/luci-app-partexp
# #
# This is free software, licensed under the Apache License, Version 2.0 . # This is free software, licensed under the Apache License, Version 2.0 .
# #
@@ -279,8 +279,8 @@ check_part_space() {
check_free_space() { check_free_space() {
DISK=$1 DISK=$1
PARTED_OUTPUT=$(parted -s /dev/$DISK unit GB print free 2>/dev/null) PARTED_OUTPUT=$(parted -s /dev/$DISK unit GB print free 2>/dev/null)
FREE_SPACE=$(echo "$PARTED_OUTPUT" | grep "Free Space" | awk '{print $3}' ) FREE_SPACE=$(echo "$PARTED_OUTPUT" | awk '/Free Space/ {last=$3} END {print last}' | sed 's/GB$//')
echo $FREE_SPACE |awk -F '.' '{print $1}' | sed 's/[A-Za-z]//g' echo $FREE_SPACE | cut -d'.' -f1
} }
show_partition_info() { show_partition_info() {
@@ -418,12 +418,6 @@ rootfs_resize() {
fi fi
} }
get_config() {
config_get target_function $1 target_function 1
config_get target_disk $1 target_disk 1
config_get_bool keep_config $1 keep_config 1
config_get format_type $1 format_type
}
# 修改 fdiskB 函数,使用环境变量参数 # 修改 fdiskB 函数,使用环境变量参数
fdiskB() { fdiskB() {
+2 -2
View File
@@ -6,12 +6,12 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=redsocks2 PKG_NAME:=redsocks2
PKG_VERSION:=0.71 PKG_VERSION:=0.71
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/semigodking/redsocks.git PKG_SOURCE_URL:=https://github.com/semigodking/redsocks.git
PKG_SOURCE_DATE:=2025-12-12 PKG_SOURCE_DATE:=2025-12-12
PKG_SOURCE_VERSION:=33c37b37c0524ef01e3f2653a6c47a10f783cc39 PKG_SOURCE_VERSION:=43ba8a3cb1b5f8c44b2a827b924375f1de733370
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_MAINTAINER:=semigodking <semigodking@gmail.com> PKG_MAINTAINER:=semigodking <semigodking@gmail.com>