#!/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, 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', ucipgrp = 'proxy_group', ucinode = 'node', uciprov = 'provider', ucichain = 'dialer_proxy', ucirule = 'ruleset', ucirout = 'rules', ucisubro = 'subrules'; /* Hardcode options */ const common_tcpport = uci.get(uciconf, ucifchm, 'common_tcpport') || '20-21,22,53,80,110,143,443,465,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', stun_port = uci.get(uciconf, ucifchm, 'stun_port') || '3478,19302', turn_port = uci.get(uciconf, ucifchm, 'turn_port') || '5349', 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, client_enabled = uci.get(uciconf, uciroute, 'client_enabled') || '0', 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_proxynode(cfg) { if (isEmpty(cfg)) return null; const label = uci.get(uciconf, cfg, 'label'); if (isEmpty(label)) die(sprintf("%s's label is missing, please check your configuration.", cfg)); else return label; } function get_proxygroup(cfg) { if (isEmpty(cfg)) return null; if (cfg in PRESET_OUTBOUND) return cfg; const label = uci.get(uciconf, cfg, 'label'); if (isEmpty(label)) 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://name_error'; 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_proxygroup(detour || c1)); })); } return servers; } function parse_entry(cfg) { if (isEmpty(cfg)) return null; let rule = json(cfg); if (rule.detour) rule.detour = get_proxygroup(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_proxygroup(cfg.chain_tail_group) || get_proxynode(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["global-client-fingerprint"] = uci.get(uciconf, ucitls, 'global_client_fingerprint'); config.tls = { "certificate": uci.get(uciconf, ucitls, 'tls_cert_path'), "private-key": uci.get(uciconf, ucitls, 'tls_key_path') }; /* 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'); /* 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"] = 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.listeners = []; push(config.listeners, { name: 'mixed-in', type: 'mixed', port: strToInt(uci.get(uciconf, uciinbound, 'mixed_port')) || 7890, listen: '::', udp: true }); if (match(proxy_mode, /redir/)) push(config.listeners, { name: 'redir-in', type: 'redir', port: strToInt(uci.get(uciconf, uciinbound, 'redir_port')) || 7891, listen: '::' }); 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 }); 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+ /* Tun settings */ if (match(proxy_mode, /tun/)) push(config.listeners, { name: 'tun-in', type: 'tun', 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')), "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'), 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')), 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 */ uci.foreach(uciconf, ucidnspoli, (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["nameserver-policy"][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') || [], }; /* 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, /* HTTP / SOCKS / Shadowsocks / VMess / VLESS / Trojan / hysteria2 / TUIC / SSH / WireGuard */ 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, "private-key": cfg.wireguard_private_key || cfg.ssh_priv_key, /* Hysteria / Hysteria2 */ ports: isEmpty(cfg.hysteria_ports) ? null : join(',', cfg.hysteria_ports), up: cfg.hysteria_up_mbps ? cfg.hysteria_up_mbps + ' Mbps' : null, down: cfg.hysteria_down_mbps ? cfg.hysteria_down_mbps + ' Mbps' : null, obfs: cfg.hysteria_obfs_type, "obfs-password": cfg.hysteria_obfs_password, /* SSH */ "private-key-passphrase": cfg.ssh_priv_key_passphrase, "host-key-algorithms": cfg.ssh_host_key_algorithms, "host-key": cfg.ssh_host_key, /* Shadowsocks */ /* Mieru */ "port-range": cfg.mieru_port_range, transport: cfg.mieru_transport, multiplexing: cfg.mieru_multiplexing, /* Snell */ psk: cfg.snell_psk, version: cfg.snell_version, "obfs-opts": cfg.type === 'snell' ? { mode: cfg.plugin_opts_obfsmode, host: cfg.plugin_opts_host, } : null, /* TUIC */ ip: cfg.tuic_ip, "congestion-controller": cfg.tuic_congestion_controller, "udp-relay-mode": cfg.tuic_udp_relay_mode, "udp-over-stream": strToBool(cfg.tuic_udp_over_stream), "udp-over-stream-version": cfg.tuic_udp_over_stream_version, "max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size) || null, "reduce-rtt": strToBool(cfg.tuic_reduce_rtt), "heartbeat-interval": strToInt(cfg.tuic_heartbeat) || null, "request-timeout": strToInt(cfg.tuic_request_timeout) || null, // fast-open: true // max-open-streams: 20 /* Trojan */ "ss-opts": cfg.trojan_ss_enabled === '1' ? { enabled: true, method: cfg.trojan_ss_chipher, password: cfg.trojan_ss_password }: null, /* 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, /* WireGuard */ ip: cfg.wireguard_ip, ipv6: cfg.wireguard_ipv6, "public-key": cfg.wireguard_peer_public_key, "pre-shared-key": cfg.wireguard_pre_shared_key, "allowed-ips": cfg.wireguard_allowed_ips, reserved: cfg.wireguard_reserved, mtu: strToInt(cfg.wireguard_mtu) || null, "remote-dns-resolve": strToBool(cfg.wireguard_remote_dns_resolve), dns: cfg.wireguard_dns, /* Plugin fields */ plugin: cfg.plugin, "plugin-opts": cfg.plugin ? { mode: cfg.plugin_opts_obfsmode, host: cfg.plugin_opts_host, password: cfg.plugin_opts_thetlspassword, version: cfg.plugin_opts_shadowtls_version, "version-hint": cfg.plugin_opts_restls_versionhint, "restls-script": cfg.plugin_opts_restls_script } : null, /* Extra fields */ udp: strToBool(cfg.udp), "udp-over-tcp": strToBool(cfg.uot), "udp-over-tcp-version": cfg.uot_version, /* TLS fields */ tls: (cfg.type in ['trojan', 'hysteria', 'hysteria2', 'tuic']) ? 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, alpn: cfg.tls_alpn, // Array "skip-cert-verify": strToBool(cfg.tls_skip_cert_verify), "client-fingerprint": cfg.tls_client_fingerprint, "reality-opts": cfg.tls_reality === '1' ? { "public-key": cfg.tls_reality_public_key, "short-id": cfg.tls_reality_short_id } : 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 } : 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 } : {}), /* Multiplex fields */ 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_proxygroup(cfg)), ...map(cfg.proxies || [], cfg => get_proxynode(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), // 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, filter: parse_filter(cfg.filter), "exclude-filter": parse_filter(cfg.exclude_filter), "exclude-type": parse_filter(cfg.exclude_type) }); }); /* Proxy Group END */ /* Provider START */ /* Provider settings */ config["proxy-providers"] = {}; uci.foreach(uciconf, uciprov, (cfg) => { if (cfg.enabled === '0') return null; /* General fields */ config["proxy-providers"][cfg['.name']] = { type: cfg.type, ...(cfg.payload ? { 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_proxygroup(cfg.proxy), header: cfg.header ? json(cfg.header) : null, "health-check": {}, override: {}, filter: parse_filter(cfg.filter), "exclude-filter": parse_filter(cfg.exclude_filter), "exclude-type": parse_filter(cfg.exclude_type) }; /* Override fields */ config["proxy-providers"][cfg['.name']].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)), // Configuration Items tfo: strToBool(cfg.override_tfo), mptcp: strToBool(cfg.override_mptcp), udp: (cfg.override_udp === '0') ? false : 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, "skip-cert-verify": strToBool(cfg.override_skip_cert_verify) || false, "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 }; /* Health fields */ if (cfg.health_enable === '0') { config["proxy-providers"][cfg['.name']]["health-check"] = null; } else { config["proxy-providers"][cfg['.name']]["health-check"] = { 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' }; } }); /* 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.payload ? { payload: trim(cfg.payload) } : { path: HM_DIR + '/ruleset/' + cfg['.name'] }), url: cfg.url, "size-limit": bytesizeToByte(cfg.size_limit) || null, interval: (cfg.type === 'http') ? durationToSecond(cfg.interval) ?? 259200 : null, proxy: get_proxygroup(cfg.proxy) }; }); /* 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 */ /* Debug dialer-proxy */ //config.dialerproxy = dialerproxy; printf('%.J\n', removeBlankAttrs(config));