mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-07-27 02:11:19 +08:00
938 lines
34 KiB
Ucode
938 lines
34 KiB
Ucode
#!/usr/bin/ucode
|
|
|
|
'use strict';
|
|
|
|
import { readfile, writefile } from 'fs';
|
|
import { connect } from 'ubus';
|
|
import { cursor } from 'uci';
|
|
|
|
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';
|
|
|
|
const ubus = connect();
|
|
|
|
/* UCI config START */
|
|
const uci = cursor();
|
|
|
|
const uciconf = 'fchomo';
|
|
uci.load(uciconf);
|
|
|
|
const ucifchm = 'config',
|
|
ucires = 'resources',
|
|
uciroute = 'routing';
|
|
|
|
const uciglobal = 'global',
|
|
uciinbound = 'inbound',
|
|
ucitls = 'tls',
|
|
uciapi = 'api',
|
|
ucisniffer = 'sniffer',
|
|
ucidns = 'dns',
|
|
uciexpr = 'experimental';
|
|
|
|
const ucisniff = 'sniff',
|
|
ucidnser = 'dns_server',
|
|
ucidnspoli = 'dns_policy',
|
|
ucidnsnpoli = 'dns_node_policy',
|
|
ucipgrp = 'proxy_group',
|
|
uciinbd = 'inbound',
|
|
ucinode = 'node',
|
|
uciprov = 'provider',
|
|
ucichain = 'dialer_proxy',
|
|
ucirule = 'ruleset',
|
|
ucirout = 'rules',
|
|
ucisubro = 'subrules';
|
|
|
|
/* Hardcode options */
|
|
const port_presets = {
|
|
common_tcpport: uci.get(uciconf, ucifchm, 'common_tcpport') || '20-21,22,53,80,110,143,443,853,873,993,995,5222,8080,8443,9418',
|
|
common_udpport: uci.get(uciconf, ucifchm, 'common_udpport') || '20-21,22,53,80,110,143,443,853,993,995,8080,8443,9418',
|
|
smtp_tcpport: uci.get(uciconf, ucifchm, 'smtp_tcpport') || '465,587',
|
|
stun_port: uci.get(uciconf, ucifchm, 'stun_port') || '3478,19302',
|
|
turn_port: uci.get(uciconf, ucifchm, 'turn_port') || '5349',
|
|
google_fcm_port: uci.get(uciconf, ucifchm, 'google_fcm_port') || '443,5228-5230',
|
|
steam_client_port: uci.get(uciconf, ucifchm, 'steam_client_port') || '27015-27050',
|
|
steam_p2p_udpport: uci.get(uciconf, ucifchm, 'steam_p2p_udpport') || '3478,4379,4380,27000-27100',
|
|
},
|
|
tun_name = uci.get(uciconf, ucifchm, 'tun_name') || 'hmtun0',
|
|
tun_addr4 = uci.get(uciconf, ucifchm, 'tun_addr4') || '198.19.0.1/30',
|
|
tun_addr6 = uci.get(uciconf, ucifchm, 'tun_addr6') || 'fdfe:dcba:9877::1/126',
|
|
route_table_id = strToInt(uci.get(uciconf, ucifchm, 'route_table_id')) || 2022, // global.js
|
|
route_rule_pref = strToInt(uci.get(uciconf, ucifchm, 'route_rule_pref')) || 9000, // global.js
|
|
redirect_gate_mark = strToInt(uci.get(uciconf, ucifchm, 'redirect_gate_mark')) || 2023,
|
|
redirect_pass_mark = strToInt(uci.get(uciconf, ucifchm, 'redirect_pass_mark')) || 2024,
|
|
self_mark = strToInt(uci.get(uciconf, ucifchm, 'self_mark')) || 200, // global.js
|
|
tproxy_mark = strToInt(uci.get(uciconf, ucifchm, 'tproxy_mark')) || 201, // global.js
|
|
tun_mark = strToInt(uci.get(uciconf, ucifchm, 'tun_mark')) || 202, // global.js
|
|
posh = 'c2luZ2JveA'; // Yes. Is true.
|
|
|
|
const listen_interfaces = uci.get(uciconf, uciroute, 'listen_interfaces') || null,
|
|
bind_interface = uci.get(uciconf, uciroute, 'bind_interface') || null,
|
|
lan_filter = uci.get(uciconf, uciroute, 'lan_filter') || null,
|
|
lan_direct_ipv4_ips = uci.get(uciconf, uciroute, 'lan_direct_ipv4_ips') || null,
|
|
lan_direct_ipv6_ips = uci.get(uciconf, uciroute, 'lan_direct_ipv6_ips') || null,
|
|
lan_direct_mac_addrs = uci.get(uciconf, uciroute, 'lan_direct_mac_addrs') || null,
|
|
lan_proxy_ipv4_ips = uci.get(uciconf, uciroute, 'lan_proxy_ipv4_ips') || null,
|
|
lan_proxy_ipv6_ips = uci.get(uciconf, uciroute, 'lan_proxy_ipv6_ips') || null,
|
|
lan_proxy_mac_addrs = uci.get(uciconf, uciroute, 'lan_proxy_mac_addrs') || null,
|
|
proxy_router = (uci.get(uciconf, uciroute, 'proxy_router') === '0') ? null : true,
|
|
top_upstream = (uci.get(uciconf, uciroute, 'top_upstream') === '1') || null,
|
|
client_enabled = (uci.get(uciconf, uciroute, 'client_enabled') === '1') || null,
|
|
routing_tcpport = uci.get(uciconf, uciroute, 'routing_tcpport') || [],
|
|
routing_udpport = uci.get(uciconf, uciroute, 'routing_udpport') || [],
|
|
routing_mode = uci.get(uciconf, uciroute, 'routing_mode') || null,
|
|
routing_domain = strToBool(uci.get(uciconf, uciroute, 'routing_domain')),
|
|
routing_dscp_mode = uci.get(uciconf, uciroute, 'routing_dscp_mode') || null,
|
|
routing_dscp_list = uci.get(uciconf, uciroute, 'routing_dscp_list') || null,
|
|
tposh = 'c2luZ2JveA';
|
|
|
|
/* WAN DNS server array */
|
|
let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server'];
|
|
if (length(wan_dns) === 0)
|
|
wan_dns = ['223.5.5.5'];
|
|
|
|
/* All DNS server object */
|
|
const dnsservers = {};
|
|
uci.foreach(uciconf, ucidnser, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return;
|
|
|
|
dnsservers[cfg['.name']] = {
|
|
label: cfg.label,
|
|
address: cfg.address
|
|
};
|
|
});
|
|
|
|
/* UCI config END */
|
|
|
|
/* Config helper START */
|
|
function parse_filter(cfg) {
|
|
if (isEmpty(cfg))
|
|
return null;
|
|
|
|
if (type(cfg) === 'array')
|
|
return join('|', cfg);
|
|
else
|
|
return cfg;
|
|
}
|
|
|
|
function get_proxy(cfg, no_verify) {
|
|
if (isEmpty(cfg))
|
|
return null;
|
|
|
|
if (cfg in PRESET_OUTBOUND)
|
|
return cfg;
|
|
|
|
const label = uci.get(uciconf, cfg, 'label');
|
|
if (isEmpty(label)) {
|
|
if (no_verify)
|
|
return cfg;
|
|
else
|
|
die(sprintf("%s's label is missing, please check your configuration.", cfg));
|
|
} else
|
|
return label;
|
|
}
|
|
|
|
function get_nameserver(cfg, detour) {
|
|
if (isEmpty(cfg))
|
|
return [];
|
|
|
|
if ('block-dns' in cfg)
|
|
//https://github.com/MetaCubeX/mihomo/blob/0128a0bb1fce17d39158c745a912d7b2b87cf975/config/config.go#L1131
|
|
return 'rcode://refused';
|
|
|
|
let servers = [];
|
|
for (let k in cfg) {
|
|
if (k === 'system-dns') {
|
|
push(servers, 'system');
|
|
} else if (k === 'default-dns') {
|
|
map(wan_dns, (dns) => {
|
|
push(servers, dns + '#DIRECT');
|
|
});
|
|
} else
|
|
push(servers, replace(dnsservers[k]?.address || '', /#detour=([^&]+)/, (m, c1) => {
|
|
return '#' + urlencode(get_proxy(detour || c1));
|
|
}));
|
|
}
|
|
|
|
return servers;
|
|
}
|
|
|
|
function parse_entry(cfg) {
|
|
if (isEmpty(cfg))
|
|
return null;
|
|
|
|
let rule = json(cfg);
|
|
if (rule.detour)
|
|
rule.detour = get_proxy(rule.detour);
|
|
|
|
function _payloadStrategy(payload) {
|
|
// LOGIC_TYPE,((payload1),(payload2))
|
|
if (payload.factor === null || (type(payload.factor) in ['bool', 'int', 'string'])) {
|
|
return sprintf(payload.deny ? 'NOT,((%s))' : '%s', join(',', [payload.type, payload.factor ?? '']));
|
|
} else if (type(payload.factor) === 'array') {
|
|
return sprintf(`${payload.type},(%s)`, join(',', map(payload.factor, p => `(${_payloadStrategy(p)})`)));
|
|
} else if (type(payload.factor) === 'object') {
|
|
die(sprintf(`Factor type cannot be an object: '%J'`, payload.factor));
|
|
} else
|
|
die(`Factor type is incorrect: '${payload.factor}'`);
|
|
}
|
|
|
|
// toMihomo
|
|
let logical = (rule.type in RULES_LOGICAL_TYPE);
|
|
let payload = _payloadStrategy(logical ? {type: rule.type, factor: rule.payload} : rule.payload[0]);
|
|
|
|
if (rule.subrule)
|
|
return sprintf('SUB-RULE,(%s),%s', payload, rule.subrule);
|
|
else
|
|
if (rule.type === 'MATCH')
|
|
return join(',', [rule.type, rule.detour]);
|
|
else
|
|
return join(',', [...[payload, rule.detour],
|
|
...(rule.params ? keys(rule.params) : [])
|
|
]);
|
|
}
|
|
/* Config helper END */
|
|
|
|
/* Main */
|
|
const config = {};
|
|
|
|
/* All Proxy chain object */
|
|
const dialerproxy = {};
|
|
uci.foreach(uciconf, ucichain, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return;
|
|
|
|
let identifier = '';
|
|
if (cfg.type === 'provider')
|
|
identifier = cfg.chain_head_sub;
|
|
else if (cfg.type === 'node')
|
|
identifier = cfg.chain_head;
|
|
else
|
|
return;
|
|
|
|
dialerproxy[identifier] = {
|
|
detour: get_proxy(cfg.chain_tail_group) || get_proxy(cfg.chain_tail)
|
|
};
|
|
});
|
|
|
|
/* General START */
|
|
/* General settings */
|
|
config["global-ua"] = 'clash.meta';
|
|
config.mode = uci.get(uciconf, uciglobal, 'mode') || 'rule';
|
|
config["find-process-mode"] = uci.get(uciconf, uciglobal, 'find_process_mode') || 'off';
|
|
config["log-level"] = uci.get(uciconf, uciglobal, 'log_level') || 'warning';
|
|
config["etag-support"] = (uci.get(uciconf, uciglobal, 'etag_support') === '0') ? false : true;
|
|
config.ipv6 = (uci.get(uciconf, uciglobal, 'ipv6') === '0') ? false : true;
|
|
config["unified-delay"] = strToBool(uci.get(uciconf, uciglobal, 'unified_delay')) || false;
|
|
config["tcp-concurrent"] = strToBool(uci.get(uciconf, uciglobal, 'tcp_concurrent')) || false;
|
|
config["keep-alive-interval"] = durationToSecond(uci.get(uciconf, uciglobal, 'keep_alive_interval')) ?? 30;
|
|
config["keep-alive-idle"] = durationToSecond(uci.get(uciconf, uciglobal, 'keep_alive_idle')) ?? 600;
|
|
/* ACL settings */
|
|
config["interface-name"] = bind_interface;
|
|
config["routing-mark"] = self_mark;
|
|
/* Global Authentication */
|
|
config.authentication = uci.get(uciconf, uciglobal, 'authentication');
|
|
config["skip-auth-prefixes"] = uci.get(uciconf, uciglobal, 'skip_auth_prefixes');
|
|
/* General END */
|
|
|
|
/* GEOX START */
|
|
/* GEOX settings */
|
|
config["geodata-mode"] = true;
|
|
config["geodata-loader"] = 'memconservative';
|
|
config["geo-auto-update"] = false;
|
|
/* GEOX END */
|
|
|
|
/* TLS START */
|
|
/* TLS settings */
|
|
config.tls = {
|
|
"certificate": uci.get(uciconf, ucitls, 'tls_cert_path'),
|
|
"private-key": uci.get(uciconf, ucitls, 'tls_key_path'),
|
|
"client-auth-type": uci.get(uciconf, ucitls, 'tls_client_auth_type'),
|
|
"client-auth-cert": uci.get(uciconf, ucitls, 'tls_client_auth_cert_path'),
|
|
"ech-key": uci.get(uciconf, ucitls, 'tls_ech_key')
|
|
};
|
|
/* TLS END */
|
|
|
|
/* API START */
|
|
const api_port = uci.get(uciconf, uciapi, 'external_controller_port');
|
|
const api_tls_port = uci.get(uciconf, uciapi, 'external_controller_tls_port');
|
|
const api_routing_mark = uci.get(uciconf, uciapi, 'external_controller_routing_mark');
|
|
/* API settings */
|
|
config["external-controller-cors"] = {
|
|
"allow-origins": uci.get(uciconf, uciapi, 'external_controller_cors_allow_origins') || ['*'],
|
|
"allow-private-network" : (uci.get(uciconf, uciapi, 'external_controller_cors_allow_private_network') === '0') ? false : true
|
|
};
|
|
config["external-controller-routing-mark"] = strToInt(api_routing_mark) || null;
|
|
config["external-controller"] = api_port ? '[::]:' + api_port : null;
|
|
config["external-controller-tls"] = api_tls_port ? '[::]:' + api_tls_port : null;
|
|
config["external-doh-server"] = uci.get(uciconf, uciapi, 'external_doh_server');
|
|
config.secret = uci.get(uciconf, uciapi, 'secret') || trim(readfile('/proc/sys/kernel/random/uuid'));
|
|
config["external-ui"] = RUN_DIR + '/ui';
|
|
config["external-ui-url"] = `https://codeload.github.com/${uci.get(uciconf, uciapi, 'dashboard_repo')}/zip/refs/heads/gh-pages`;
|
|
/* API END */
|
|
|
|
/* Cache START */
|
|
/* Cache settings */
|
|
config.profile = {
|
|
"store-selected": true,
|
|
"store-fake-ip": false
|
|
};
|
|
/* Cache END */
|
|
|
|
/* Experimental START */
|
|
/* Experimental settings */
|
|
config.experimental = {
|
|
"quic-go-disable-gso": strToBool(uci.get(uciconf, uciexpr, 'quic_go_disable_gso')),
|
|
"quic-go-disable-ecn": strToBool(uci.get(uciconf, uciexpr, 'quic_go_disable_ecn')),
|
|
"dialer-ip4p-convert": strToBool(uci.get(uciconf, uciexpr, 'dialer_ip4p_convert'))
|
|
};
|
|
/* Experimental END */
|
|
|
|
/* Sniffer START */
|
|
/* Sniffer settings */
|
|
config.sniffer = {
|
|
enable: true,
|
|
"force-dns-mapping": true,
|
|
"parse-pure-ip": true,
|
|
"override-destination": (uci.get(uciconf, ucisniffer, 'override_destination') === '0') ? false : true,
|
|
sniff: {},
|
|
"force-domain": uci.get(uciconf, ucisniffer, 'force_domain'),
|
|
"skip-domain": uci.get(uciconf, ucisniffer, 'skip_domain'),
|
|
"skip-src-address": uci.get(uciconf, ucisniffer, 'skip_src_address'),
|
|
"skip-dst-address": uci.get(uciconf, ucisniffer, 'skip_dst_address')
|
|
};
|
|
/* Sniff protocol settings */
|
|
uci.foreach(uciconf, ucisniff, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return null;
|
|
|
|
config.sniffer.sniff[cfg.protocol] = {
|
|
ports: map(cfg.ports, ports => strToInt(ports) || null), // @DEBUG ERROR data type *utils.IntRanges[uint16]
|
|
"override-destination": (cfg.override_destination === '0') ? false : true
|
|
};
|
|
});
|
|
/* Sniffer END */
|
|
|
|
/* Inbound START */
|
|
const proxy_mode = uci.get(uciconf, uciinbound, 'proxy_mode') || 'redir_tproxy';
|
|
/* Listen ports */
|
|
config["allow-lan"] = true;
|
|
config["bind-address"] = "*";
|
|
config["mixed-port"] = strToInt(uci.get(uciconf, uciinbound, 'mixed_port')) || 7890;
|
|
if (match(proxy_mode, /redir/))
|
|
config["redir-port"] = strToInt(uci.get(uciconf, uciinbound, 'redir_port')) || 7891;
|
|
if (match(proxy_mode, /tproxy/))
|
|
config["tproxy-port"] = strToInt(uci.get(uciconf, uciinbound, 'tproxy_port')) || 7892;
|
|
config.listeners = [];
|
|
push(config.listeners, {
|
|
name: 'dns-in',
|
|
type: 'tunnel',
|
|
port: strToInt(uci.get(uciconf, uciinbound, 'tunnel_port')) || 7893,
|
|
listen: '::',
|
|
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;
|
|
|
|
const listener = parseListener(cfg);
|
|
listener.proxy = get_proxy(listener.proxy);
|
|
if (listener["shadow-tls"])
|
|
listener["shadow-tls"].handshake.proxy = get_proxy(listener["shadow-tls"].handshake.proxy);
|
|
if (listener["res-tls"])
|
|
listener["res-tls"].proxy = get_proxy(listener["res-tls"].proxy);
|
|
if (listener["jls-config"])
|
|
listener["jls-config"].proxy = get_proxy(listener["jls-config"].proxy);
|
|
if (listener["jls-upstream"])
|
|
listener["jls-upstream"].proxy = get_proxy(listener["jls-upstream"].proxy);
|
|
|
|
push(config.listeners, listener);
|
|
});
|
|
/* Tun settings */
|
|
if (match(proxy_mode, /tun/))
|
|
config.tun = {
|
|
enable: true,
|
|
device: tun_name,
|
|
stack: uci.get(uciconf, uciinbound, 'tun_stack') || 'system',
|
|
"dns-hijack": ['udp://[::]:53', 'tcp://[::]:53'],
|
|
"inet4-address": [ tun_addr4 ],
|
|
"inet6-address": [ tun_addr6 ],
|
|
mtu: strToInt(uci.get(uciconf, uciinbound, 'tun_mtu')) || 9000,
|
|
gso: strToBool(uci.get(uciconf, uciinbound, 'tun_gso')) || false,
|
|
"gso-max-size": strToInt(uci.get(uciconf, uciinbound, 'tun_gso_max_size')) || 65536,
|
|
"auto-route": false,
|
|
"iproute2-table-index": route_table_id,
|
|
"iproute2-rule-index": route_rule_pref,
|
|
"auto-redirect": false,
|
|
"auto-redirect-input-mark": redirect_gate_mark,
|
|
"auto-redirect-output-mark": redirect_pass_mark,
|
|
"strict-route": false,
|
|
"route-address": [
|
|
"0.0.0.0/1",
|
|
"128.0.0.0/1",
|
|
"::/1",
|
|
"8000::/1"
|
|
],
|
|
"route-exclude-address": [
|
|
"192.168.0.0/16",
|
|
"fc00::/7"
|
|
],
|
|
"route-address-set": [],
|
|
"route-exclude-address-set": [],
|
|
"include-interface": [],
|
|
"exclude-interface": [],
|
|
"udp-timeout": durationToSecond(uci.get(uciconf, uciinbound, 'tun_udp_timeout')) || 300,
|
|
"endpoint-independent-nat": strToBool(uci.get(uciconf, uciinbound, 'tun_endpoint_independent_nat')),
|
|
"disable-icmp-forwarding": (uci.get(uciconf, uciinbound, 'tun_disable_icmp_forwarding') === '0') ? false : true,
|
|
"auto-detect-interface": true
|
|
};
|
|
/* Inbound END */
|
|
|
|
/* DNS START */
|
|
/* DNS settings */
|
|
config.dns = {
|
|
enable: true,
|
|
"prefer-h3": false,
|
|
listen: '[::]:' + (uci.get(uciconf, ucidns, 'dns_port') || '7853'),
|
|
"listen-routing-mark": strToInt(uci.get(uciconf, ucidns, 'routing_mark')) || null,
|
|
ipv6: (uci.get(uciconf, ucidns, 'ipv6') === '0') ? false : true,
|
|
"enhanced-mode": 'redir-host',
|
|
"use-hosts": true,
|
|
"use-system-hosts": true,
|
|
"respect-rules": true,
|
|
"default-nameserver": get_nameserver(uci.get(uciconf, ucidns, 'boot_server')),
|
|
"proxy-server-nameserver": get_nameserver(uci.get(uciconf, ucidns, 'bootnode_server')),
|
|
"proxy-server-nameserver-policy": {},
|
|
nameserver: get_nameserver(uci.get(uciconf, ucidns, 'default_server')),
|
|
fallback: get_nameserver(uci.get(uciconf, ucidns, 'fallback_server')),
|
|
"nameserver-policy": {},
|
|
"fallback-filter": {
|
|
geoip: false
|
|
}
|
|
};
|
|
/* DNS policy */
|
|
map([
|
|
[ucidnspoli, "nameserver-policy"], // DNS policy
|
|
[ucidnsnpoli, "proxy-server-nameserver-policy"] // Bootstrap DNS policy (Node)
|
|
], (e) => {
|
|
const sectiontype = e[0];
|
|
const field = e[1];
|
|
|
|
uci.foreach(uciconf, sectiontype, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return null;
|
|
|
|
let key;
|
|
if (cfg.type === 'domain') {
|
|
key = isEmpty(cfg.domain) ? null : join(',', cfg.domain);
|
|
} else if (cfg.type === 'geosite') {
|
|
key = isEmpty(cfg.geosite) ? null : 'geosite:' + join(',', cfg.geosite);
|
|
} else if (cfg.type === 'rule_set') {
|
|
key = isEmpty(cfg.rule_set) ? null : 'rule-set:' + join(',', cfg.rule_set);
|
|
};
|
|
|
|
if (!key)
|
|
return null;
|
|
|
|
config.dns[field][key] = get_nameserver(cfg.server, cfg.proxy);
|
|
});
|
|
});
|
|
/* Fallback filter */
|
|
if (!isEmpty(config.dns.fallback))
|
|
config.dns["fallback-filter"] = {
|
|
geoip: (uci.get(uciconf, ucidns, 'fallback_filter_geoip') === '0') ? false : true,
|
|
"geoip-code": uci.get(uciconf, ucidns, 'fallback_filter_geoip_code') || 'cn',
|
|
geosite: uci.get(uciconf, ucidns, 'fallback_filter_geosite') || [],
|
|
ipcidr: uci.get(uciconf, ucidns, 'fallback_filter_ipcidr') || [],
|
|
domain: uci.get(uciconf, ucidns, 'fallback_filter_domain') || [],
|
|
};
|
|
config.dns["fallback-lazy-query"] = strToBool(uci.get(uciconf, ucidns, 'fallback_lazy_query'));
|
|
/* DNS END */
|
|
|
|
/* Hosts START */
|
|
/* Hosts */
|
|
config.hosts = {};
|
|
/* Hosts END */
|
|
|
|
/* Proxy Node START */
|
|
/* Proxy Node */
|
|
config.proxies = [
|
|
/*{
|
|
name: 'direct-out',
|
|
type: 'direct',
|
|
udp: true,
|
|
"ip-version": undefined,
|
|
"interface-name": undefined,
|
|
"routing-mark": undefined
|
|
},*/
|
|
{
|
|
name: 'dns-out',
|
|
type: 'dns'
|
|
}
|
|
];
|
|
uci.foreach(uciconf, ucinode, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return null;
|
|
|
|
push(config.proxies, {
|
|
name: cfg.label,
|
|
type: cfg.type,
|
|
|
|
server: cfg.server,
|
|
port: strToInt(cfg.port) || null,
|
|
|
|
/* Dial fields */
|
|
tfo: strToBool(cfg.tfo),
|
|
mptcp: strToBool(cfg.mptcp),
|
|
"dialer-proxy": dialerproxy[cfg['.name']]?.detour,
|
|
"interface-name": cfg.interface_name,
|
|
"routing-mark": strToInt(cfg.routing_mark) || null,
|
|
"ip-version": cfg.ip_version,
|
|
|
|
/* Rematch */
|
|
"target-rematch-name": cfg.target_rematch_name,
|
|
"target-sub-rule": cfg.target_sub_rule,
|
|
|
|
/* HTTP / SOCKS / Shadowsocks / VMess / VLESS / Trojan / TUIC / hysteria2 / ShadowQUIC / WireGuard / Masque */
|
|
username: cfg.username,
|
|
uuid: cfg.vmess_uuid || cfg.uuid,
|
|
cipher: cfg.vmess_chipher || cfg.shadowsocks_chipher,
|
|
password: cfg.shadowsocks_password || cfg.password,
|
|
headers: cfg.headers ? json(cfg.headers) : null,
|
|
ip: cfg.masque_ip || cfg.wireguard_ip,
|
|
ipv6: cfg.masque_ipv6 || cfg.wireguard_ipv6,
|
|
mtu: strToInt(cfg.masque_mtu ?? cfg.wireguard_mtu) || null,
|
|
"remote-dns-resolve": strToBool(cfg.masque_remote_dns_resolve ?? cfg.wireguard_remote_dns_resolve),
|
|
dns: cfg.masque_dns || cfg.wireguard_dns,
|
|
|
|
/* Shadowsocks */
|
|
|
|
/* Mieru */
|
|
"port-range": cfg.mieru_port_range,
|
|
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,
|
|
"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,
|
|
"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,
|
|
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,
|
|
version: strToInt(cfg.snell_version),
|
|
reuse: strToBool(cfg.snell_reuse),
|
|
|
|
/* VMess / VLESS */
|
|
flow: cfg.vless_flow,
|
|
alterId: strToInt(cfg.vmess_alterid),
|
|
"global-padding": cfg.type === 'vmess' ? (cfg.vmess_global_padding === '0' ? false : true) : null,
|
|
"authenticated-length": strToBool(cfg.vmess_authenticated_length),
|
|
"packet-encoding": cfg.vmess_packet_encoding,
|
|
encryption: cfg.vless_encryption === '1' ? cfg.vless_encryption_encryption : null,
|
|
|
|
/* Trojan */
|
|
"ss-opts": cfg.trojan_ss_enabled === '1' ? {
|
|
enabled: true,
|
|
method: cfg.trojan_ss_chipher,
|
|
password: cfg.trojan_ss_password
|
|
} : null,
|
|
|
|
/* AnyTLS */
|
|
"idle-session-check-interval": durationToSecond(cfg.anytls_idle_session_check_interval),
|
|
"idle-session-timeout": durationToSecond(cfg.anytls_idle_session_timeout),
|
|
"min-idle-session": strToInt(cfg.anytls_min_idle_session),
|
|
|
|
/* TUIC */
|
|
ip: cfg.tuic_ip,
|
|
"udp-relay-mode": cfg.tuic_udp_relay_mode,
|
|
"udp-over-stream-version": cfg.tuic_udp_over_stream_version,
|
|
"max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size) || null,
|
|
"fast-open": strToBool(cfg.tuic_fast_open),
|
|
"reduce-rtt": strToBool(cfg.tuic_reduce_rtt),
|
|
"request-timeout": strToInt(cfg.tuic_request_timeout) || null,
|
|
|
|
/* Brutal */
|
|
up: strToInt(cfg.brutal_up_mbps),
|
|
down: strToInt(cfg.brutal_down_mbps),
|
|
|
|
/* Hysteria / Hysteria2 */
|
|
ports: isEmpty(cfg.hysteria_ports) ? null : join(',', cfg.hysteria_ports),
|
|
"hop-interval": strToInt(cfg.hysteria_hop_interval), // @DEBUG ERROR data type *utils.IntRanges[uint16]
|
|
obfs: cfg.hysteria_obfs_type,
|
|
"obfs-password": cfg.hysteria_obfs_password,
|
|
"obfs-min-packet-size": strToInt(cfg.hysteria_obfs_min_packet_size),
|
|
"obfs-max-packet-size": strToInt(cfg.hysteria_obfs_max_packet_size),
|
|
"realm-opts": cfg.hysteria2_realm === '1' ? {
|
|
enable: true,
|
|
"server-url": cfg.hysteria2_realm_server_url,
|
|
token: cfg.hysteria2_realm_token,
|
|
"realm-id": cfg.hysteria2_realm_id,
|
|
"stun-servers": cfg.hysteria2_realm_stun_servers,
|
|
// @TLS of server-url
|
|
//sni,
|
|
//alpn,
|
|
//"skip-cert-verify",
|
|
//fingerprint,
|
|
//certificate,
|
|
//"private-key"
|
|
} : null,
|
|
|
|
/* ShadowQUIC */
|
|
"quic-versions": cfg.shadowquic_quic_versions,
|
|
"zero-rtt": strToBool(cfg.shadowquic_zero_rtt),
|
|
// @# cwnd: 10 # default: 32,
|
|
// @# max-datagram-frame-size: 1400,
|
|
// @# recv-window-conn: 0,
|
|
// @# recv-window: 0,
|
|
// @# disable-mtu-discovery: false,
|
|
|
|
/* TrustTunnel */
|
|
"health-check": cfg.type === 'trusttunnel' ? (cfg.trusttunnel_health_check === '0' ? false : true) : null,
|
|
quic: strToBool(cfg.trusttunnel_quic),
|
|
|
|
/* WireGuard */
|
|
"pre-shared-key": cfg.wireguard_pre_shared_key,
|
|
"allowed-ips": cfg.wireguard_allowed_ips,
|
|
reserved: cfg.wireguard_reserved,
|
|
"persistent-keepalive": strToInt(cfg.wireguard_persistent_keepalive),
|
|
|
|
/* Masque */
|
|
network: cfg.masque_network || null,
|
|
|
|
/* SSH */
|
|
"private-key-passphrase": cfg.ssh_priv_key_passphrase,
|
|
"host-key-algorithms": cfg.ssh_host_key_algorithms,
|
|
"host-key": cfg.ssh_host_key,
|
|
|
|
/* Extra fields */
|
|
"udp-over-stream": strToBool(cfg.shadowquic_udp_over_stream || cfg.tuic_udp_over_stream),
|
|
"heartbeat-interval": strToInt(cfg.tuic_heartbeat) || null,
|
|
"keep-alive-interval": strToInt(cfg.shadowquic_heartbeat) || null,
|
|
"congestion-controller": cfg.congestion_controller,
|
|
"bbr-profile": cfg.bbr_profile,
|
|
"max-open-streams": strToInt(cfg.max_open_streams) || null,
|
|
|
|
"handshake-timeout": strToInt(cfg.handshake_timeout),
|
|
udp: strToBool(cfg.udp),
|
|
"udp-over-tcp": strToBool(cfg.uot),
|
|
"udp-over-tcp-version": cfg.uot_version,
|
|
|
|
/* Plugin fields */
|
|
...(cfg.plugin === '1' ? (
|
|
cfg.type in ['vmess', 'vless', 'trojan', 'anytls'] ? {
|
|
...arrToObj([[(cfg.type in ['vmess', 'vless']) ? 'servername' : 'sni', cfg.plugin_opts_host]]),
|
|
// shadow-tls
|
|
"shadow-tls-opts": cfg.plugin_type === 'shadow-tls' ? {
|
|
version: strToInt(cfg.plugin_opts_shadowtls_version),
|
|
password: cfg.plugin_opts_thetlspassword
|
|
} : null,
|
|
// restls
|
|
"restls-opts": cfg.plugin_type === 'restls' ? {
|
|
password: cfg.plugin_opts_thetlspassword,
|
|
"version-hint": cfg.plugin_opts_restls_versionhint,
|
|
"restls-script": cfg.plugin_opts_restls_script
|
|
} : null,
|
|
// jls
|
|
"jls-opts": cfg.plugin_type === 'jls' ? {
|
|
username: cfg.plugin_opts_thetlsusername,
|
|
password: cfg.plugin_opts_thetlspassword
|
|
} : null,
|
|
} : {
|
|
// snell / shadowsocks
|
|
plugin: cfg.type === 'snell' ? null : cfg.plugin_type,
|
|
...arrToObj([[cfg.type === 'snell' ? "obfs-opts" : "plugin-opts",
|
|
{
|
|
mode: (cfg.type === 'snell' && cfg.plugin_type !== 'obfs') ? cfg.plugin_type : cfg.plugin_opts_obfsmode,
|
|
host: cfg.plugin_opts_host,
|
|
username: cfg.plugin_opts_thetlsusername,
|
|
password: cfg.plugin_opts_thetlspassword,
|
|
version: strToInt(cfg.plugin_opts_shadowtls_version),
|
|
alpn: cfg.tls_alpn, // Array
|
|
"version-hint": cfg.plugin_opts_restls_versionhint,
|
|
"restls-script": cfg.plugin_opts_restls_script
|
|
}
|
|
]])
|
|
}
|
|
) : {}),
|
|
|
|
/* SSH / WireGuard / Masque */
|
|
/* TLS fields */
|
|
tls: (cfg.type in ['trojan', 'anytls', 'tuic', 'hysteria', 'hysteria2', 'shadowquic', 'trusttunnel', 'masque']) ? null : strToBool(cfg.tls),
|
|
"disable-sni": strToBool(cfg.tls_disable_sni),
|
|
...(cfg.tls_sni ? arrToObj([[(cfg.type in ['vmess', 'vless']) ? 'servername' : 'sni', cfg.tls_sni]]) : {}),
|
|
fingerprint: cfg.tls_fingerprint,
|
|
alpn: cfg.tls === '1' ? cfg.tls_alpn : null, // Array
|
|
"name-cert-verify": cfg.tls_name_cert_verify,
|
|
"skip-cert-verify": strToBool(cfg.tls_skip_cert_verify),
|
|
certificate: cfg.tls_cert_path, // mTLS
|
|
"private-key": cfg.masque_private_key || cfg.wireguard_private_key || cfg.ssh_priv_key || cfg.tls_key_path, // mTLS/SSH/WireGuard/Masque
|
|
"public-key": cfg.masque_endpoint_public_key || cfg.wireguard_peer_public_key, // WireGuard/Masque
|
|
"client-fingerprint": cfg.tls_client_fingerprint,
|
|
"ech-opts": cfg.tls_ech === '1' ? {
|
|
enable: true,
|
|
config: cfg.tls_ech_config,
|
|
"query-server-name": cfg.tls_ech_query_server_name
|
|
} : null,
|
|
"reality-opts": cfg.tls_reality === '1' ? {
|
|
"public-key": cfg.tls_reality_public_key,
|
|
"short-id": cfg.tls_reality_short_id,
|
|
"support-x25519mlkem768": strToBool(cfg.tls_reality_support_x25519mlkem768)
|
|
} : null,
|
|
|
|
/* Transport fields */
|
|
// https://github.com/muink/mihomo/blob/3e966e82c793ca99e3badc84bf3f2907b100edae/adapter/outbound/vmess.go#L74
|
|
...(cfg.transport_enabled === '1' ? {
|
|
network: cfg.transport_type,
|
|
"http-opts": cfg.transport_type === 'http' ? {
|
|
method: cfg.transport_http_method,
|
|
path: isEmpty(cfg.transport_paths) ? ['/'] : cfg.transport_paths, // Array
|
|
headers: cfg.transport_http_headers ? json(cfg.transport_http_headers) : null,
|
|
} : null,
|
|
"h2-opts": cfg.transport_type === 'h2' ? {
|
|
host: cfg.transport_hosts, // Array
|
|
path: cfg.transport_path || '/',
|
|
} : null,
|
|
"grpc-opts": cfg.transport_type === 'grpc' ? {
|
|
"grpc-service-name": cfg.transport_grpc_servicename,
|
|
"grpc-user-agent": cfg.transport_grpc_user_agent,
|
|
"ping-interval": strToInt(cfg.transport_grpc_ping_interval) || null,
|
|
"max-connections": strToInt(cfg.smux_max_connections) || null,
|
|
"min-streams": strToInt(cfg.smux_min_streams) || null,
|
|
"max-streams": strToInt(cfg.smux_max_streams) || null,
|
|
} : null,
|
|
"ws-opts": cfg.transport_type === 'ws' ? {
|
|
path: cfg.transport_path || '/',
|
|
headers: cfg.transport_http_headers ? json(cfg.transport_http_headers) : null,
|
|
"max-early-data": strToInt(cfg.transport_ws_max_early_data) || null,
|
|
"early-data-header-name": cfg.transport_ws_early_data_header,
|
|
"v2ray-http-upgrade": strToBool(cfg.transport_ws_v2ray_http_upgrade),
|
|
"v2ray-http-upgrade-fast-open": strToBool(cfg.transport_ws_v2ray_http_upgrade_fast_open)
|
|
} : null,
|
|
"xhttp-opts": cfg.transport_type === 'xhttp' ? {
|
|
host: cfg.transport_host,
|
|
path: cfg.transport_path || '/',
|
|
headers: cfg.transport_http_headers ? json(cfg.transport_http_headers) : null,
|
|
mode: cfg.transport_xhttp_mode,
|
|
"no-grpc-header": strToBool(cfg.transport_xhttp_no_grpc_header),
|
|
"x-padding-bytes": cfg.transport_xhttp_x_padding_bytes,
|
|
// @bypassing CDN's potential detection /* https://github.com/MetaCubeX/mihomo/commit/2337d70d86fa15efe7b69ee54bff6139ebfabcf6 */
|
|
"sc-max-each-post-bytes": strToInt(cfg.transport_xhttp_sc_max_each_post_bytes) || null,
|
|
"sc-min-posts-interval-ms": strToInt(cfg.transport_xhttp_sc_min_posts_interval_ms) || null,
|
|
"reuse-settings": cfg.transport_xhttp_xmux ? {
|
|
"max-concurrency": cfg.transport_xhttp_xmux_max_concurrency,
|
|
"max-connections": cfg.transport_xhttp_xmux_max_connections,
|
|
"c-max-reuse-times": cfg.transport_xhttp_xmux_max_reuse_times,
|
|
"h-max-request-times": cfg.transport_xhttp_xmux_max_request_times,
|
|
"h-max-reusable-secs": cfg.transport_xhttp_xmux_max_reusable_secs,
|
|
"h-keep-alive-period": strToInt(cfg.transport_xhttp_xmux_keep_alive_period)
|
|
// @download-settings
|
|
} : null
|
|
} : null
|
|
} : {}),
|
|
|
|
/* Multiplex fields */
|
|
...(cfg.type in ['trusttunnel'] ? {
|
|
"max-connections": strToInt(cfg.smux_max_connections) || null,
|
|
"min-streams": strToInt(cfg.smux_min_streams) || null,
|
|
"max-streams": strToInt(cfg.smux_max_streams) || null
|
|
} : {}),
|
|
smux: cfg.smux_enabled === '1' ? {
|
|
enabled: true,
|
|
protocol: cfg.smux_protocol,
|
|
"max-connections": strToInt(cfg.smux_max_connections) || null,
|
|
"min-streams": strToInt(cfg.smux_min_streams) || null,
|
|
"max-streams": strToInt(cfg.smux_max_streams) || null,
|
|
statistic: strToBool(cfg.smux_statistic),
|
|
"only-tcp": strToBool(cfg.smux_only_tcp),
|
|
padding: strToBool(cfg.smux_padding),
|
|
"brutal-opts": cfg.smux_brutal === '1' ? {
|
|
enabled: true,
|
|
up: strToInt(cfg.smux_brutal_up) || null, // Mbps
|
|
down: strToInt(cfg.smux_brutal_down) || null // Mbps
|
|
} : null
|
|
} : null
|
|
});
|
|
});
|
|
/* Proxy Node END */
|
|
|
|
/* Proxy Group START */
|
|
/* Proxy Group */
|
|
config["proxy-groups"] = [];
|
|
uci.foreach(uciconf, ucipgrp, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return null;
|
|
|
|
push(config["proxy-groups"], {
|
|
name: cfg.label,
|
|
type: cfg.type,
|
|
proxies: [
|
|
...map(cfg.groups || [], cfg => get_proxy(cfg)),
|
|
...map(cfg.proxies || [], cfg => get_proxy(cfg))
|
|
],
|
|
use: cfg.use,
|
|
"include-all": strToBool(cfg.include_all),
|
|
"include-all-proxies": strToBool(cfg.include_all_proxies),
|
|
"include-all-providers": strToBool(cfg.include_all_providers),
|
|
"empty-fallback": cfg.empty_fallback ? get_proxy(cfg.empty_fallback, true) : null,
|
|
// Select fields
|
|
"default-selected": cfg.default_selected ? get_proxy(cfg.default_selected, true) : null,
|
|
// Url-test fields
|
|
tolerance: (cfg.type === 'url-test') ? strToInt(cfg.tolerance) ?? 150 : null,
|
|
// Load-balance fields
|
|
strategy: cfg.strategy,
|
|
// Override fields
|
|
"disable-udp": strToBool(cfg.disable_udp) || false,
|
|
// Health fields
|
|
url: cfg.url,
|
|
interval: cfg.url ? durationToSecond(cfg.interval) ?? 600 : null,
|
|
timeout: cfg.url ? strToInt(cfg.timeout) || 5000 : null,
|
|
lazy: (cfg.lazy === '0') ? false : null,
|
|
"expected-status": cfg.url ? cfg.expected_status || '204' : null,
|
|
"max-failed-times": cfg.url ? strToInt(cfg.max_failed_times) ?? 5 : null,
|
|
// General fields
|
|
filter: parse_filter(cfg.filter),
|
|
"exclude-filter": parse_filter(cfg.exclude_filter),
|
|
"exclude-type": parse_filter(cfg.exclude_type),
|
|
hidden: strToBool(cfg.hidden),
|
|
icon: cfg.icon
|
|
});
|
|
});
|
|
/* Proxy Group END */
|
|
|
|
/* Provider START */
|
|
/* Provider settings */
|
|
config["proxy-providers"] = {};
|
|
uci.foreach(uciconf, uciprov, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return null;
|
|
|
|
config["proxy-providers"][cfg['.name']] = {
|
|
type: cfg.type,
|
|
...(cfg.type === 'inline' ? {
|
|
"dialer-proxy": dialerproxy[cfg['.name']]?.detour,
|
|
payload: trim(cfg.payload)
|
|
} : {
|
|
path: HM_DIR + '/provider/' + cfg['.name'],
|
|
url: cfg.url,
|
|
"size-limit": bytesizeToByte(cfg.size_limit) || null,
|
|
interval: (cfg.type === 'http') ? durationToSecond(cfg.interval) ?? 86400 : null,
|
|
proxy: get_proxy(cfg.proxy),
|
|
"age-secret-key": cfg.age_private_key,
|
|
header: cfg.header ? json(cfg.header) : null,
|
|
/* Health fields */
|
|
"health-check": cfg.health_enable === '0' ? {enable: false} : {
|
|
enable: true,
|
|
url: cfg.health_url,
|
|
interval: durationToSecond(cfg.health_interval) ?? 600,
|
|
timeout: strToInt(cfg.health_timeout) || 5000,
|
|
lazy: (cfg.health_lazy === '0') ? false : null,
|
|
"expected-status": cfg.health_expected_status || '204'
|
|
},
|
|
/* Override fields */
|
|
override: {
|
|
"additional-prefix": cfg.override_prefix,
|
|
"additional-suffix": cfg.override_suffix,
|
|
"proxy-name": isEmpty(cfg.override_replace) ? null : map(cfg.override_replace, obj => json(obj)),
|
|
// Other configuration items
|
|
tfo: strToBool(cfg.override_tfo),
|
|
mptcp: strToBool(cfg.override_mptcp),
|
|
udp: (cfg.override_udp === '0') ? null : true,
|
|
"udp-over-tcp": strToBool(cfg.override_uot),
|
|
up: cfg.override_up ? cfg.override_up + ' Mbps' : null,
|
|
down: cfg.override_down ? cfg.override_down + ' Mbps' : null,
|
|
"name-cert-verify": cfg.override_name_cert_verify,
|
|
"skip-cert-verify": cfg.override_skip_cert_verify ? strToBool(cfg.override_skip_cert_verify) || false : null,
|
|
"dialer-proxy": dialerproxy[cfg['.name']]?.detour,
|
|
"interface-name": cfg.override_interface_name,
|
|
"routing-mark": strToInt(cfg.override_routing_mark) || null,
|
|
"ip-version": cfg.override_ip_version,
|
|
"override-expr": cfg.override_expr
|
|
},
|
|
/* General fields */
|
|
filter: parse_filter(cfg.filter),
|
|
"exclude-filter": parse_filter(cfg.exclude_filter),
|
|
"exclude-type": parse_filter(cfg.exclude_type)
|
|
})
|
|
};
|
|
});
|
|
/* Provider END */
|
|
|
|
/* Rule set START */
|
|
/* Rule set settings */
|
|
config["rule-providers"] = {};
|
|
uci.foreach(uciconf, ucirule, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return null;
|
|
|
|
config["rule-providers"][cfg['.name']] = {
|
|
type: cfg.type,
|
|
format: cfg.format,
|
|
behavior: cfg.behavior,
|
|
...(cfg.type === 'inline' ? {
|
|
payload: trim(cfg.payload)
|
|
} : {
|
|
path: HM_DIR + '/ruleset/' + cfg['.name'],
|
|
url: cfg.url,
|
|
"path-in-bundle": cfg.path_in_bundle,
|
|
"size-limit": bytesizeToByte(cfg.size_limit) || null,
|
|
interval: (cfg.type === 'http') ? durationToSecond(cfg.interval) ?? 259200 : null,
|
|
proxy: get_proxy(cfg.proxy),
|
|
header: cfg.header ? json(cfg.header) : null
|
|
})
|
|
};
|
|
});
|
|
/* Rule set END */
|
|
|
|
/* Routing rules START */
|
|
/* Routing rules */
|
|
config.rules = [
|
|
"IN-NAME,dns-in,dns-out", // @Not required for v1.19.2+
|
|
"DST-PORT,53,dns-out"
|
|
];
|
|
uci.foreach(uciconf, ucirout, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return null;
|
|
|
|
push(config.rules, parse_entry(cfg.entry));
|
|
});
|
|
/* Routing rules END */
|
|
|
|
/* Sub rules START */
|
|
/* Sub rules */
|
|
config["sub-rules"] = {};
|
|
uci.foreach(uciconf, ucisubro, (cfg) => {
|
|
if (cfg.enabled === '0')
|
|
return null;
|
|
|
|
if (!config["sub-rules"][cfg.group])
|
|
config["sub-rules"][cfg.group] = [];
|
|
push(config["sub-rules"][cfg.group], parse_entry(cfg.entry));
|
|
});
|
|
/* Sub rules END */
|
|
|
|
printf('%.J\n', removeBlankAttrs(config));
|