import {SaveApplyUtils} from "../utils/actions/save-apply/index";
import {ServiceUtils} from "../utils/base/luci/service";
import {FormUtils} from "../utils/base/luci/form";
import {ValidationUtils} from "../utils/base/luci/validation";
import {NotificationUtils} from "../utils/base/luci/notification";
import {ShuntFormUtils} from "../utils/feature/shunt/form";
import {ShuntUtils} from "../utils/feature/shunt/section";
import {CORE_TYPE, FIREWALL_DNS_FORWARD, HijpassValues, LuciFlied} from "../enum/hijpass";
import {FirewallDnsForwardUtils} from "../utils/feature/firewall/dns-forward";
import form from "form";
import fs from "fs";
import view from 'view';
import uci from 'uci';
const XRAY_ROUTE_PROTOCOLS = ['http', 'tls', 'quic', 'bittorrent'];
type GeoRuleQueryType = 'geosite' | 'geoip';
function ensureInlineRuleQueryStyles() {
if (document.getElementById('hijpass-inline-rule-query-styles')) {
return;
}
const style = document.createElement('style');
style.id = 'hijpass-inline-rule-query-styles';
style.textContent = `
.hijpass-rule-editor-query-layout {
display: grid;
grid-template-columns: auto minmax(14rem, 18rem);
gap: 1rem;
align-items: start;
width: fit-content;
max-width: 100%;
}
.cbi-value[data-name="domain_list"],
.cbi-value[data-name="ip_list"] {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
}
.cbi-value[data-name="domain_list"] > .cbi-value-field,
.cbi-value[data-name="ip_list"] > .cbi-value-field {
display: block;
flex: 1 1 0;
min-width: 0;
}
.hijpass-rule-editor-query-layout > .hijpass-rule-editor,
.hijpass-rule-editor-query-layout > .hijpass-inline-rule-query {
min-width: 0;
}
.hijpass-rule-editor-query-layout > .hijpass-rule-editor {
width: 370px;
max-width: 100%;
overflow: hidden;
}
.hijpass-rule-editor-query-layout .hijpass-rule-editor textarea.cbi-input-textarea {
box-sizing: border-box;
display: block;
width: 100%;
min-width: 0;
max-width: none;
max-height: 50vh;
resize: both;
}
.hijpass-inline-rule-query {
display: flex;
flex-direction: column;
gap: .375rem;
box-sizing: border-box;
width: 100%;
padding: .5rem;
border: 1px solid rgba(127, 127, 127, .35);
border-radius: 4px;
background: rgba(127, 127, 127, .05);
}
.hijpass-inline-rule-query > .cbi-section-descr {
margin: 0;
}
.hijpass-inline-rule-query-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: .5rem;
min-width: 0;
}
.hijpass-inline-rule-query-controls {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: .5rem;
align-self: stretch;
min-width: 0;
}
.hijpass-rule-editor-query-layout .hijpass-inline-rule-query-controls > input.cbi-input-text {
box-sizing: border-box;
min-width: 0;
width: auto;
max-width: none;
}
.hijpass-inline-rule-query-controls > .cbi-button {
margin: 0;
white-space: nowrap;
}
.hijpass-inline-rule-query-status {
flex: 0 0 auto;
min-height: 0;
text-align: right;
white-space: nowrap;
}
.hijpass-inline-rule-query-status[data-state="success"] {
color: var(--success-color, #2e7d32);
}
.hijpass-inline-rule-query-status[data-state="error"] {
color: var(--danger-color, #c62828);
}
.hijpass-rule-editor-query-layout textarea.hijpass-inline-rule-query-result {
box-sizing: border-box;
align-self: stretch;
width: auto;
min-width: 0;
max-width: none;
height: 5rem;
min-height: 4rem;
max-height: 50vh;
resize: vertical;
}
@media (max-width: 900px) {
.cbi-value[data-name="domain_list"],
.cbi-value[data-name="ip_list"] {
display: block;
}
.cbi-value[data-name="domain_list"] > .cbi-value-title,
.cbi-value[data-name="ip_list"] > .cbi-value-title,
.cbi-value[data-name="domain_list"] > .cbi-value-field,
.cbi-value[data-name="ip_list"] > .cbi-value-field {
box-sizing: border-box;
display: block;
float: none;
width: 100%;
margin-left: 0;
text-align: left;
}
.hijpass-rule-editor-query-layout {
grid-template-columns: minmax(0, 1fr);
width: 100%;
}
.hijpass-rule-editor-query-layout > .hijpass-rule-editor,
.hijpass-rule-editor-query-layout .hijpass-rule-editor textarea.cbi-input-textarea {
width: 100%;
max-width: 100%;
}
}
`;
document.head.appendChild(style);
}
function createGeoRuleQuery(
queryType: GeoRuleQueryType,
editorId: string
): HTMLElement {
const titleId = editorId + '-query-title';
const inputId = editorId + '-query-keyword';
const tagPrefix = queryType + ':';
const statusElement = E('div', {
'class': 'hijpass-inline-rule-query-status',
'role': 'status',
'aria-live': 'polite'
}) as HTMLElement;
const resultElement = E('textarea', {
'class': 'cbi-input-textarea hijpass-inline-rule-query-result',
'readonly': 'readonly',
'placeholder': _('Query results will appear here'),
'aria-label': _('Query Results')
}) as HTMLTextAreaElement;
const keywordInput = E('input', {
'id': inputId,
'class': 'cbi-input-text',
'type': 'text',
'placeholder': queryType === 'geosite' ? 'www.example.com / geosite:cn' : '1.1.1.1 / geoip:cn',
'aria-label': _('Query Keyword')
}) as HTMLInputElement;
const queryButton = E('button', {
'class': 'cbi-button cbi-button-action',
'type': 'button'
}, _('Query')) as HTMLButtonElement;
const panel = E('div', {
'class': 'hijpass-inline-rule-query',
'role': 'group',
'aria-labelledby': titleId
}, [
E('div', {'class': 'hijpass-inline-rule-query-header'}, [
E('strong', {'id': titleId}, _('Geo Database Query')),
statusElement
]),
E('div', {'class': 'cbi-section-descr'}, queryType === 'geosite' ? [
E('div', {}, _('Domain → GeoSite tags')),
E('div', {}, _('geosite:tag → domains in tag'))
] : [
E('div', {}, _('IP address → GeoIP tags')),
E('div', {}, _('geoip:tag → networks in tag'))
]),
E('div', {'class': 'hijpass-inline-rule-query-controls'}, [keywordInput, queryButton]),
resultElement
]) as HTMLElement;
const setStatus = function (message: string, state?: 'success' | 'error') {
statusElement.textContent = message;
if (state) {
statusElement.dataset.state = state;
} else {
delete statusElement.dataset.state;
}
};
const runQuery = async function () {
if (queryButton.disabled) {
return;
}
const keyword = keywordInput.value.trim();
if (!keyword) {
setStatus(_('Enter Query Keyword'), 'error');
keywordInput.focus();
return;
}
resultElement.value = '';
queryButton.disabled = true;
panel.setAttribute('aria-busy', 'true');
setStatus(_('Querying'));
try {
const isTagQuery = keyword.toLowerCase().startsWith(tagPrefix);
const queryValue = isTagQuery ? keyword.slice(tagPrefix.length).trim() : keyword;
if (!queryValue) {
setStatus(_('Enter Query Keyword'), 'error');
keywordInput.focus();
return;
}
const result = await fs.exec('/usr/lib/hijpass/geoview.sh',
[isTagQuery ? '-e' : '-l', queryType, queryValue]);
if (result.code !== 0) {
throw new Error(result.stderr || _('Query Failed'));
}
let output = (result.stdout || '').trim();
if (output) {
if (!isTagQuery) {
output = output.split(/\r?\n/)
.filter(Boolean)
.map(tag => tagPrefix + tag)
.join('\n');
}
resultElement.value = output;
setStatus(_('Query Complete'), 'success');
} else {
setStatus(_('No Matching Results'));
}
} catch (error) {
console.log(error);
setStatus(_('Query Failed'), 'error');
} finally {
queryButton.disabled = false;
panel.removeAttribute('aria-busy');
}
};
queryButton.addEventListener('click', function (event) {
event.preventDefault();
runQuery();
});
keywordInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') {
event.preventDefault();
runQuery();
}
});
return panel;
}
function syncRuleEditorWidth(
layout: HTMLElement,
editor: HTMLElement,
queryPanel: HTMLElement,
textarea: HTMLTextAreaElement
) {
if (typeof ResizeObserver === 'undefined') {
return;
}
const mediaQuery = window.matchMedia('(max-width: 900px)');
const initialTextareaWidth = textarea.style.width;
let narrowLayout: boolean | null = null;
let preferredWidth = 370;
let appliedWidth = 0;
let wasConnected = false;
let observedField: HTMLElement | null = null;
const updateWidth = function () {
if (layout.isConnected) {
wasConnected = true;
} else if (wasConnected) {
observer.disconnect();
return;
}
if (mediaQuery.matches) {
if (narrowLayout !== true) {
editor.style.removeProperty('width');
textarea.style.width = initialTextareaWidth;
}
narrowLayout = true;
appliedWidth = 0;
return;
}
if (narrowLayout === true) {
editor.style.removeProperty('width');
textarea.style.width = initialTextareaWidth;
preferredWidth = 370;
}
narrowLayout = false;
const field = layout.parentElement;
if (!field || field.clientWidth === 0) {
return;
}
if (field !== observedField) {
observer.observe(field);
observedField = field;
}
const currentWidth = textarea.offsetWidth;
if (currentWidth > 0 && Math.abs(currentWidth - appliedWidth) > 1) {
preferredWidth = currentWidth;
}
const gap = parseFloat(window.getComputedStyle(layout).columnGap) || 0;
const availableWidth = field.clientWidth - queryPanel.offsetWidth - gap;
if (availableWidth <= 0) {
return;
}
const nextWidth = Math.min(preferredWidth, availableWidth);
const width = nextWidth + 'px';
appliedWidth = nextWidth;
if (textarea.style.width !== width) {
textarea.style.width = width;
}
if (editor.style.width !== width) {
editor.style.width = width;
}
};
const observer = new ResizeObserver(updateWidth);
observer.observe(textarea);
observer.observe(queryPanel);
window.requestAnimationFrame(function () {
updateWidth();
});
}
function configureGeoRuleQuery(option: any, queryType: GeoRuleQueryType) {
option.renderWidget = function (sectionId: string, optionIndex: number, cfgvalue: string): Node {
const widget = form.TextValue.prototype.renderWidget.call(
this, sectionId, optionIndex, cfgvalue) as HTMLElement;
const editor = E('div', {'class': 'hijpass-rule-editor'}, [widget]) as HTMLElement;
const queryPanel = createGeoRuleQuery(queryType, this.cbid(sectionId));
const layout = E('div', {'class': 'hijpass-rule-editor-query-layout'}, [
editor,
queryPanel
]) as HTMLElement;
const textarea = widget.querySelector('textarea.cbi-input-textarea') as HTMLTextAreaElement;
if (textarea) {
syncRuleEditorWidth(layout, editor, queryPanel, textarea);
}
return layout;
};
}
function getCurrentShuntValue(section: LuCI.form.AbstractSection, option: string) {
const shuntSection = uci.get_first(LuciFlied.CONF_NAME, LuciFlied.SHUNT_SECTION_TYPE);
const sectionId = shuntSection?.['.name'];
const lookup = sectionId ? section.map.lookupOption(option, sectionId) : null;
return lookup ? lookup[0].formvalue(lookup[1]) : shuntSection?.[option];
}
function hasSplitDnsTarget(section: any, sectionId: string) {
const ruleDTO = ShuntUtils.parseRuleList({
domainList: section.formvalue(sectionId, 'domain_list') || '',
ipList: section.formvalue(sectionId, 'ip_list') || '',
network: [],
protocol: [],
port: [],
portRange: []
});
const shuntType = getCurrentShuntValue(section, 'type');
const rulesetConvert = getCurrentShuntValue(section, 'ruleset_convert');
if (shuntType === CORE_TYPE.XRAY) {
return ruleDTO.containDomain() || ruleDTO.geoSite.length > 0;
}
return ruleDTO.containDomain()
|| ruleDTO.containRuleSet()
|| (rulesetConvert === '1' && ruleDTO.geoSite.length > 0);
}
function getShuntDnsDependencyError(section: LuCI.form.AbstractSection, sectionId: string) {
if (FirewallDnsForwardUtils.getSource() === FIREWALL_DNS_FORWARD.ROUTING) {
return _('Firewall DNS forwarding is using Core DNS. Select another forwarding source in Firewall before disabling proxy routing.');
}
const dnsService = uci.get_first(LuciFlied.CONF_NAME, LuciFlied.DNS_SECTION_TYPE, 'dns_service');
const directDns = uci.get_first(LuciFlied.CONF_NAME, LuciFlied.DNS_SECTION_TYPE, 'direct_dns');
const proxyDns = uci.get_first(LuciFlied.CONF_NAME, LuciFlied.DNS_SECTION_TYPE, 'proxy_dns');
const dnsListenPort = section.formvalue(sectionId, 'dns_listen_port');
if (dnsService === 'chinadns-ng'
&& dnsListenPort
&& (directDns === '127.0.0.1#' + dnsListenPort
|| proxyDns === '127.0.0.1#' + dnsListenPort)) {
return _('Independent DNS is using this Core DNS port. Change it in the Independent DNS module before disabling proxy routing.');
}
return null;
}
const v = view.extend({
load: function () {
return Promise.all([
uci.load(LuciFlied.CONF_NAME)
]);
},
handleSave: async function (ev: Event) {
try {
await view.prototype.handleSave(ev)
} catch (e) {
console.log(e);
NotificationUtils.error(_('Configuration Save Failed'), _(e.message), 5000);
}
},
handleSaveApply: SaveApplyUtils.genHandleSaveApply(),
render: function () {
ensureInlineRuleQueryStyles();
let m = new form.Map(LuciFlied.CONF_NAME, _('Proxy Routing'),
_('Configure proxy core routing, DNS, and rule-set settings'));
ServiceUtils.createStatusSection(m, 'shunt');
let s = m.section(form.TypedSection, LuciFlied.SHUNT_SECTION_TYPE);
s.anonymous = true;
s.addremove = false;
// 创建标签页
createTabs(s);
// 创建基础配置选项
createBasicOptions(s);
createConfigEditor(s);
createRouteOptions(s);
createDNSOptions(s);
return m.render();
}
})
// 创建基础配置选项
function createBasicOptions(s: LuCI.form.AbstractSection) {
// 总开关
let enabled = s.taboption('basic', form.Flag, 'enabled', _('Enable Shunt'), '');
enabled.default = '1';
enabled.rmempty = false;
enabled.validate = function (sectionId: string, value: string) {
if (value !== '0') {
return true;
}
return getShuntDnsDependencyError(this.section, sectionId) || true;
};
enabled.renderWidget = function (sectionId: string, optionIndex: number, cfgvalue: string): Node {
const widget = form.Flag.prototype.renderWidget.call(
this, sectionId, optionIndex, cfgvalue) as HTMLElement;
const checkbox = widget.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
checkbox?.addEventListener('change', (event: Event) => {
if (checkbox.checked) {
return;
}
const error = getShuntDnsDependencyError(this.section, sectionId);
if (!error) {
return;
}
checkbox.checked = true;
event.stopImmediatePropagation();
this.triggerValidation(sectionId);
NotificationUtils.warning(_('Configuration conflict'), error, 8000);
}, true);
return widget;
};
// 分流类型
let serviceType = s.taboption('basic', form.ListValue, 'type', _('Shunt Type'),
_('Select Shunt Type'));
HijpassValues.SHUNT_CONF_TYPE.forEach(function (type) {
serviceType.value(type);
})
serviceType.default = CORE_TYPE.XRAY;
serviceType.rmempty = false;
let configType = s.taboption('basic', form.ListValue, 'config_type', _('Configuration Type'),
_('Select Configuration Type'));
configType.value('tmpl', _('Template Configuration'));
configType.value('custom', _('Custom Edit'));
configType.default = 'tmpl';
configType.rmempty = false;
// 分流监听端口
let shuntListenPort = s.taboption('basic', form.Value, 'shunt_listen_port', _('Shunt Listen Port'),
_('Shunt Listen Port'));
shuntListenPort.default = '1041';
shuntListenPort.datatype = 'port';
shuntListenPort.rmempty = false;
shuntListenPort.validate = function (sectionId: string, value: string) {
const currentDnsListenPort = this.section.formvalue(sectionId, 'dns_listen_port');
if (currentDnsListenPort === value) {
return _("Conflicts with DNS Port")
}
return ValidationUtils.validatePortConflict.call(this, sectionId, value);
}
// DNS监听端口
let dnsListenPort = s.taboption('basic', form.Value, 'dns_listen_port', _('DNS Listen Port'),
_('DNS Listen Port'));
dnsListenPort.default = '10153';
dnsListenPort.datatype = 'port';
dnsListenPort.rmempty = false;
dnsListenPort.validate = function (sectionId: string, value: string) {
const currentShuntListenPort = this.section.formvalue(sectionId, 'shunt_listen_port');
if (currentShuntListenPort === value) {
return _("Conflicts with Shunt Port")
}
return ValidationUtils.validatePortConflict.call(this, sectionId, value);
}
// 日志开关
FormUtils.createLogFlagOption(s, LuciFlied.CONF_NAME, 'basic');
// 日志路径
let logLevel = s.taboption('basic', form.ListValue, 'log_level', _('Log Level'), _('Logging Level'));
HijpassValues.SHUNT_LOG_LEVELS.forEach(function (level) {
logLevel.value(level);
})
logLevel.default = 'warn';
logLevel.rmempty = false;
logLevel.depends('log_enabled', '1');
// 预览配置按钮
let previewConfigButton = s.taboption('basic', form.Button, '_preview_config', _('Preview Configuration'),
_('Click to Preview'));
previewConfigButton.depends('config_type', 'tmpl');
previewConfigButton.inputtitle = _('Preview Configuration');
previewConfigButton.inputstyle = 'apply';
previewConfigButton.onclick = ShuntFormUtils.showPreview
}
function createConfigEditor(s: LuCI.form.AbstractSection) {
let o = s.taboption('config', form.TextValue, 'shunt_conf_data', null, _('Edit Shunt Configuration File'));
o.depends('config_type', 'custom')
ShuntFormUtils.configureConfigEditor(o);
}
// 创建路由配置选项
function createRouteOptions(s: LuCI.form.AbstractSection) {
// 默认全局路由出口配置
let routeTypeSectionValue = ShuntFormUtils.createSectionContainer(s, 'route', '_route_global',
form.TypedSection, LuciFlied.SHUNT_SECTION_TYPE, '');
let routeTypeSection = routeTypeSectionValue.subsection;
FormUtils.setAnonymousSection(routeTypeSection);
let defaultProxyNode = ShuntFormUtils.createRequiredListValue(routeTypeSection, 'default_proxy_node', _('Default Global Exit'),
_('Select Default Exit Node'),
function (section_id: any, value: any) {
if (!value) {
return _('Configure Global Exit First');
}
return true;
});
ShuntFormUtils.loadProxyNodeOptions(defaultProxyNode, true);
let rulesetOutNode = routeTypeSection.option(form.ListValue, 'ruleset_out_node', _('Rule-set Exit'),
_('Rule-set Download Exit'));
ShuntFormUtils.loadProxyNodeOptions(rulesetOutNode, false);
let rulesetConvert = routeTypeSection.option(form.Flag, 'ruleset_convert',
_('Use GeoSite Rule-set'), _('Convert Geo Rules'));
rulesetConvert.rmempty = false;
rulesetConvert.depends('type', CORE_TYPE.SING_BOX);
let routeDiagnostics = routeTypeSection.option(form.Button, '_route_diagnostics', _('Shunt Troubleshooting'),
_('Trace router or client traffic and inspect routing failures in Diagnostics.'));
routeDiagnostics.inputtitle = _('Open Diagnostics');
routeDiagnostics.inputstyle = 'action';
routeDiagnostics.onclick = function () {
window.location.href = L.url('admin/services/hijpass/log') + '?tab=trace';
};
// 批量替换节点按钮
let replaceNodeBtn = routeTypeSection.option(form.Button, '_replace_node', _('Bulk Replace Nodes'));
replaceNodeBtn.depends({config_type: "tmpl"});
replaceNodeBtn.inputtitle = _('Bulk Replace Nodes');
replaceNodeBtn.inputstyle = 'apply';
replaceNodeBtn.onclick = ShuntFormUtils.showBulkReplaceModal;
// 路由规则配置
let routeGridSectionValue = s.taboption('route', form.SectionValue, '_route_rule',
form.GridSection, LuciFlied.SHUNT_ROUTE_RULE_TYPE, _('Routing Rule Configuration'));
routeGridSectionValue.depends({config_type: "tmpl"})
let routeGridSection = routeGridSectionValue.subsection;
routeGridSection.rowcolors = true
FormUtils.setAnonymousGridSectionMembers(routeGridSection)
routeGridSection.modaltitle = _('Edit Routing Rule');
routeGridSection.sectiontitle = function (section_id: any) {
return uci.get(LuciFlied.CONF_NAME, section_id, 'name') || _('Unnamed Rule');
};
routeGridSection.rowcolors = true
routeGridSection.tab('_rule', _('Rule'));
routeGridSection.tab('_dns', _('DNS Configuration'));
// 规则名称
let name = routeGridSection.taboption('_rule', form.Value, 'name', _('Rule Name'));
name.validate = ValidationUtils.createTagValidator(LuciFlied.CONF_NAME, LuciFlied.SHUNT_ROUTE_RULE_TYPE, 'name')
name.rmempty = false;
name.modalonly = true;
// 启用开关(从路由配置移过来)
let enabled = routeGridSection.taboption('_rule', form.Flag, 'enabled', _('Enable'));
enabled.default = '1';
enabled.rmempty = false;
enabled.editable = true;
let proxyNodeView = routeGridSection.option(form.DummyValue, '_proxy_node_view', _('Proxy Node'));
proxyNodeView.textvalue = function (sectionId) {
if (uci.get(LuciFlied.CONF_NAME, sectionId, 'ip_version_split') === '1') {
return _('IPv4: %s / IPv6: %s').format(
uci.get(LuciFlied.CONF_NAME, sectionId, 'proxy_node_v4') || '-',
uci.get(LuciFlied.CONF_NAME, sectionId, 'proxy_node_v6') || '-'
);
}
return uci.get(LuciFlied.CONF_NAME, sectionId, 'proxy_node') || '-';
}
// 代理节点(动态生成选项)
let proxyNode = routeGridSection.taboption('_rule', form.ListValue, 'proxy_node', _('Proxy Node'));
proxyNode.rmempty = true;
proxyNode.modalonly = true;
proxyNode.depends({ip_version_split: '1', '!reverse': true});
proxyNode.validate = function (sectionId: string, value: string) {
if (this.section.formvalue(sectionId, 'ip_version_split') !== '1' && !value) {
return _('Configure Proxy Node First');
}
return true;
}
ShuntFormUtils.loadProxyNodeOptions(proxyNode, true); // 包含阻止和负载均衡
let ipVersionSplit = routeGridSection.taboption('_rule', form.Flag, 'ip_version_split',
_('Split IPv4/IPv6 Exit'),
_('Use separate proxy nodes for IPv4 and IPv6 while sharing the same rule conditions'));
ipVersionSplit.default = '0';
ipVersionSplit.rmempty = false;
ipVersionSplit.modalonly = true;
let proxyNodeV4 = routeGridSection.taboption('_rule', form.ListValue, 'proxy_node_v4',
_('IPv4 Proxy Node'), _('IPv4 traffic exit node'));
proxyNodeV4.depends('ip_version_split', '1');
proxyNodeV4.modalonly = true;
proxyNodeV4.validate = function (sectionId: string, value: string) {
if (this.section.formvalue(sectionId, 'ip_version_split') === '1' && !value) {
return _('Configure IPv4 Proxy Node First');
}
return true;
}
ShuntFormUtils.loadProxyNodeOptions(proxyNodeV4, true);
let proxyNodeV6 = routeGridSection.taboption('_rule', form.ListValue, 'proxy_node_v6',
_('IPv6 Proxy Node'), _('IPv6 traffic exit node'));
proxyNodeV6.depends('ip_version_split', '1');
proxyNodeV6.modalonly = true;
proxyNodeV6.validate = function (sectionId: string, value: string) {
if (this.section.formvalue(sectionId, 'ip_version_split') === '1' && !value) {
return _('Configure IPv6 Proxy Node First');
}
return true;
}
ShuntFormUtils.loadProxyNodeOptions(proxyNodeV6, true);
// DNS服务器(动态生成选项)
let dnsNode = routeGridSection.taboption('_dns', form.ListValue, 'dns_node', _('DNS Server'), _('Only Effective with Domain/GeoSite/Rule-set'));
// dnsNode.modalonly = true;
ShuntFormUtils.loadDNSServerOptions(dnsNode, true); // 包含默认选项
dnsNode.textvalue = function (sectionId) {
let dnsNode = uci.get(LuciFlied.CONF_NAME, sectionId, 'dns_node');
if (!dnsNode) {
return _('-');
}
let dnsProxyNode = uci.get(LuciFlied.CONF_NAME, sectionId, 'dns_proxy_node');
let value: string;
if (!dnsProxyNode) {
value = dnsNode
} else {
dnsProxyNode = dnsProxyNode === 'hijpass-direct' ? _('Direct') : dnsProxyNode;
value = dnsNode + ' -> ' + dnsProxyNode;
}
return value;
}
let dnsStrategy = routeGridSection.taboption('_dns', form.ListValue, 'dns_strategy', _('DNS Resolution Strategy'));
dnsStrategy.value('', _('Use Global Default Resolution Strategy'))
HijpassValues.DNS_STRATEGY_VALUES.forEach(function (strategy) {
dnsStrategy.value(strategy.value, _(strategy.label));
});
dnsStrategy.modalonly = true;
let dnsProxyNode = routeGridSection.taboption('_dns', form.ListValue, 'dns_proxy_node', _('DNS Exit Node'));
dnsProxyNode.value('', _('Use Routing Proxy Node'));
dnsProxyNode.value('hijpass-direct', _('Direct'));
uci.sections(LuciFlied.CONF_NAME, LuciFlied.PROXY_NODE_TYPE, function (section) {
if (section.name && section.enabled === '1') {
let displayName = section.name;
if (section.socks_port) {
displayName += ' (' + section.socks_port + ')';
}
dnsProxyNode.value(section.name, displayName);
}
});
dnsProxyNode.modalonly = true;
dnsProxyNode.validate = function (sectionId: string, value: string) {
const section = this.section;
if (section.formvalue(sectionId, 'ip_version_split') === '1' && !value && hasSplitDnsTarget(section, sectionId)) {
return _('Configure DNS Exit Node when splitting IPv4/IPv6 exit');
}
return true;
}
// 网络
let network = routeGridSection.taboption('_rule', form.MultiValue, 'network', _('Network'));
network.value('tcp');
network.value('udp');
network.cfgvalue = function (sectionId) {
let networks = uci.get(LuciFlied.CONF_NAME, sectionId, 'network');
if (networks) {
return networks.join(',');
}
return '-';
}
// network.modalonly = true
// 协议
let protocol = routeGridSection.taboption('_rule', form.MultiValue, 'protocol', _('Protocol'), _('Current Xray only supports http, tls, quic, and bittorrent'));
protocol.value('tls');
protocol.value('http');
protocol.value('quic');
protocol.value('stun');
protocol.value('dns');
protocol.value('bittorrent');
protocol.value('dtls');
protocol.value('ssh');
protocol.value('rdp');
protocol.value('ntp');
// protocol.modalonly = true
protocol.validate = function (_sectionId: string, value: any) {
const shuntType = getCurrentShuntValue(this.section, 'type');
if (shuntType !== CORE_TYPE.XRAY) {
return true;
}
const selectedProtocols = Array.isArray(value) ? value : (value ? [value] : []);
const unsupported = selectedProtocols.filter((item) => XRAY_ROUTE_PROTOCOLS.indexOf(item) === -1);
if (unsupported.length > 0) {
return _('Xray only supports route protocol filters: %s').format(XRAY_ROUTE_PROTOCOLS.join(', '));
}
return true;
}
protocol.cfgvalue = function (sectionId) {
let protocols = uci.get(LuciFlied.CONF_NAME, sectionId, 'protocol');
if (protocols) {
return protocols.join(',')
}
return '-';
}
// 端口
let port = routeGridSection.taboption('_rule', form.DynamicList, 'port', _('Port'));
port.datatype = 'port';
port.placeholder = '80';
port.modalonly = true
// 端口范围
let portRange = routeGridSection.taboption('_rule', form.DynamicList, 'port_range', _('Port Range'));
portRange.datatype = 'portrange';
portRange.placeholder = '1000-2000';
portRange.modalonly = true
let portView = routeGridSection.option(form.TextValue, '_port_view', _('Port Range'))
portView.textvalue = function (sectionId) {
let port = uci.get(LuciFlied.CONF_NAME, sectionId, 'port');
let portRange = uci.get(LuciFlied.CONF_NAME, sectionId, 'port_range');
let value = []
if (port) {
value.push(...port)
}
if (portRange) {
value.push(...portRange)
}
return value.join(',') || '-';
}
portView.modalonly = false
// 域名规则列表
let domainList = routeGridSection.taboption('_rule', form.TextValue, 'domain_list', _('Domain Rule List'), _('Plain string: When this string matches any part of the target domain, the rule takes effect.
' +
'Regular expression: Starts with \'regexp:\', followed by a regular expression.
' +
'Subdomain (recommended): Starts with \'domain:\', followed by a domain. The rule takes effect when this domain is the target domain or one of its subdomains.
' +
'Full match: Starts with \'full:\', followed by a domain. The rule takes effect when the target domain exactly matches this domain.
' +
'geosite: e.g. \'geosite:cn\'.
' +
'rule-set: e.g. \'rule-set:local:/path/rulefile\' or \'rule-set:remote:https://file\'.
' +
'Comment: starts with #'));
domainList.rows = 4;
domainList.monospace = true;
domainList.modalonly = true
configureGeoRuleQuery(domainList, 'geosite');
let domainListView = routeGridSection.option(form.TextValue, '_domain_list_view', _('Domain Rule Count'))
domainListView.textvalue = function (sectionId) {
let domainList = uci.get(LuciFlied.CONF_NAME, sectionId, 'domain_list');
if (domainList) {
return domainList.split('\n').filter(item => !item.startsWith('#')).length;
}
return '-';
}
// IP规则列表
let ipList = routeGridSection.taboption('_rule', form.TextValue, 'ip_list', _('IP Rule List'), _('IP: e.g. \'127.0.0.1\'.
' +
'CIDR: e.g. \'10.0.0.0/8\'.
' +
'geoip: e.g. \'geoip:cn\'.
' +
'rule-set: e.g. \'rule-set:local:/path/rulefile\' or \'rule-set:remote:https://file\'.
' +
'Comment: starts with #'));
ipList.rows = 4;
ipList.monospace = true;
ipList.modalonly = true
configureGeoRuleQuery(ipList, 'geoip');
let ipListView = routeGridSection.option(form.TextValue, '_ip_list_view', _('IP Rule Count'))
ipListView.textvalue = function (sectionId) {
let ipList = uci.get(LuciFlied.CONF_NAME, sectionId, 'ip_list');
if (ipList) {
return ipList.split('\n').filter(item => !item.startsWith('#')).length;
}
return '-';
}
}
// 创建DNS服务器配置选项
function createDNSOptions(s: LuCI.form.AbstractSection) {
let dnsTypeSectionValue = ShuntFormUtils.createSectionContainer(s, 'dns', '_dns_global',
form.TypedSection, LuciFlied.SHUNT_SECTION_TYPE, '');
let dnsTypeSection = dnsTypeSectionValue.subsection;
FormUtils.setAnonymousSection(dnsTypeSection);
// 默认全局DNS配置
let defaultDNSNode = ShuntFormUtils.createRequiredListValue(dnsTypeSection, 'default_dns_node', _('Default Global DNS'),
_('Select Default DNS'),
function (section_id: string, value: any) {
if (!value) {
return _('Please configure the global DNS server first');
}
return true;
});
ShuntFormUtils.loadDNSServerOptions(defaultDNSNode, false);
let rulesetDNSNode = dnsTypeSection.option(form.ListValue, 'ruleset_dns_node',
_('Rule-set DNS'), _('Select Rule-set DNS'));
ShuntFormUtils.loadDNSServerOptions(rulesetDNSNode, true);
let defaultStrategy = dnsTypeSection.option(form.ListValue, 'default_strategy', _('Internal DNS Resolution Strategy'),
_('Select DNS Resolution Strategy'));
HijpassValues.DNS_STRATEGY_VALUES.forEach(function (strategy) {
defaultStrategy.value(strategy.value, _(strategy.label));
});
defaultStrategy.rmempty = false;
let useCache = dnsTypeSection.option(form.Flag, 'use_cache', _('DNS Cache'));
useCache.rmempty = false;
let dnsGridSectionValue = s.taboption('dns', form.SectionValue, '_dns_servers',
form.GridSection, LuciFlied.SHUNT_DNS_NODE_TYPE, _('DNS Node Configuration'));
dnsGridSectionValue.depends({config_type: "tmpl"})
let dnsGridSection = dnsGridSectionValue.subsection;
dnsGridSection.rowcolors = true
dnsGridSection.anonymous = true;
dnsGridSection.addremove = true;
dnsGridSection.sortable = true;
dnsGridSection.nodescriptions = true;
dnsGridSection.modaltitle = _('Edit DNS Server');
dnsGridSection.sectiontitle = function (section_id) {
return uci.get(LuciFlied.CONF_NAME, section_id, 'tag') || _('Unnamed');
};
let enabled = dnsGridSection.option(form.Flag, 'enabled', _('Enable'));
enabled.default = '1';
enabled.rmempty = false;
enabled.editable = true;
// DNS类型
let type = dnsGridSection.option(form.ListValue, 'type', _('Type'),
_('DNS Server Type'));
type.value('tcp', 'TCP');
type.value('udp', 'UDP');
type.value('tls', _('TLS (sing-box only)'));
type.value('https', 'HTTPS');
type.value('quic', 'QUIC');
type.default = 'tcp';
type.rmempty = false;
type.validate = function (_sectionId: string, value: string) {
return value === 'tls' && getCurrentShuntValue(this.section, 'type') === CORE_TYPE.XRAY
? _('TLS (sing-box only)')
: true;
};
// DNS标签
let tag = dnsGridSection.option(form.Value, 'tag', _('Tag'),
_('DNS Server Tag'));
tag.placeholder = 'dns-local';
tag.rmempty = false;
tag.validate = ValidationUtils.createTagValidator(LuciFlied.CONF_NAME, LuciFlied.SHUNT_DNS_NODE_TYPE, 'tag')
tag.modalonly = true
// DNS服务器地址
let server = dnsGridSection.option(form.Value, 'server', _('DNS Server'),
_('DNS Server Address'));
server.placeholder = '8.8.8.8';
server.datatype = 'ipaddr';
server.rmempty = false;
// DNS服务器端口
let port = dnsGridSection.option(form.Value, 'port', _('Port'),
_('DNS Server Port'));
port.placeholder = '53';
port.datatype = 'port';
port.rmempty = true;
}
// 创建所有标签页
function createTabs(s: LuCI.form.AbstractSection) {
s.tab('basic', _('Basic Configuration'));
s.tab('config', _('Shunt Configuration Editor'));
s.tab('route', _('Routing Rules'));
s.tab('dns', _('Core DNS'));
s.tab('ruleset', _('Rule-set'));
}
export default v;