mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 02:11:19 +08:00
💐 Sync 2026-03-10 20:23:29
This commit is contained in:
parent
62c1cd7501
commit
52bf5fc663
@ -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
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
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')]
|
||||
];
|
||||
|
||||
1011
luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js
Normal file
1011
luci-app-fchomo/htdocs/luci-static/resources/fchomo/listeners.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -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'),
|
||||
|
||||
@ -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 */
|
||||
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;
|
||||
|
||||
|
||||
@ -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
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
33
luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_server
Executable file
33
luci-app-fchomo/root/etc/uci-defaults/99_luci-app-fchomo-migration_server
Executable 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
|
||||
@ -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,
|
||||
|
||||
@ -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 */
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
54
luci-app-kai/luasrc/model/kai.lua
Normal file
54
luci-app-kai/luasrc/model/kai.lua
Normal 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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user