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

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

View File

@ -11,7 +11,7 @@ LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +curl +bandix
PKG_MAINTAINER:=timsaya
PKG_VERSION:=0.12.6
PKG_RELEASE:=3
PKG_RELEASE:=4
include $(TOPDIR)/feeds/luci/luci.mk

View File

@ -3118,8 +3118,8 @@ return view.extend({
};
var daysText = days.length > 0 ? days.map(function (d) { return dayNames[d] || d; }).join(', ') : '-';
var uploadLimit = rule.w_tx_r_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0;
var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.wan_rx_rate_limit || 0;
// 使用 isRuleActive 函数检查规则是否激活
var isActive = isRuleActive(rule);
@ -3746,8 +3746,8 @@ return view.extend({
});
// 设置限速值
var uploadLimit = editingRule.w_tx_r_limit || 0;
var downloadLimit = editingRule.w_rx_r_limit || 0;
var uploadLimit = editingRule.wan_tx_rate_limit || 0;
var downloadLimit = editingRule.wan_rx_rate_limit || 0;
// 根据限速值选择合适的单位
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 endTime = rule.time_slot && rule.time_slot.end ? rule.time_slot.end : '';
var uploadLimit = rule.w_tx_r_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0;
var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.wan_rx_rate_limit || 0;
var ruleItem = E('div', { 'class': 'schedule-rule-item' }, [
E('div', { 'class': 'schedule-rule-info' }, [
@ -4262,8 +4262,8 @@ return view.extend({
var downloadLimits = [];
activeRules.forEach(function (rule) {
var uploadLimit = rule.w_tx_r_limit || 0;
var downloadLimit = rule.w_rx_r_limit || 0;
var uploadLimit = rule.wan_tx_rate_limit || 0;
var downloadLimit = rule.wan_rx_rate_limit || 0;
// 只收集非零的限制值
if (uploadLimit > 0) {

View File

@ -18,7 +18,7 @@ LUCI_DEPENDS:=+ip-full +tc +nftables +bc +kmod-ifb
LUCI_PKGARCH:=all
PKG_VERSION:=1.3.0
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com>
define Build/Compile

View File

@ -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

View File

@ -124,6 +124,10 @@ const glossary = {
prefmt: 'rule_%s',
field: 'rule-providers',
},
inbound: {
prefmt: 'inbound_%s',
field: 'listeners',
},
server: {
prefmt: 'server_%s',
field: 'listeners',
@ -148,7 +152,8 @@ const inbound_type = [
['anytls', _('AnyTLS') + ' - ' + _('TCP')],
['tuic', _('TUIC') + ' - ' + _('UDP')],
['hysteria2', _('Hysteria2') + ' - ' + _('UDP')],
//['tunnel', _('Tunnel') + ' - ' + _('TCP/UDP')]
['trusttunnel', _('TrustTunnel') + ' - ' + _('TCP/UDP')],
['tunnel', _('Tunnel') + ' - ' + _('TCP/UDP')]
];
const ip_version = [
@ -183,6 +188,7 @@ const outbound_type = [
['hysteria2', _('Hysteria2') + ' - ' + _('UDP')],
['tuic', _('TUIC') + ' - ' + _('UDP')],
['masque', _('Masque') + ' - ' + _('UDP')], // https://blog.cloudflare.com/post-quantum-warp/
['trusttunnel', _('TrustTunnel') + ' - ' + _('TCP/UDP')],
['wireguard', _('WireGuard') + ' - ' + _('UDP')],
['ssh', _('SSH') + ' - ' + _('TCP')]
];

File diff suppressed because it is too large Load Diff

View File

@ -795,7 +795,7 @@ function renderRules(s, uciconfig) {
UIEl.setValue(rule.toString('json'));
}
o.write = function() {};
//o.depends('SUB-RULE', '');
//o.depends('SUB-RULE', ''); // work on subrules not rules
o.editable = true;
o = s.option(form.Flag, 'src', _('src'),

View File

@ -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();
}
});

View File

@ -255,13 +255,13 @@ return view.extend({
/* hm.validateAuth */
so = ss.taboption('field_general', form.Value, 'username', _('Username'));
so.validate = hm.validateAuthUsername;
so.depends({type: /^(http|socks5|mieru|ssh)$/});
so.depends({type: /^(http|socks5|mieru|trusttunnel|ssh)$/});
so.modalonly = true;
so = ss.taboption('field_general', form.Value, 'password', _('Password'));
so.password = true;
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 = ss.taboption('field_general', hm.TextValue, 'headers', _('HTTP header'));
@ -375,6 +375,12 @@ return view.extend({
so.depends('type', 'mieru');
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 */
so = ss.taboption('field_general', form.Value, 'sudoku_key', _('Key'),
_('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('poll', _('poll') + ' - ' + _('CDN support'));
so.value('auto', _('Auto') + ' - ' + _('CDN support'));
so.value('ws', _('WebSocket') + ' - ' + _('CDN support'));
so.depends('sudoku_http_mask', '1');
so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'sudoku_http_mask_tls', _('HTTP mask: %s').format(_('TLS')));
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 = ss.taboption('field_general', form.Value, 'sudoku_http_mask_host', _('HTTP mask: %s').format(_('Host/SNI override')));
so.datatype = 'or(hostname, hostport)';
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 = 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) {
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))
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;
}
@ -512,15 +521,6 @@ return view.extend({
so.depends('type', 'tuic');
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'),
_('UDP packet relay mode.'));
so.default = 'native';
@ -714,12 +714,15 @@ return view.extend({
so.depends('masque_remote_dns_resolve', '1');
so.modalonly = true;
so = ss.taboption('field_general', form.ListValue, 'masque_congestion_controller', _('Congestion controller'));
so.default = hm.congestion_controller[0][0];
hm.congestion_controller.forEach((res) => {
so.value.apply(so, res);
})
so.depends('type', 'masque');
/* TrustTunnel fields */
so = ss.taboption('field_general', form.Flag, 'trusttunnel_health_check', _('Health check'));
so.default = so.enabled;
so.depends('type', 'trusttunnel');
so.modalonly = true;
so = ss.taboption('field_general', form.Flag, 'trusttunnel_quic', _('QUIC'));
so.default = so.disabled;
so.depends('type', 'trusttunnel');
so.modalonly = true;
/* WireGuard fields */
@ -848,9 +851,17 @@ return view.extend({
so.modalonly = true;
/* 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.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 = 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');
// Force enabled
if (['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic'].includes(type)) {
if (['trojan', 'anytls', 'hysteria', 'hysteria2', 'tuic', 'trusttunnel'].includes(type)) {
tls.checked = true;
tls.disabled = true;
} else {
@ -950,7 +961,7 @@ return view.extend({
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 = 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'),
_('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.modalonly = true;
@ -991,6 +1002,9 @@ return view.extend({
case 'anytls':
def_alpn = ['h2', 'http/1.1'];
break;
case 'trusttunnel':
def_alpn = ['h3', 'h2'];
break;
default:
def_alpn = [];
}
@ -1000,7 +1014,7 @@ return view.extend({
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.modalonly = true;
@ -1022,7 +1036,7 @@ return view.extend({
'<br/>' +
_('This is <strong>DANGEROUS</strong>, your traffic is almost like <strong>PLAIN TEXT</strong>! Use at your own risk!'));
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 = 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) => {
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.modalonly = true;

View File

@ -6,125 +6,17 @@
'require view';
'require fchomo as hm';
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}'`);
}
}
'require fchomo.listeners as lsnr'
return view.extend({
load() {
return Promise.all([
uci.load('fchomo'),
hm.getFeatures()
uci.load('fchomo')
]);
},
render(data) {
const dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo');
const features = data[1];
let m, s, o;
@ -168,845 +60,7 @@ return view.extend({
s.hm_field = hm.glossary[s.sectiontype].field;
s.hm_lowcase_only = false;
s.tab('field_general', _('General fields'));
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;
lsnr.renderListeners(s, data[0], false);
/* Server settings END */
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

View File

@ -45,6 +45,31 @@ opmc() { # @less_25_12
$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"
# define global var: DEF_WAN DEF_WAN6 NIC_* NIC6_*
@ -333,6 +358,15 @@ start_service() {
procd_set_param stderr 1
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
fi
fi
@ -380,30 +414,6 @@ start_service() {
procd_set_param respawn
# 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
# configure firewall
json_add_array firewall

View File

@ -34,6 +34,22 @@ migrate() {
uci_set "$CONF" "$1" vless_encryption_encryption "$vless_encryption"
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

View File

@ -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

View File

@ -11,6 +11,7 @@ import { urldecode, urlencode } from 'luci.http';
import {
isEmpty, strToBool, strToInt, bytesizeToByte, durationToSecond,
arrToObj, removeBlankAttrs,
parseListener,
HM_DIR, RUN_DIR, PRESET_OUTBOUND, RULES_LOGICAL_TYPE
} from 'fchomo';
@ -39,6 +40,7 @@ const ucisniff = 'sniff',
ucidnspoli = 'dns_policy',
ucidnsnpoli = 'dns_node_policy',
ucipgrp = 'proxy_group',
uciinbd = 'inbound',
ucinode = 'node',
uciprov = 'provider',
ucichain = 'dialer_proxy',
@ -326,29 +328,14 @@ uci.foreach(uciconf, ucisniff, (cfg) => {
/* Inbound START */
const proxy_mode = uci.get(uciconf, uciinbound, 'proxy_mode') || 'redir_tproxy';
/* Listen ports */
config.listeners = [];
push(config.listeners, {
name: 'mixed-in',
type: 'mixed',
port: strToInt(uci.get(uciconf, uciinbound, 'mixed_port')) || 7890,
listen: '::',
udp: true
});
config["allow-lan"] = true;
config["bind-address"] = "*";
config["mixed-port"] = strToInt(uci.get(uciconf, uciinbound, 'mixed_port')) || 7890;
if (match(proxy_mode, /redir/))
push(config.listeners, {
name: 'redir-in',
type: 'redir',
port: strToInt(uci.get(uciconf, uciinbound, 'redir_port')) || 7891,
listen: '::'
});
config["redir-port"] = strToInt(uci.get(uciconf, uciinbound, 'redir_port')) || 7891;
if (match(proxy_mode, /tproxy/))
push(config.listeners, {
name: 'tproxy-in',
type: 'tproxy',
port: strToInt(uci.get(uciconf, uciinbound, 'tproxy_port')) || 7892,
listen: '::',
udp: true
});
config["tproxy-port"] = strToInt(uci.get(uciconf, uciinbound, 'tproxy_port')) || 7892;
config.listeners = [];
push(config.listeners, {
name: 'dns-in',
type: 'tunnel',
@ -357,6 +344,13 @@ push(config.listeners, {
network: ['tcp', 'udp'],
target: '1.1.1.1:53'
}); // @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 */
if (match(proxy_mode, /tun/))
config.tun = {
@ -502,7 +496,6 @@ uci.foreach(uciconf, ucinode, (cfg) => {
cipher: cfg.vmess_chipher || cfg.shadowsocks_chipher,
password: cfg.shadowsocks_password || cfg.password,
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,
"public-key": cfg.masque_endpoint_public_key || cfg.wireguard_peer_public_key,
ip: cfg.masque_ip || cfg.wireguard_ip,
@ -531,6 +524,7 @@ uci.foreach(uciconf, ucinode, (cfg) => {
transport: cfg.mieru_transport,
multiplexing: cfg.mieru_multiplexing,
"handshake-mode": cfg.mieru_handshake_mode,
"traffic-pattern": cfg.mieru_traffic_pattern,
/* Sudoku */
key: cfg.sudoku_key,
@ -540,12 +534,16 @@ uci.foreach(uciconf, ucinode, (cfg) => {
"table-type": cfg.sudoku_table_type,
"custom-tables": cfg.sudoku_custom_tables,
"enable-pure-downlink": (cfg.sudoku_enable_pure_downlink === '0') ? false : null,
"http-mask": (cfg.sudoku_http_mask === '0') ? false : true,
"http-mask-mode": cfg.sudoku_http_mask_mode,
"http-mask-tls": strToBool(cfg.sudoku_http_mask_tls),
"http-mask-host": cfg.sudoku_http_mask_host,
"path-root": cfg.sudoku_path_root,
"http-mask-multiplex": cfg.sudoku_http_mask_multiplex,
...(cfg.type === 'sudoku' ? {
httpmask: (cfg.sudoku_http_mask === '0') ? { disable: true } : {
disable: false,
mode: cfg.sudoku_http_mask_mode,
tls: strToBool(cfg.sudoku_http_mask_tls) || false,
host: cfg.sudoku_http_mask_host,
path_root: cfg.sudoku_path_root,
multiplex: cfg.sudoku_http_mask_multiplex,
}
} : {}),
/* Snell */
psk: cfg.snell_psk,
@ -587,6 +585,10 @@ uci.foreach(uciconf, ucinode, (cfg) => {
"packet-encoding": cfg.vmess_packet_encoding,
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 */
"pre-shared-key": cfg.wireguard_pre_shared_key,
"allowed-ips": cfg.wireguard_allowed_ips,
@ -605,12 +607,13 @@ uci.foreach(uciconf, ucinode, (cfg) => {
} : null,
/* Extra fields */
"congestion-controller": cfg.congestion_controller,
udp: strToBool(cfg.udp),
"udp-over-tcp": strToBool(cfg.uot),
"udp-over-tcp-version": cfg.uot_version,
/* 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),
...arrToObj([[(cfg.type in ['vmess', 'vless']) ? 'servername' : 'sni', cfg.tls_sni]]),
fingerprint: cfg.tls_fingerprint,

View File

@ -7,6 +7,7 @@ import { cursor } from 'uci';
import {
isEmpty, strToBool, strToInt, durationToSecond,
arrToObj, removeBlankAttrs,
parseListener,
HM_DIR, RUN_DIR, PRESET_OUTBOUND
} from 'fchomo';
@ -33,23 +34,6 @@ function parse_users(cfg) {
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 */
/* Main */
@ -61,131 +45,7 @@ uci.foreach(uciconf, uciserver, (cfg) => {
if (cfg.enabled === '0')
return;
push(config.listeners, {
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
} : {})
});
push(config.listeners, parseListener(cfg, false));
});
/* Inbound END */

View File

@ -34,6 +34,14 @@
"path": "fchomo/hosts"
}
},
"admin/services/fchomo/inbound": {
"title": "Inbound",
"order": 27,
"action": {
"type": "view",
"path": "fchomo/inbound"
}
},
"admin/services/fchomo/node": {
"title": "Node",
"order": 30,

View File

@ -191,3 +191,163 @@ export function removeBlankAttrs(res) {
return content;
};
/* 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 */

View File

@ -7,8 +7,8 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=kai
PKG_VERSION:=1.0.0
PKG_RELEASE:=2
PKG_VERSION:=1.0.1
PKG_RELEASE:=3
LUCI_DEPENDS:=+kai +kai_session +luci-compat
LUCI_MINIFY_CSS:=0
LUCI_MINIFY_JS:=0

View File

@ -9,11 +9,20 @@ s.anonymous=true
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"))
data_dir.placeholder = ""
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).")
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"))
port.default = "8197"
port.rmempty = false

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

View File

@ -11,7 +11,7 @@ LUCI_TITLE:=LuCI Support for Automatic Partition Mount
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+fdisk +block-mount +bc +blkid +parted +btrfs-progs +losetup +resize2fs +e2fsprogs +f2fs-tools +kmod-loop
PKG_VERSION:=2.0.3
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Sirpdboy <herboy2008@gmail.com>

View File

@ -1,7 +1,7 @@
#!/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 .
#
@ -279,8 +279,8 @@ check_part_space() {
check_free_space() {
DISK=$1
PARTED_OUTPUT=$(parted -s /dev/$DISK unit GB print free 2>/dev/null)
FREE_SPACE=$(echo "$PARTED_OUTPUT" | grep "Free Space" | awk '{print $3}' )
echo $FREE_SPACE |awk -F '.' '{print $1}' | sed 's/[A-Za-z]//g'
FREE_SPACE=$(echo "$PARTED_OUTPUT" | awk '/Free Space/ {last=$3} END {print last}' | sed 's/GB$//')
echo $FREE_SPACE | cut -d'.' -f1
}
show_partition_info() {
@ -418,12 +418,6 @@ rootfs_resize() {
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() {

View File

@ -6,12 +6,12 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=redsocks2
PKG_VERSION:=0.71
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/semigodking/redsocks.git
PKG_SOURCE_DATE:=2025-12-12
PKG_SOURCE_VERSION:=33c37b37c0524ef01e3f2653a6c47a10f783cc39
PKG_SOURCE_VERSION:=43ba8a3cb1b5f8c44b2a827b924375f1de733370
PKG_MIRROR_HASH:=skip
PKG_MAINTAINER:=semigodking <semigodking@gmail.com>