/* * SPDX-License-Identifier: GPL-2.0-only * * Copyright (C) 2022-2025 ImmortalWrt.org */ 'use strict'; 'require baseclass'; 'require form'; 'require uci'; 'require homeproxy-pro as hp'; /* Build the shared body of a routing/dns rule section. The two sections * differ in their tab/option names, the sniffed-protocol value list, the * order of the protocol/network/query-type triplet, a handful of * descriptions and their action-specific field sets; everything else is * emitted identically. */ function renderRuleSection(s, kind, self) { let o, ss, so; let is_dns = (kind === 'dns'); let uci_type = is_dns ? 'dns_rule' : 'routing_rule'; s.tab(uci_type, is_dns ? _('DNS Rules') : _('Routing Rules')); o = s.taboption(uci_type, form.SectionValue, '_' + uci_type, form.GridSection, uci_type); o.depends('routing_mode', 'custom'); ss = o.subsection; ss.addremove = true; ss.rowcolors = true; ss.sortable = true; ss.nodescriptions = true; ss.modaltitle = L.bind(hp.loadModalTitle, hp, is_dns ? _('DNS rule') : _('Routing rule'), is_dns ? _('Add a DNS rule') : _('Add a routing rule'), 'homeproxy-pro'); ss.sectiontitle = L.bind(hp.loadDefaultLabel, self, 'homeproxy-pro'); /* Plain wrapper, not L.bind(): see hp.renderSectionAdd() for why the bound * form hands a call site's button factory to the wrong argument. */ ss.renderSectionAdd = function(extra_class) { return hp.renderSectionAdd(ss, extra_class); }; ss.tab('field_other', _('Other fields')); ss.tab('field_host', _('Host/IP fields')); ss.tab('field_port', _('Port fields')); ss.tab('fields_process', _('Process fields')); so = ss.taboption('field_other', form.Value, 'label', _('Label')); so.load = L.bind(hp.loadDefaultLabel, self, 'homeproxy-pro'); so.validate = L.bind(hp.validateUniqueValue, self, 'homeproxy-pro', uci_type, 'label'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'enabled', _('Enable')); so.default = so.enabled; so.rmempty = false; so.editable = true; so = ss.taboption('field_other', form.ListValue, 'mode', _('Mode'), is_dns ? _('The default rule uses the following matching logic:
' + '(domain || domain_suffix || domain_keyword || domain_regex) &&
' + '(port || port_range) &&
' + '(source_ip_cidr || source_ip_is_private) &&
' + '(source_port || source_port_range) &&
' + 'other fields.
' + 'Additionally, included rule sets can be considered merged rather than as a single rule sub-item.') : _('The default rule uses the following matching logic:
' + '(domain || domain_suffix || domain_keyword || domain_regex || ip_cidr || ip_is_private) &&
' + '(port || port_range) &&
' + '(source_ip_cidr || source_ip_is_private) &&
' + '(source_port || source_port_range) &&
' + 'other fields.
' + 'Additionally, included rule sets can be considered merged rather than as a single rule sub-item.')); so.value('default', _('Default')); so.default = 'default'; so.rmempty = false; so.readonly = true; if (is_dns) so.modalonly = true; so = ss.taboption('field_other', form.ListValue, 'ip_version', _('IP version'), is_dns ? null : _('4 or 6. Not limited if empty.')); so.value('4', _('IPv4')); so.value('6', _('IPv6')); so.value('', _('Both')); so.modalonly = true; function addSniffedProtocol() { let opt = ss.taboption('field_other', form.MultiValue, 'protocol', _('Protocol'), _('Sniffed protocol, see Sniff for details.')); opt.value('bittorrent', _('BitTorrent')); if (!is_dns) opt.value('dns', _('DNS')); opt.value('dtls', _('DTLS')); opt.value('http', _('HTTP')); opt.value('quic', _('QUIC')); opt.value('rdp', _('RDP')); opt.value('ssh', _('SSH')); opt.value('stun', _('STUN')); opt.value('tls', _('TLS')); return opt; } function addNetwork() { let opt = ss.taboption('field_other', form.ListValue, 'network', _('Network')); opt.value('tcp', _('TCP')); opt.value('udp', _('UDP')); opt.value('', _('Both')); return opt; } if (is_dns) { so = ss.taboption('field_other', form.DynamicList, 'query_type', _('Query type'), _('Match query type.')); so.modalonly = true; so = addNetwork(); so = addSniffedProtocol(); } else { so = addSniffedProtocol(); so = ss.taboption('field_other', form.Value, 'client', _('Client'), _('Sniffed client type (QUIC client type or SSH client name).')); so.value('chromium', _('Chromium / Cronet')); so.value('firefox', _('Firefox / uquic firefox')); so.value('quic-go', _('quic-go / uquic chrome')); so.value('safari', _('Safari / Apple Network API')); so.depends('protocol', 'quic'); so.depends('protocol', 'ssh'); so.modalonly = true; so = addNetwork(); } so = ss.taboption('field_other', form.DynamicList, 'user', _('User'), _('Match user name.')); so.modalonly = true; so = ss.taboption('field_other', hp.CBIStaticList, 'rule_set', _('Rule set'), is_dns ? _('Match rule set. DNS: rules referencing rule-sets that contain query_type are incompatible with legacy address filters (ip_cidr / ip_is_private) — use match_response instead (1.14).') : _('Match rule set. Since 1.14: a rule-set holding a single default rule is merged into this rule; any other rule-set matches as an OR collection.')); so.load = function(section_id) { delete this.keylist; delete this.vallist; uci.sections('homeproxy-pro', 'ruleset', (res) => { if (res.enabled === '1') this.value(res['.name'], res.label); }); return this.super('load', section_id); } so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'rule_set_ip_cidr_match_source', _('Rule set IP CIDR as source IP'), is_dns ? _('Make IP CIDR in rule sets match the source IP.') : _('Make IP CIDR in rule set used to match the source IP.')); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'invert', _('Invert'), _('Invert match result.')); so.modalonly = true; /* Action options are split by which schema they belong to: * dns_rule actions: route, reject, predefined, evaluate (1.14 only) * routing_rule actions: route, route-options, reject, resolve, hijack/sniff * (sniff/hijack are emitted by the proxy-mode builder, * not exposed in the form). * * `route-options` is a routing_rule-only action: sing-box 1.14's dns_rule * schema has no such action, and emitting it would make the whole dns block * fail with "unknown action". Earlier code added it to both branches and * the depends() guards for dns_disable_cache / rewrite_ttl / client_subnet * pinned a stale 'route-options' in the UCI that resurfaced as "unknown * field" the next time the user flipped back to 'route'. */ so = ss.taboption('field_other', form.ListValue, 'action', _('Action')); so.value('route', _('Route')); so.value('reject', _('Reject')); if (is_dns) { so.value('predefined', _('Predefined')); so.value('evaluate', _('Evaluate (1.14)')); } else { so.value('route-options', _('Route options')); so.value('resolve', _('Resolve')); } so.default = 'route'; so.rmempty = false; so.editable = true; if (is_dns) { so = ss.taboption('field_other', form.ListValue, 'server', _('Server'), _('Tag of the target dns server.')); so.load = function(section_id) { delete this.keylist; delete this.vallist; this.value('default-dns', _('Default DNS (issued by WAN)')); this.value('system-dns', _('System DNS')); uci.sections('homeproxy-pro', 'dns_server', (res) => { if (res.enabled === '1') this.value(res['.name'], res.label); }); return this.super('load', section_id); } so.rmempty = false; so.editable = true; so.depends('action', 'route'); so.depends('action', 'evaluate'); so = ss.taboption('field_other', form.Flag, 'dns_disable_cache', _('Disable dns cache'), _('Disable cache and save cache in this query.')); so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'rewrite_ttl', _('Rewrite TTL'), _('Rewrite TTL in DNS responses.')); so.datatype = 'uinteger'; so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'client_subnet', _('EDNS Client subnet'), _('Append a edns0-subnet OPT extra record with the specified IP prefix to every query by default.
' + 'If value is an IP address instead of prefix, /32 or /128 will be appended automatically.')); so.datatype = 'or(cidr, ipaddr)'; so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'match_response', _('Match response'), _('1 matches the latest untagged evaluate result; other values match an evaluate tag (1.14).')); so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'evaluate_tag', _('Evaluate tag'), _('Optional tag for this evaluate result.')); so.depends('action', 'evaluate'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'race', _('Race'), _('Judge this response-dependent rule in parallel; first match wins (1.14).')); so.depends({'action': 'route', 'match_response': /[\s\S]/}); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'speculative', _('Speculative'), _('Send the query before pending race rules are judged (1.14).')); so.depends('action', 'route'); so.depends('action', 'evaluate'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'disable_optimistic_cache', _('Disable optimistic cache'), _('Disable optimistic DNS caching for this query.')); so.depends('action', 'route'); so.depends('action', 'evaluate'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'dns_timeout', _('Query timeout'), _('Override dns.timeout for this query, in seconds.')); so.datatype = 'uinteger'; so.depends('action', 'route'); so.depends('action', 'evaluate'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'remove_client_subnet', _('Remove EDNS client subnet'), _('Remove the edns0-subnet record from the query (1.14).')); so.depends('action', 'route'); so.depends('action', 'evaluate'); so.modalonly = true; so = ss.taboption('field_other', form.ListValue, 'response_rcode', _('Response RCode'), _('Match the evaluated response code (requires match_response).')); for (let rc of [ 'NOERROR', 'FORMERR', 'SERVFAIL', 'NXDOMAIN', 'NOTIMP', 'REFUSED' ]) so.value(rc); so.depends({'action': 'route', 'match_response': /[\s\S]/}); so.modalonly = true; so = ss.taboption('field_other', form.DynamicList, 'response_answer', _('Response answer'), _('Text DNS records to match in the evaluated answer.')); so.depends({'action': 'route', 'match_response': /[\s\S]/}); so.modalonly = true; so = ss.taboption('field_other', form.DynamicList, 'response_ns', _('Response NS')); so.depends({'action': 'route', 'match_response': /[\s\S]/}); so.modalonly = true; so = ss.taboption('field_other', form.DynamicList, 'response_extra', _('Response extra')); so.depends({'action': 'route', 'match_response': /[\s\S]/}); so.modalonly = true; so = ss.taboption('field_other', form.ListValue, 'reject_method', _('Method')); so.value('default', _('Reply with REFUSED')); so.value('drop', _('Drop requests')); so.default = 'default'; so.depends('action', 'reject'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'reject_no_drop', _('Don\'t drop requests'), _('%s will be temporarily overwritten to %s after 50 triggers in 30s if not enabled.').format( _('Method'), _('Drop requests'))); so.depends('reject_method', 'default'); so.modalonly = true; so = ss.taboption('field_other', form.ListValue, 'predefined_rcode', _('RCode'), _('The response code.')); so.value('NOERROR'); so.value('FORMERR'); so.value('SERVFAIL'); so.value('NXDOMAIN'); so.value('NOTIMP'); so.value('REFUSED'); so.default = 'NOERROR'; so.depends('action', 'predefined'); so.modalonly = true; so = ss.taboption('field_other', form.DynamicList, 'predefined_answer', _('Answer'), _('List of text DNS record to respond as answers.')); so.depends('action', 'predefined'); so.modalonly = true; so = ss.taboption('field_other', form.DynamicList, 'predefined_ns', _('NS'), _('List of text DNS record to respond as name servers.')); so.depends('action', 'predefined'); so.modalonly = true; so = ss.taboption('field_other', form.DynamicList, 'predefined_extra', _('Extra records'), _('List of text DNS record to respond as extra records.')); so.depends('action', 'predefined'); so.modalonly = true; } else { so = ss.taboption('field_other', form.ListValue, 'outbound', _('Outbound'), _('Tag of the target outbound.')); so.load = function(section_id) { delete this.keylist; delete this.vallist; this.value('direct-out', _('Direct')); uci.sections('homeproxy-pro', 'routing_node', (res) => { if (res.enabled === '1') this.value(res['.name'], res.label); }); return this.super('load', section_id); } so.rmempty = false; so.depends('action', 'route'); so.editable = true; so = ss.taboption('field_other', form.Value, 'override_address', _('Override address'), _('Override the connection destination address.')); so.datatype = 'ipaddr'; so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'override_port', _('Override port'), _('Override the connection destination port.')); so.datatype = 'port'; so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'udp_disable_domain_unmapping', _('Disable UDP domain unmapping'), _('If enabled, for UDP proxy requests addressed to a domain, the original packet address will be sent in the response instead of the mapped domain.')); so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'udp_connect', _('connect UDP connections'), _('If enabled, attempts to connect UDP connection to the destination instead of listen.')); so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'udp_timeout', _('UDP timeout'), _('Timeout for UDP connections.
Setting a larger value than the UDP timeout in inbounds will have no effect.')); so.datatype = 'uinteger'; so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'tls_record_fragment', _('TLS record fragment'), _('Fragment TLS handshake into multiple TLS records.')); so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'tls_fragment', _('TLS fragment'), _('Fragment TLS handshakes. Due to poor performance, try %s first.').format( _('TLS record fragment'))); so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'tls_fragment_fallback_delay', _('Fragment fallback delay'), _('The fallback value in milliseconds used when TLS segmentation cannot automatically determine the wait time.')); so.datatype = 'uinteger'; so.placeholder = '500'; so.depends('tls_fragment', '1'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'tls_spoof', _('TLS spoof SNI (1.14)'), _('Inject a forged TLS ClientHello carrying this SNI before the real one to fool SNI-filtering middleboxes. Requires elevated privileges.')); so.datatype = 'hostname'; so.depends('action', 'route'); so.modalonly = true; so = ss.taboption('field_other', form.ListValue, 'tls_spoof_method', _('TLS spoof method (1.14)'), _('How the forged segment is rejected by the real server.')); so.value('', _('wrong-sequence (default)')); so.value('wrong-checksum', _('wrong-checksum')); so.value('wrong-ack', _('wrong-ack')); so.value('wrong-md5', _('wrong-md5')); so.value('wrong-timestamp', _('wrong-timestamp')); so.depends('action', 'route'); so.depends('tls_spoof', /[\s\S]/); so.modalonly = true; so = ss.taboption('field_other', form.ListValue, 'resolve_server', _('DNS server'), _('Specifies DNS server tag to use instead of selecting through DNS routing.')); so.load = function(section_id) { delete this.keylist; delete this.vallist; this.value('', _('Default')); this.value('default-dns', _('Default DNS (issued by WAN)')); this.value('system-dns', _('System DNS')); uci.sections('homeproxy-pro', 'dns_server', (res) => { if (res.enabled === '1') this.value(res['.name'], res.label); }); return this.super('load', section_id); } so.depends('action', 'resolve'); so.modalonly = true; so = ss.taboption('field_other', form.ListValue, 'reject_method', _('Method')); so.value('default', _('Reply with TCP RST / ICMP port unreachable')); so.value('drop', _('Drop packets')); so.depends('action', 'reject'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'reject_no_drop', _('Don\'t drop packets'), _('%s will be temporarily overwritten to %s after 50 triggers in 30s if not enabled.').format( _('Method'), _('Drop packets'))); so.depends('reject_method', 'default'); so.modalonly = true; so = ss.taboption('field_other', form.ListValue, 'resolve_strategy', _('Resolve strategy'), _('Domain strategy for resolving the domain names.')); for (let i in hp.dns_strategy) so.value(i, hp.dns_strategy[i]); so.depends('action', 'resolve'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'resolve_disable_cache', _('Disable DNS cache'), _('Disable DNS cache in this query.')); so.depends('action', 'resolve'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'resolve_rewrite_ttl', _('Rewrite TTL'), _('Rewrite TTL in DNS responses.')); so.datatype = 'uinteger'; so.depends('action', 'resolve'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'resolve_client_subnet', _('EDNS Client subnet'), _('Append a edns0-subnet OPT extra record with the specified IP prefix to every query by default.
' + 'If value is an IP address instead of prefix, /32 or /128 will be appended automatically.')); so.datatype = 'or(cidr, ipaddr)'; so.depends('action', 'resolve'); so.modalonly = true; so = ss.taboption('field_other', form.Flag, 'resolve_disable_optimistic_cache', _('Disable optimistic cache'), _('Disable optimistic DNS caching in this lookup (1.14).')); so.depends('action', 'resolve'); so.modalonly = true; so = ss.taboption('field_other', form.Value, 'resolve_timeout', _('Query timeout'), _('Override dns.timeout for this lookup, in seconds (1.14).')); so.datatype = 'uinteger'; so.depends('action', 'resolve'); so.modalonly = true; } so = ss.taboption('field_host', form.DynamicList, 'domain', _('Domain name'), _('Match full domain.')); so.datatype = 'hostname'; so.modalonly = true; so = ss.taboption('field_host', form.DynamicList, 'domain_suffix', _('Domain suffix'), _('Match domain suffix.')); so.modalonly = true; so = ss.taboption('field_host', form.DynamicList, 'domain_keyword', _('Domain keyword'), _('Match domain using keyword.')); so.modalonly = true; so = ss.taboption('field_host', form.DynamicList, 'domain_regex', _('Domain regex'), _('Match domain using regular expression.')); so.modalonly = true; so = ss.taboption('field_host', form.DynamicList, 'source_ip_cidr', _('Source IP CIDR'), _('Match source IP CIDR.')); so.datatype = 'or(cidr, ipaddr)'; so.modalonly = true; so = ss.taboption('field_host', form.Flag, 'source_ip_is_private', _('Match private source IP')); so.modalonly = true; so = ss.taboption('field_host', form.DynamicList, 'ip_cidr', _('IP CIDR'), is_dns ? _('Match IP CIDR with the evaluated response (needs match_response; old rules are auto-wrapped in 1.14).') : _('Match IP CIDR.')); so.datatype = 'or(cidr, ipaddr)'; so.modalonly = true; so = ss.taboption('field_host', form.Flag, 'ip_is_private', _('Match private IP'), is_dns ? _('Match private IP with the evaluated response (needs match_response).') : null); so.modalonly = true; so = ss.taboption('field_host', form.DynamicList, 'source_mac_address', _('Source MAC address'), _('Match LAN device by MAC address.')); so.datatype = 'macaddr'; so.modalonly = true; so = ss.taboption('field_host', form.DynamicList, 'source_hostname', _('Source hostname'), _('Match LAN device hostname via neighbor resolution; enable find_neighbor.')); so.modalonly = true; if (is_dns) { so = ss.taboption('field_host', form.Value, 'query_client_subnet', _('Query EDNS client subnet'), _('Match the EDNS Client Subnet in the query (1.14).')); so.datatype = 'or(cidr, ipaddr)'; so.modalonly = true; so = ss.taboption('field_host', form.Flag, 'query_dnssec', _('Match DNSSEC OK'), _('Match queries with the DNSSEC OK bit set (1.14).')); so.modalonly = true; } so = ss.taboption('field_port', form.DynamicList, 'source_port', _('Source port'), _('Match source port.')); so.datatype = 'port'; so.modalonly = true; so = ss.taboption('field_port', form.DynamicList, 'source_port_range', _('Source port range'), _('Match source port range. Format as START:/:END/START:END.')); so.validate = hp.validatePortRange; so.modalonly = true; so = ss.taboption('field_port', form.DynamicList, 'port', _('Port'), _('Match port.')); so.datatype = 'port'; so.modalonly = true; so = ss.taboption('field_port', form.DynamicList, 'port_range', _('Port range'), _('Match port range. Format as START:/:END/START:END.')); so.validate = hp.validatePortRange; so.modalonly = true; so = ss.taboption('fields_process', form.DynamicList, 'process_name', _('Process name'), _('Match process name.')); so.modalonly = true; so = ss.taboption('fields_process', form.DynamicList, 'process_path', _('Process path'), _('Match process path.')); so.modalonly = true; so = ss.taboption('fields_process', form.DynamicList, 'process_path_regex', _('Process path (regex)'), _('Match process path using regular expression.')); so.modalonly = true; } return baseclass.extend({ renderRuleSection });