#!/usr/bin/env ucode let common = require("core.common"); let connections = require("config.connections"); let runtime_constants = require("singbox.constants"); let option = common.option; let bool_option = common.bool_option; let as_string = common.as_string; let bool_value = common.bool_value; function normalize_port_entry(value) { value = trim(as_string(value)); if (value == "" || match(value, /^[0-9]+$/) == null) return null; let number = int(value); return number >= 1 && number <= 65535 ? number : null; } // P2P leak protection can be pinned down to the torrent client's own ports so // nothing sneaks past the protocol sniffer. Each entry is "proto" or // "proto:port" or "proto:port-port" (tcp/udp, comma separated). Without a // port the whole protocol is routed direct. function p2p_direct_rules(settings) { let rules = []; for (let entry in split(option(settings, "p2p_ports", ""), ",")) { entry = trim(entry); if (entry == "") continue; let proto = "tcp"; let port_spec = ""; let colon = index(entry, ":"); if (colon >= 0) { proto = trim(substr(entry, 0, colon)); port_spec = trim(substr(entry, colon + 1)); } else { proto = entry; } if (proto != "tcp" && proto != "udp") continue; let rule = { protocol: [ proto ], action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG }; if (port_spec != "") { let dash = index(port_spec, "-"); if (dash >= 0) { let start = normalize_port_entry(substr(port_spec, 0, dash)); let end = normalize_port_entry(substr(port_spec, dash + 1)); if (start != null && end != null && start <= end) rule.source_port_range = [ sprintf("%d:%d", start, end) ]; else continue; } else { let port = normalize_port_entry(port_spec); if (port != null) rule.source_port = port; else continue; } } push(rules, rule); } return rules; } function config(settings, runtime) { let output_network_interface = option(settings, "output_network_interface", ""); let mwan3_active = type(runtime) == "object" && bool_value(runtime.mwan3_active); let sniff_inbounds = [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.DNS_INBOUND_TAG ]; if (type(runtime) == "object" && bool_value(runtime.source_aware_dns)) push(sniff_inbounds, runtime_constants.SOURCE_DNS_INBOUND_TAG); if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array") for (let inbound in runtime.dns_health_inbounds) push(sniff_inbounds, inbound); let rules = [ { action: "sniff", inbound: sniff_inbounds }, { action: "hijack-dns", port: 53 }, { action: "hijack-dns", protocol: "dns" } ]; if (type(runtime) == "object" && bool_value(runtime.source_aware_dns)) push(rules, { action: "hijack-dns", inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ] }); let result = { rules, rule_set: [], final: runtime_constants.DIRECT_OUTBOUND_TAG, auto_detect_interface: output_network_interface == "" && !mwan3_active, default_domain_resolver: type(runtime) == "object" && as_string(runtime.default_domain_resolver) != "" ? as_string(runtime.default_domain_resolver) : runtime_constants.DNS_SERVER_TAG, default_mark: runtime_constants.OUTBOUND_MARK }; if (output_network_interface != "") result.default_interface = output_network_interface; if (bool_option(settings, "disable_quic", false)) push(result.rules, { action: "reject", inbound: runtime_constants.TPROXY_INBOUND_TAG, protocol: "quic" }); if (bool_option(settings, "isolate_p2p", false)) { push(result.rules, { protocol: "bittorrent", action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG }); for (let rule in p2p_direct_rules(settings)) push(result.rules, rule); } return result; } function target(section, outbound_tag_name) { let action = option(section, "action", ""); if (connections.is_connections_action(action) || action == "awg" || action == "warp" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "masque" || action == "openvpn" || action == "byedpi" || action == "zapret" || action == "zapret2") return { action: "route", outbound: outbound_tag_name }; if (action == "bypass") return { action: "route", outbound: runtime_constants.BYPASS_OUTBOUND_TAG }; if (action == "block") return { action: "reject" }; if (action == "hosts" || action == "dns") return { action: "route", outbound: runtime_constants.DIRECT_OUTBOUND_TAG }; return { unsupported: "unsupported action " + action }; } function has_resolve_matchers(rule) { return rule.domain != null || rule.domain_suffix != null || rule.domain_keyword != null || rule.domain_regex != null || rule.rule_set != null; } function resolve_rule_for_section(section, route_rule) { let action = option(section, "action", ""); let should_resolve = action == "byedpi" || (connections.is_connections_action(action) && bool_option(section, "resolve_real_ip_for_routing", false)); if (!should_resolve) return null; if (!has_resolve_matchers(route_rule)) { return { warning: "Resolve real IP is enabled for '" + section[".name"] + "', but no domain or rule-set matchers found" }; } let resolve_rule = {}; for (let key in [ "inbound", "source_ip_cidr", "domain", "domain_suffix", "domain_keyword", "domain_regex", "rule_set", "port", "port_range" ]) { if (route_rule[key] != null) resolve_rule[key] = route_rule[key]; } resolve_rule.action = "resolve"; resolve_rule.server = runtime_constants.DNS_SERVER_TAG; return { rule: resolve_rule }; } return { config, target, has_resolve_matchers, resolve_rule_for_section };