module("luci.passwall.util_sing-box", package.seeall) local api = require "luci.passwall.api" local uci = api.uci local sys = api.sys local jsonc = api.jsonc local appname = "passwall" local fs = api.fs local split = api.split local ech_domain = {} local local_version = api.get_app_version("sing-box"):match("[^v]+") local version_ge_1_14_0 = api.compare_versions(local_version, ">=", "1.14.0") local GLOBAL = { DNS_SERVER = {}, DNS_HOSTNAME = {}, VPS_EXCLUDE = {} } local GEO_VAR = { OK = nil, DIR = nil, SITE_PATH = nil, IP_PATH = nil, SITE_TAGS = {}, IP_TAGS = {}, TO_SRS_PATH = "/tmp/etc/" .. appname .."_tmp/singbox_srss/" } function check_geoview() if not GEO_VAR.OK then -- Only get once GEO_VAR.OK = (api.finded_com("geoview") and api.compare_versions(api.get_app_version("geoview"), ">=", "0.1.10")) and 1 or 0 end if GEO_VAR.OK == 0 then api.log("!!!注意:缺少 Geoview 组件或版本过低,Sing-Box 分流无法启用!") else GEO_VAR.DIR = GEO_VAR.DIR or (uci:get(appname, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"):match("^(.*)/") GEO_VAR.SITE_PATH = GEO_VAR.SITE_PATH or (GEO_VAR.DIR .. "/geosite.dat") GEO_VAR.IP_PATH = GEO_VAR.IP_PATH or (GEO_VAR.DIR .. "/geoip.dat") if not fs.access(GEO_VAR.TO_SRS_PATH) then fs.mkdir(GEO_VAR.TO_SRS_PATH) end end return GEO_VAR.OK end function geo_convert_srs(var) if check_geoview() ~= 1 then return end local geo_path = var["geo_path"] local prefix = var["prefix"] local rule_name = var["rule_name"] local output_srs_file = GEO_VAR.TO_SRS_PATH .. prefix .. "-" .. rule_name .. ".srs" local bin = api.finded_com("geoview") if not fs.access(output_srs_file) and bin then local cmd = string.format("%q -type %q -action convert -input %q -list %q -output %q -lowmem=true", bin, prefix, geo_path, rule_name, output_srs_file) sys.call(cmd) local status = fs.access(output_srs_file) and "success." or "failed!" if status == "failed!" then api.log(string.format(" - %s:%s 转换为srs格式:%s", prefix, rule_name, status)) end end end local function convert_geofile() if check_geoview() ~= 1 then return end local function convert(file_path, prefix, tags) if next(tags) and fs.access(file_path) then local md5_file = GEO_VAR.TO_SRS_PATH .. prefix .. ".dat.md5" local new_md5 = sys.exec("md5sum " .. file_path .. " 2>/dev/null | awk '{print $1}'"):gsub("\n", "") local old_md5 = sys.exec("[ -f " .. md5_file .. " ] && head -n 1 " .. md5_file .. " | tr -d ' \t\n' || echo ''") if new_md5 ~= "" and new_md5 ~= old_md5 then sys.call("printf '%s' " .. new_md5 .. " > " .. md5_file) sys.call("rm -rf " .. GEO_VAR.TO_SRS_PATH .. prefix .. "-*.srs" ) end for k in pairs(tags) do geo_convert_srs({["geo_path"] = file_path, ["prefix"] = prefix, ["rule_name"] = k}) end end end --api.log("Sing-Box 规则集转换:") convert(GEO_VAR.SITE_PATH, "geosite", GEO_VAR.SITE_TAGS) convert(GEO_VAR.IP_PATH, "geoip", GEO_VAR.IP_TAGS) end function gen_outbound(flag, node, tag, proxy_table) local result = nil if node then local node_id = node[".name"] if tag == nil then tag = node_id end local remarks = node.remarks local proxy_tag = nil local fragment = nil local record_fragment = nil local run_socks_instance = true if proxy_table ~= nil and type(proxy_table) == "table" then proxy_tag = proxy_table.tag or nil fragment = (proxy_table.fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil record_fragment = (proxy_table.record_fragment and node.protocol ~= "naive" and not node.hysteria2_realms) and true or nil run_socks_instance = proxy_table.run_socks_instance end if node.type ~= "sing-box" then local relay_port = node.port local new_port = api.get_new_port() local config_file = string.format("%s_%s_%s.json", flag, tag, new_port) if tag and node_id and not tag:find(node_id) then config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port) end if run_socks_instance then sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null', appname, string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s", new_port, --flag node_id, --node "127.0.0.1", --bind new_port, --socks port config_file, --config file (proxy_tag and relay_port) and tostring(relay_port) or "" --relay port ) ) ) end node = { protocol = "socks", address = "127.0.0.1", port = new_port } proxy_tag = "socks <- " .. node_id else if proxy_tag then node.detour = proxy_tag end end if remarks then tag = tag .. ":" .. remarks end node.address = (node.address or ""):lower() result = { _id = node_id, _flag = flag, _flag_proxy_tag = proxy_tag, tag = tag, type = node.protocol, server = node.address, server_port = tonumber(node.port), domain_resolver = { server = "direct", strategy = node.domain_strategy }, detour = node.detour, } if api.datatypes.hostname(node.address) and node.domain_resolver and (node.domain_resolver_dns or node.domain_resolver_dns_https) then local dns_proto = node.domain_resolver local server_address local server_port local server_path if dns_proto == "https" then local _a = api.parseDoH(node.domain_resolver_dns_https) if _a then server_address = _a.hostname server_port = _a.port or 443 server_path = _a.pathname or "" if _a.hostname and api.datatypes.hostname(_a.hostname) then GLOBAL.DNS_HOSTNAME[_a.hostname] = true end end else server_address = node.domain_resolver_dns server_port = 53 local split = api.split(server_address, ":") if #split > 1 then server_address = split[1] server_port = tonumber(split[#split]) end end local dns_key = dns_proto .. "|" .. tostring(server_address) .. "|" .. tostring(server_port) .. "|" .. tostring(server_path or "") if not GLOBAL.DNS_SERVER[dns_key] then GLOBAL.DNS_SERVER[dns_key] = { server = { tag = "dns-node-" .. api.gen_short_uuid(), type = dns_proto, server = server_address, server_port = server_port, path = server_path, domain_resolver = "direct", detour = "direct" }, domain = {} } end local exists for _, d in ipairs(GLOBAL.DNS_SERVER[dns_key].domain) do if d == node.address then exists = true; break end end if not exists then table.insert(GLOBAL.DNS_SERVER[dns_key].domain, node.address) end result.domain_resolver.server = GLOBAL.DNS_SERVER[dns_key].server.tag GLOBAL.VPS_EXCLUDE[node.address] = true end local tls = nil if node.protocol == "hysteria" or node.protocol == "hysteria2" or node.protocol == "tuic" or node.protocol == "naive" then node.tls = "1" end if node.tls == "1" then local alpn = nil if node.alpn and node.alpn ~= "default" then alpn = {} string.gsub(node.alpn, '[^' .. "," .. ']+', function(w) table.insert(alpn, w) end) end tls = { enabled = true, disable_sni = (node.tls_disable_sni == "1") and true or false, --不要在 ClientHello 中发送服务器名称. server_name = node.tls_serverName, --用于验证返回证书上的主机名,除非设置不安全。它还包含在 ClientHello 中以支持虚拟主机,除非它是 IP 地址。 insecure = node.tls_allowInsecure == "1" or (node.tls_pinSHA256 and node.tls_pinSHA256 ~= ""), --接受任何服务器证书。(兼顾 xray 的 pinnedPeerCertSha256 ) alpn = alpn, --支持的应用层协议协商列表,按优先顺序排列。如果两个对等点都支持 ALPN,则选择的协议将是此列表中的一个,如果没有相互支持的协议则连接将失败。 --min_version = "1.2", --max_version = "1.3", fragment = fragment, record_fragment = record_fragment, certificate = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and split(node.tls_certificate_pem, "\n") or nil, ech = (node.ech == "1") and (function() local function get_ech_domain(s) --兼容xray "域名+DNS" 格式ech local domain, dns = s:match("^([^+]+)%+(.+)$") if not domain or not dns then return nil end if not (dns:match("^https?://") or dns:match("^tcp://") or dns:match("^udp://") or dns:match("^h2c://")) then return nil end if not domain:match("^[%w%-%.]+%.[%a]+$") then return nil end return domain end local ech = { enabled = true } local config = node.ech_config local qname = node.ech_query_server_name if config and not qname then qname = get_ech_domain(config) if not qname and not (config:match("%-+%s*BEGIN") and config:match("%-+%s*END")) then config = "-----BEGIN ECH CONFIGS-----\n" .. config:gsub("%s+", "") .. "\n-----END ECH CONFIGS-----" end end if qname then ech.query_server_name = qname ech_domain[qname] = true elseif config then ech.config = { config } elseif node.tls_serverName and node.tls_serverName ~= "" then ech_domain[node.tls_serverName] = true end return ech end)() or nil, utls = (node.utls == "1" or node.reality == "1") and { enabled = true, fingerprint = node.fingerprint or "chrome" } or nil, reality = (node.reality == "1") and { enabled = true, public_key = node.reality_publicKey, short_id = node.reality_shortId } or nil } end local mux = nil if node.mux == "1" then mux = { enabled = true, protocol = node.mux_type or "h2mux", max_connections = ( (node.tcpbrutal == "1") and 1 ) or tonumber(node.mux_concurrency) or 4, padding = (node.mux_padding == "1") and true or false, --min_streams = 4, --max_streams = 0, brutal = { enabled = (node.tcpbrutal == "1") and true or false, up_mbps = tonumber(node.tcpbrutal_up_mbps) or 10, down_mbps = tonumber(node.tcpbrutal_down_mbps) or 50, }, } end local v2ray_transport = nil if node.transport == "tcp" and node.tcp_guise == "http" and (node.tcp_guise_http_host or "") ~= "" then --模拟xray raw(tcp)传输 v2ray_transport = { type = "http", host = node.tcp_guise_http_host, path = node.tcp_guise_http_path and (function() local first = node.tcp_guise_http_path[1] return (first == "" or not first) and "/" or first end)() or "/", headers = node.user_agent and { ["User-Agent"] = node.user_agent } or nil, idle_timeout = (node.http_h2_health_check == "1") and node.http_h2_read_idle_timeout or nil, ping_timeout = (node.http_h2_health_check == "1") and node.http_h2_health_check_timeout or nil, } --不强制执行 TLS。如果未配置 TLS,将使用纯 HTTP 1.1。 end if node.transport == "http" then v2ray_transport = { type = "http", host = node.http_host or {}, path = node.http_path or "/", headers = node.user_agent and { ["User-Agent"] = node.user_agent } or nil, idle_timeout = (node.http_h2_health_check == "1") and node.http_h2_read_idle_timeout or nil, ping_timeout = (node.http_h2_health_check == "1") and node.http_h2_health_check_timeout or nil, } --不强制执行 TLS。如果未配置 TLS,将使用纯 HTTP 1.1。 end if node.transport == "ws" then v2ray_transport = { type = "ws", path = node.ws_path or "/", headers = (node.ws_host or node.user_agent) and { Host = node.ws_host, ["User-Agent"] = node.user_agent } or nil, max_early_data = tonumber(node.ws_maxEarlyData) or nil, early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil --要与 Xray-core 兼容,请将其设置为 Sec-WebSocket-Protocol。它需要与服务器保持一致。 } end if node.transport == "httpupgrade" then v2ray_transport = { type = "httpupgrade", host = node.httpupgrade_host, path = node.httpupgrade_path or "/", headers = node.user_agent and { ["User-Agent"] = node.user_agent } or nil } end if node.transport == "quic" then v2ray_transport = { type = "quic" } --没有额外的加密支持: 它基本上是重复加密。 并且 Xray-core 在这里与 v2ray-core 不兼容。 end if node.transport == "grpc" then v2ray_transport = { type = "grpc", service_name = node.grpc_serviceName, idle_timeout = tonumber(node.grpc_idle_timeout) or nil, ping_timeout = tonumber(node.grpc_health_check_timeout) or nil, permit_without_stream = (node.grpc_permit_without_stream == "1") and true or nil, } end local protocol_table = nil if node.protocol == "socks" then protocol_table = { version = "5", username = (node.username and node.password) and node.username or nil, password = (node.username and node.password) and node.password or nil, udp_over_tcp = node.uot == "1" and { enabled = true, version = 2 } or nil, } end if node.protocol == "http" then protocol_table = { username = (node.username and node.password) and node.username or nil, password = (node.username and node.password) and node.password or nil, path = nil, headers = nil, tls = tls } end if node.protocol == "shadowsocks" then protocol_table = { method = node.method or nil, password = node.password or "", plugin = (node.plugin_enabled and node.plugin) or nil, plugin_opts = (node.plugin_enabled and node.plugin_opts) or nil, udp_over_tcp = node.uot == "1" and { enabled = true, version = 2 } or nil, multiplex = mux, } end if node.protocol == "trojan" then protocol_table = { password = node.password, tls = tls, multiplex = mux, transport = v2ray_transport } end if node.protocol == "vmess" then protocol_table = { uuid = node.uuid, security = node.security, alter_id = (node.alter_id) and tonumber(node.alter_id) or 0, global_padding = (node.global_padding == "1") and true or false, authenticated_length = (node.authenticated_length == "1") and true or false, tls = tls, packet_encoding = "", --UDP 包编码。(空):禁用 packetaddr:由 v2ray 5+ 支持 xudp:由 xray 支持 multiplex = mux, transport = v2ray_transport, } end if node.protocol == "vless" then protocol_table = { uuid = node.uuid, flow = (node.tls == '1' and node.flow) and node.flow or nil, tls = tls, packet_encoding = "xudp", --UDP 包编码。(空):禁用 packetaddr:由 v2ray 5+ 支持 xudp:由 xray 支持 multiplex = mux, transport = v2ray_transport, } end if node.protocol == "wireguard" then if node.wireguard_reserved then local bytes = {} if not node.wireguard_reserved:match("[^%d,]+") then node.wireguard_reserved:gsub("%d+", function(b) bytes[#bytes + 1] = tonumber(b) end) else local result = api.base64Decode(node.wireguard_reserved) for i = 1, #result do bytes[i] = result:byte(i) end end node.wireguard_reserved = #bytes > 0 and bytes or nil end protocol_table = { system_interface = nil, interface_name = nil, local_address = node.wireguard_local_address, private_key = node.wireguard_secret_key, peer_public_key = node.wireguard_public_key, pre_shared_key = node.wireguard_preSharedKey, reserved = node.wireguard_reserved, mtu = tonumber(node.wireguard_mtu), } end if node.protocol == "hysteria" then local server_ports = {} if node.hysteria_hop then node.hysteria_hop = string.gsub(node.hysteria_hop, "-", ":") for range in node.hysteria_hop:gmatch("([^,]+)") do if range:match("^%d+:%d+$") then table.insert(server_ports, range) end end result.server_port = nil end protocol_table = { server_ports = next(server_ports) and server_ports or nil, hop_interval = (function() if not next(server_ports) then return nil end local v = tonumber((node.hysteria_hop_interval or "30"):match("^%d+")) return (v and v >= 5) and (v .. "s") or "30s" end)(), up_mbps = tonumber(node.hysteria_up_mbps), down_mbps = tonumber(node.hysteria_down_mbps), obfs = node.hysteria_obfs, auth = (node.hysteria_auth_type == "base64") and node.hysteria_auth_password or nil, auth_str = (node.hysteria_auth_type == "string") and node.hysteria_auth_password or nil, recv_window_conn = tonumber(node.hysteria_recv_window_conn), --1.14 将变更为 stream_receive_window recv_window = tonumber(node.hysteria_recv_window), --1.14 将变更为 connection_receive_window disable_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false, --1.14 将变更为 disable_path_mtu_discovery tls = tls } end if node.protocol == "shadowtls" then protocol_table = { version = tonumber(node.shadowtls_version), password = (node.shadowtls_version == "2" or node.shadowtls_version == "3") and node.password or nil, tls = tls, } end if node.protocol == "tuic" then protocol_table = { uuid = node.uuid, password = node.password, congestion_control = node.tuic_congestion_control or "cubic", udp_relay_mode = node.tuic_udp_relay_mode or "native", udp_over_stream = false, zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1") and true or false, heartbeat = (tonumber(node.tuic_heartbeat) or 3) .. "s", tls = tls } node.tuic_alpn = (node.tuic_alpn and node.tuic_alpn ~= "default") and node.tuic_alpn or "h3" local alpn = {} string.gsub(node.tuic_alpn, '[^,]+', function(w) table.insert(alpn, w) end) if #alpn > 0 then protocol_table.tls.alpn = alpn end end if node.protocol == "hysteria2" then local server_ports = {} if node.hysteria2_hop then node.hysteria2_hop = string.gsub(node.hysteria2_hop, "-", ":") for range in node.hysteria2_hop:gmatch("([^,]+)") do if range:match("^%d+:%d+$") then table.insert(server_ports, range) end end result.server_port = nil end if node.hysteria2_realms then server_ports = {} end local interval, interval_max if next(server_ports) then interval = "30s" local t = node.hysteria2_hop_interval or "30s" if t:find("-", 1, true) then local min, max = t:match("^(%d+)%-(%d+)$") min = tonumber(min) max = tonumber(max) if min and max then min = (min >= 5) and min or 5 max = (max >= min) and max or min interval = min .. "s" interval_max = max .. "s" end else t = tonumber(t:match("^%d+")) t = (t and t >= 5) and t or 30 interval = t .. "s" end end protocol_table = { server_ports = next(server_ports) and server_ports or nil, hop_interval = interval, hop_interval_max = interval_max, up_mbps = (node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) or nil, down_mbps = (node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) or nil, obfs = (function(t) if not t or t == "" then return nil end local o = { type = t, password = node.hysteria2_obfs_password } if t == "gecko" then local min = tonumber(node.hysteria2_obfs_MinPacketSize) or 512 local max = tonumber(node.hysteria2_obfs_MaxPacketSize) or 1200 if min <= 0 or min > max or max > 2048 then min = 512 max = 1200 end o.min_packet_size = min o.max_packet_size = max end return o end)(node.hysteria2_obfs_type), password = node.hysteria2_auth_password or nil, idle_timeout = (function(t) if not version_ge_1_14_0 then return nil end t = tonumber(tostring(t or "30"):match("^%d+")) return (t and t >= 4 and t <= 120) and t or 30 end)(node.hysteria2_idle_timeout), keep_alive_period = (function(t) if not version_ge_1_14_0 then return nil end t = tonumber(tostring(t or "0"):match("^%d+")) return (t and t >= 2 and t <= 60) and t or nil end)(node.hysteria2_keep_alive_period), disable_path_mtu_discovery = version_ge_1_14_0 and (tonumber(node.hysteria2_disable_mtu_discovery) == 1) or nil, tls = tls, realm = node.hysteria2_realms and (function() result.server = nil result.server_port = nil local realm = api.parse_realm_uri(node.hysteria2_realm_url) if realm then realm.server_url = (realm.scheme == "realm+http" and "http://" or "https://") .. realm.server_url realm.stun_servers = realm.stun_servers or node.hysteria2_realm_stun realm.scheme = nil realm.address = nil realm.port = nil return realm end return nil end)() or nil } end if node.protocol == "anytls" then protocol_table = { password = (node.password and node.password ~= "") and node.password or "", idle_session_check_interval = "30s", idle_session_timeout = "30s", min_idle_session = 5, tls = tls } end if node.protocol == "ssh" then protocol_table = { user = (node.username and node.username ~= "") and node.username or "root", password = (node.password and node.password ~= "") and node.password or "", private_key = node.ssh_priv_key, private_key_passphrase = node.ssh_priv_key_pp, host_key = node.ssh_host_key, host_key_algorithms = node.ssh_host_key_algo, client_version = node.ssh_client_version } end if node.protocol == "naive" then protocol_table = { username = (node.username and node.username ~= "") and node.username or "", password = (node.password and node.password ~= "") and node.password or "", insecure_concurrency = tonumber(node.naive_insecure_concurrency or 0) > 0 and tonumber(node.naive_insecure_concurrency) or 0, udp_over_tcp = node.uot == "1" and { enabled = true, version = 2 } or false, extra_headers = node.user_agent and { ["User-Agent"] = node.user_agent } or nil, quic = node.naive_quic == "1" and true or false, quic_congestion_control = (node.naive_quic == "1" and node.naive_congestion_control) and node.naive_congestion_control or nil, tls = tls } end if protocol_table then for key, value in pairs(protocol_table) do result[key] = value end end end return result end function gen_config_server(node) local outbounds = { { type = "direct", tag = "direct" } } local tls = { enabled = true, certificate_path = node.tls_certificateFile, key_path = node.tls_keyFile, alpn = (node.alpn and node.alpn ~= "default") and (function() local alpn = {} string.gsub(node.alpn, '[^,]+', function(w) table.insert(alpn, w) end) if #alpn > 0 then return alpn end return nil end)() or nil } if node.tls == "1" and node.reality == "1" then tls.certificate_path = nil tls.key_path = nil tls.server_name = node.reality_handshake_server tls.reality = { enabled = true, private_key = node.reality_private_key, short_id = { node.reality_shortId }, handshake = { server = node.reality_handshake_server, server_port = tonumber(node.reality_handshake_server_port) } } end if node.tls == "1" and node.ech == "1" then tls.ech = { enabled = true, key = node.ech_key and { node.ech_key } or nil } end local mux = nil if node.mux == "1" then mux = { enabled = true, padding = (node.mux_padding == "1") and true or false, brutal = { enabled = (node.tcpbrutal == "1") and true or false, up_mbps = tonumber(node.tcpbrutal_up_mbps) or 10, down_mbps = tonumber(node.tcpbrutal_down_mbps) or 50, }, } end local v2ray_transport = nil if node.transport == "http" then v2ray_transport = { type = "http", host = node.http_host or {}, path = node.http_path or "/", } end if node.transport == "ws" then v2ray_transport = { type = "ws", path = node.ws_path or "/", headers = (node.ws_host ~= nil) and { Host = node.ws_host } or nil, early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil --要与 Xray-core 兼容,请将其设置为 Sec-WebSocket-Protocol。它需要与服务器保持一致。 } end if node.transport == "httpupgrade" then v2ray_transport = { type = "httpupgrade", host = node.httpupgrade_host, path = node.httpupgrade_path or "/", } end if node.transport == "quic" then v2ray_transport = { type = "quic" } --没有额外的加密支持: 它基本上是重复加密。 并且 Xray-core 在这里与 v2ray-core 不兼容。 end if node.transport == "grpc" then v2ray_transport = { type = "grpc", service_name = node.grpc_serviceName, } end local inbound = { type = node.protocol, tag = "inbound", listen = (node.bind_local == "1") and "127.0.0.1" or "::", listen_port = tonumber(node.port), } local protocol_table = nil if node.protocol == "mixed" then protocol_table = { users = (node.auth == "1") and { { username = node.username, password = node.password } } or nil, set_system_proxy = false } end if node.protocol == "socks" then protocol_table = { users = (node.auth == "1") and { { username = node.username, password = node.password } } or nil } end if node.protocol == "http" then protocol_table = { users = (node.auth == "1") and { { username = node.username, password = node.password } } or nil, tls = (node.tls == "1") and tls or nil, } end if node.protocol == "shadowsocks" then protocol_table = { method = node.method, password = node.password, multiplex = mux, } end if node.protocol == "vmess" then if node.uuid then local users = {} for i = 1, #node.uuid do users[i] = { name = node.uuid[i], uuid = node.uuid[i], alterId = 0, } end protocol_table = { users = users, tls = (node.tls == "1") and tls or nil, multiplex = mux, transport = v2ray_transport, } end end if node.protocol == "vless" then if node.uuid then local users = {} for i = 1, #node.uuid do users[i] = { name = node.uuid[i], uuid = node.uuid[i], flow = node.flow, } end protocol_table = { users = users, tls = (node.tls == "1") and tls or nil, multiplex = mux, transport = v2ray_transport, } end end if node.protocol == "trojan" then if node.uuid then local users = {} for i = 1, #node.uuid do users[i] = { name = node.uuid[i], password = node.uuid[i], } end protocol_table = { users = users, tls = (node.tls == "1") and tls or nil, fallback = nil, fallback_for_alpn = nil, multiplex = mux, transport = v2ray_transport, } end end if node.protocol == "naive" then protocol_table = { users = { { username = node.username, password = node.password } }, tls = tls, } end if node.protocol == "hysteria" then protocol_table = { up = node.hysteria_up_mbps .. " Mbps", down = node.hysteria_down_mbps .. " Mbps", up_mbps = tonumber(node.hysteria_up_mbps), down_mbps = tonumber(node.hysteria_down_mbps), obfs = node.hysteria_obfs, users = { { name = "user1", auth = (node.hysteria_auth_type == "base64") and node.hysteria_auth_password or nil, auth_str = (node.hysteria_auth_type == "string") and node.hysteria_auth_password or nil, } }, recv_window_conn = node.hysteria_recv_window_conn and tonumber(node.hysteria_recv_window_conn) or nil, --1.14 to stream_receive_window recv_window_client = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil, --1.14 to connection_receive_window max_conn_client = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil, --1.14 to max_concurrent_streams disable_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false, --1.14 to disable_path_mtu_discover tls = tls } end if node.protocol == "tuic" then if node.uuid then local users = {} for i = 1, #node.uuid do users[i] = { name = node.uuid[i], uuid = node.uuid[i], password = node.password } end tls.alpn = (node.tuic_alpn and node.tuic_alpn ~= "default") and (function() local alpn = {} string.gsub(node.tuic_alpn, '[^,]+', function(w) table.insert(alpn, w) end) if #alpn > 0 then return alpn end return nil end)() or nil protocol_table = { users = users, congestion_control = node.tuic_congestion_control or "cubic", zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1") and true or false, heartbeat = (tonumber(node.tuic_heartbeat) or 3) .. "s", tls = tls } end end if node.protocol == "hysteria2" then protocol_table = { up_mbps = (node.hysteria2_ignore_client_bandwidth ~= "1" and node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) or nil, down_mbps = (node.hysteria2_ignore_client_bandwidth ~= "1" and node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) or nil, obfs = (function(t) if not t or t == "" then return nil end local o = { type = t, password = node.hysteria2_obfs_password } if t == "gecko" then local min = tonumber(node.hysteria2_obfs_MinPacketSize) or 512 local max = tonumber(node.hysteria2_obfs_MaxPacketSize) or 1200 if min <= 0 or min > max or max > 2048 then min = 512 max = 1200 end o.min_packet_size = min o.max_packet_size = max end return o end)(node.hysteria2_obfs_type), users = { { name = "user1", password = node.hysteria2_auth_password or nil, } }, ignore_client_bandwidth = (node.hysteria2_ignore_client_bandwidth == "1") and true or false, tls = tls, realm = node.hysteria2_realms and (function() local realm = api.parse_realm_uri(node.hysteria2_realm_url) if realm then realm.server_url = (realm.scheme == "realm+http" and "http://" or "https://") .. realm.server_url realm.stun_servers = realm.stun_servers or node.hysteria2_realm_stun realm.scheme = nil realm.address = nil realm.port = nil realm.stun_domain_resolver = "direct" return realm end return nil end)() or nil } end if node.protocol == "anytls" then protocol_table = { users = { { name = (node.username and node.username ~= "") and node.username or "sekai", password = node.password } }, tls = tls, } end if node.protocol == "direct" then protocol_table = { network = (node.d_protocol ~= "TCP,UDP") and node.d_protocol or nil, override_address = node.d_address, override_port = tonumber(node.d_port) } end if protocol_table then for key, value in pairs(protocol_table) do inbound[key] = value end end local route = { rules = { { ip_is_private = true, action = node.accept_lan == "1" and "route" or "reject", outbound = node.accept_lan == "1" and "direct" or nil } } } if node.outbound_node then local outbound = nil if node.outbound_node == "_iface" and node.outbound_node_iface then outbound = { type = "direct", tag = "outbound", bind_interface = node.outbound_node_iface, routing_mark = 255, } sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.outbound_node_iface)) else local outbound_node_t = uci:get_all("passwall", node.outbound_node) if node.outbound_node == "_socks" or node.outbound_node == "_http" then outbound_node_t = { type = node.type, protocol = node.outbound_node:gsub("_", ""), address = node.outbound_node_address, port = tonumber(node.outbound_node_port), username = (node.outbound_node_username and node.outbound_node_username ~= "") and node.outbound_node_username or nil, password = (node.outbound_node_password and node.outbound_node_password ~= "") and node.outbound_node_password or nil, } end outbound = require("luci.passwall.util_sing-box").gen_outbound(nil, outbound_node_t, "outbound") end if outbound then route.final = outbound.tag table.insert(outbounds, 1, outbound) end end local config = { log = { disabled = (not node or node.log == "0") and true or false, level = node.loglevel or "info", timestamp = true, --output = logfile, }, dns = { servers = {{ type = "local", tag = "direct" }} }, inbounds = { inbound }, outbounds = outbounds, route = route } for index, value in ipairs(config.outbounds) do for k, v in pairs(config.outbounds[index]) do if k:find("_") == 1 then config.outbounds[index][k] = nil end end end return config end function gen_config(var) local flag = var["flag"] local log = var["log"] or "0" local loglevel = var["loglevel"] or "warn" local logfile = var["logfile"] or "/dev/null" local node_id = var["node"] local server_host = var["server_host"] local server_port = var["server_port"] local tcp_proxy_way = var["tcp_proxy_way"] local tcp_redir_port = var["tcp_redir_port"] local udp_redir_port = var["udp_redir_port"] local local_socks_address = var["local_socks_address"] or "0.0.0.0" local local_socks_port = var["local_socks_port"] local local_socks_username = var["local_socks_username"] local local_socks_password = var["local_socks_password"] local local_http_address = var["local_http_address"] or "0.0.0.0" local local_http_port = var["local_http_port"] local local_http_username = var["local_http_username"] local local_http_password = var["local_http_password"] local dns_listen_port = var["dns_listen_port"] local direct_dns_port = var["direct_dns_port"] local direct_dns_udp_server = var["direct_dns_udp_server"] local direct_dns_tcp_server = var["direct_dns_tcp_server"] local direct_dns_query_strategy = var["direct_dns_query_strategy"] local remote_dns_udp_server = var["remote_dns_udp_server"] local remote_dns_udp_port = var["remote_dns_udp_port"] local remote_dns_tcp_server = var["remote_dns_tcp_server"] local remote_dns_tcp_port = var["remote_dns_tcp_port"] local remote_dns_doh = var["remote_dns_doh"] local remote_dns_http3 = var["remote_dns_http3"] local remote_dns_client_ip = var["remote_dns_client_ip"] local remote_dns_query_strategy = var["remote_dns_query_strategy"] local remote_dns_fake = var["remote_dns_fake"] local dns_cache = var["dns_cache"] local dns_socks_address = var["dns_socks_address"] local dns_socks_port = var["dns_socks_port"] local no_run = var["no_run"] local dns_domain_rules = {} local dns = nil local inbounds = {} local outbounds = {} local rule_set_table = {} local COMMON = {} local singbox_settings = uci:get_all(appname, "@global_singbox[0]") or {} local route = { rules = {} } local experimental = nil function add_rule_set(tab) if tab and next(tab) and tab.tag and not rule_set_table[tab.tag]then rule_set_table[tab.tag] = tab end end function parse_rule_set(w, rs) -- Format: remote:https://raw.githubusercontent.com/lyc8503/sing-box-rules/rule-set-geosite/geosite-netflix.srs' -- Format: local:/usr/share/sing-box/geosite-netflix.srs' local result = nil if w and #w > 0 then if w:find("local:") == 1 or w:find("remote:") == 1 then local _type = w:sub(1, w:find(":") - 1) -- "local" or "remote" w = w:sub(w:find(":") + 1, #w) local format = nil local filename = w:sub(-w:reverse():find("/") + 1) -- geosite-netflix.srs local suffix = "" local find_doc = filename:reverse():find("%.") if find_doc then suffix = filename:sub(-find_doc + 1) -- "srs" or "json" end if suffix == "srs" then format = "binary" elseif suffix == "json" then format = "source" end if format then local rule_set_tag = filename:sub(1, filename:find("%.") - 1) --geosite-netflix if rule_set_tag and #rule_set_tag > 0 then if rs then rule_set_tag = "rs_" .. rule_set_tag end result = { type = _type, tag = rule_set_tag, format = format, path = _type == "local" and w or nil, url = _type == "remote" and w or nil, --download_detour = _type == "remote" and "", --update_interval = _type == "remote" and "", } end end end end return result end function geo_rule_set(prefix, rule_name) local output_srs_file = "local:" .. GEO_VAR.TO_SRS_PATH .. prefix .. "-" .. rule_name .. ".srs" return parse_rule_set(output_srs_file) end if node_id then local node = uci:get_all(appname, node_id) if node then if server_host and server_port then node.address = server_host node.port = server_port end end if local_socks_port then local inbound = { type = "socks", tag = "socks-in", listen = local_socks_address, listen_port = tonumber(local_socks_port), } if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then inbound.users = { { username = local_socks_username, password = local_socks_password } } end table.insert(inbounds, inbound) table.insert(route.rules, { action = "sniff", inbound = inbound.tag }) end if local_http_port then local inbound = { type = "http", tag = "http-in", listen = local_http_address, listen_port = tonumber(local_http_port) } if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then inbound.users = { { username = local_http_username, password = local_http_password } } end table.insert(inbounds, inbound) end if tcp_redir_port then local inbound if tcp_proxy_way ~= "tproxy" then inbound = { type = "redirect", tag = "redirect_tcp", listen = "::", listen_port = tonumber(tcp_redir_port) } else inbound = { type = "tproxy", tag = "tproxy_tcp", network = "tcp", listen = "::", listen_port = tonumber(tcp_redir_port) } end table.insert(inbounds, inbound) table.insert(route.rules, { action = "sniff", inbound = inbound.tag }) end if udp_redir_port then local inbound = { type = "tproxy", tag = "tproxy_udp", network = "udp", listen = "::", listen_port = tonumber(udp_redir_port) } table.insert(inbounds, inbound) table.insert(route.rules, { action = "sniff", inbound = inbound.tag }) end function gen_socks_config_node(node_id, socks_id, remarks) if node_id then socks_id = node_id:sub(1 + #"Socks_") end local result local socks_node = uci:get_all(appname, socks_id) or nil if socks_node then if not remarks then remarks = socks_node.port end result = { [".name"] = "Socksid_" .. socks_id, remarks = remarks, type = "sing-box", protocol = "socks", address = "127.0.0.1", port = socks_node.port, uot = "1" } end return result end local nodes_list = {} function get_urltest_batch_nodes(_node) if #nodes_list == 0 then for k, e in ipairs(api.get_valid_nodes()) do if e.node_type == "normal" and (not e.chain_proxy or e.chain_proxy == "") then nodes_list[#nodes_list + 1] = { id = e[".name"], remarks = e["remarks"], group = e["group"] } end end end if not _node.node_group or _node.node_group == "" then return {} end local nodes = {} for g in _node.node_group:gmatch("%S+") do g = api.UrlDecode(g) for k, v in pairs(nodes_list) do local gn = (v.group and v.group ~= "") and v.group or "default" if gn:lower() == g:lower() and api.match_node_rule(v.remarks, _node.node_match_rule) then nodes[#nodes + 1] = v.id end end end return nodes end function get_node_by_id(node_id) if not node_id or node_id == "" or node_id == "nil" then return nil end if node_id:find("Socks_") then return gen_socks_config_node(node_id) else return uci:get_all(appname, node_id) end end function gen_urltest_outbound(_node) local urltest_id = _node[".name"] local urltest_tag = "urltest-" .. urltest_id -- existing urltest for _, v in ipairs(outbounds) do if v.tag == urltest_tag then return v, true end end -- new urltest local ut_nodes if _node.node_add_mode and _node.node_add_mode == "batch" then ut_nodes = get_urltest_batch_nodes(_node) else ut_nodes = _node.urltest_node end api.log(" - 加载 Sing-Box URLTest 节点【" .. (_node.remarks or "") .. "】,子节点数量:" .. #(ut_nodes or {})) local valid_nodes = {} for i = 1, #(ut_nodes or {}) do local ut_node_id = ut_nodes[i] local ut_node_tag = "ut-" .. ut_node_id local is_new_ut_node = true for _, outbound in ipairs(outbounds) do if string.sub(outbound.tag, 1, #ut_node_tag) == ut_node_tag then is_new_ut_node = false valid_nodes[#valid_nodes + 1] = outbound.tag break end end if is_new_ut_node then local outboundTag = gen_outbound_get_tag(flag, ut_node_id, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run }) if outboundTag then valid_nodes[#valid_nodes + 1] = outboundTag end end end if #valid_nodes == 0 then return nil end local outbound = { type = "urltest", tag = urltest_tag, outbounds = valid_nodes, url = _node.urltest_url or "https://www.gstatic.com/generate_204", interval = (api.format_go_time(_node.urltest_interval) ~= "0s") and api.format_go_time(_node.urltest_interval) or "3m", tolerance = (_node.urltest_tolerance and tonumber(_node.urltest_tolerance) > 0) and tonumber(_node.urltest_tolerance) or 50, idle_timeout = (api.format_go_time(_node.urltest_idle_timeout) ~= "0s") and api.format_go_time(_node.urltest_idle_timeout) or "30m", interrupt_exist_connections = (_node.urltest_interrupt_exist_connections == "true" or _node.urltest_interrupt_exist_connections == "1") and true or false } return outbound end function set_outbound_detour(node, outbound, outbounds_table) if not node or not outbound or not outbounds_table then return nil end local default_outTag = outbound.tag local last_insert_outbound if node.shadowtls == "1" then local _node = { type = "sing-box", protocol = "shadowtls", shadowtls_version = node.shadowtls_version, password = (node.shadowtls_version == "2" or node.shadowtls_version == "3") and node.shadowtls_password or nil, address = node.address, port = node.port, tls = "1", tls_serverName = node.shadowtls_serverName, utls = node.shadowtls_utls, fingerprint = node.shadowtls_fingerprint } local shadowtls_outbound = gen_outbound(nil, _node, outbound.tag .. "_shadowtls") if shadowtls_outbound then last_insert_outbound = shadowtls_outbound outbound.detour = outbound.tag .. "_shadowtls" outbound.server = nil outbound.server_port = nil end end if node.chain_proxy == "1" and node.preproxy_node then if outbound["_flag_proxy_tag"] then --Ignore else local preproxy_node = get_node_by_id(node.preproxy_node) if preproxy_node then local preproxy_outbound, exist if preproxy_node.protocol == "_urltest" then preproxy_outbound, exist = gen_urltest_outbound(preproxy_node) else preproxy_outbound = gen_outbound(node[".name"], preproxy_node) end if preproxy_outbound then outbound.tag = preproxy_outbound.tag .. " -> " .. outbound.tag outbound.detour = preproxy_outbound.tag if not exist then last_insert_outbound = preproxy_outbound end default_outTag = outbound.tag end end end end if node.chain_proxy == "2" and node.to_node then local to_node = get_node_by_id(node.to_node) if to_node then -- Landing Node not support use special node. if to_node.protocol:find("^_") then to_node = nil end end if to_node then local to_outbound if to_node.type ~= "sing-box" then local tag = to_node[".name"] local new_port = api.get_new_port() table.insert(inbounds, { type = "direct", tag = tag, listen = "127.0.0.1", listen_port = new_port, override_address = to_node.address, override_port = tonumber(to_node.port), }) table.insert(rules, 1, { action = "route", inbound = {tag}, outbound = outbound.tag, }) if to_node.tls_serverName == nil then to_node.tls_serverName = to_node.address end to_node.address = "127.0.0.1" to_node.port = new_port to_outbound = gen_outbound(node[".name"], to_node, tag, { tag = tag, run_socks_instance = not no_run }) else to_outbound = gen_outbound(node[".name"], to_node) end if to_outbound then to_outbound.tag = outbound.tag .. " -> " .. to_outbound.tag if to_node.type == "sing-box" then to_outbound.detour = outbound.tag end table.insert(outbounds_table, to_outbound) default_outTag = to_outbound.tag end end end if node.chain_proxy == "3" and node.outbound_iface then outbound.detour = nil outbound.bind_interface = node.outbound_iface end return default_outTag, last_insert_outbound end function gen_outbound_get_tag(flag, node_id, tag, proxy_table) if not node_id or node_id == "nil" then return nil end local node if type(node_id) == "string" then node = get_node_by_id(node_id) elseif type(node_id) == "table" then node = node_id end if not tag then tag = node[".name"] end if node then if proxy_table.chain_proxy == "1" or proxy_table.chain_proxy == "2" then node.chain_proxy = proxy_table.chain_proxy node.preproxy_node = proxy_table.chain_proxy == "1" and proxy_table.preproxy_node node.to_node = proxy_table.chain_proxy == "2" and proxy_table.to_node proxy_table.chain_proxy = nil proxy_table.preproxy_node = nil proxy_table.to_node = nil end local outbound, exist if node.protocol == "_urltest" then outbound, exist = gen_urltest_outbound(node) if exist then return outbound.tag end elseif node.protocol == "_iface" then if node.iface then outbound = { tag = tag, type = "direct", bind_interface = node.iface, routing_mark = 255, } sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface)) end else for _, _outbound in ipairs(outbounds) do -- Avoid generating duplicate nested processes if _outbound["_flag_proxy_tag"] and _outbound["_flag_proxy_tag"]:find("socks <- " .. node[".name"], 1, true) then outbound = api.clone(_outbound) outbound.tag = tag break end end if not outbound then outbound = gen_outbound(flag, node, tag, proxy_table) end end if outbound then local default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds) table.insert(outbounds, outbound) if last_insert_outbound then table.insert(outbounds, last_insert_outbound) end return default_outbound_tag end end end rules = {} if node and node.protocol == "_shunt" then inner_fakedns = node.fakedns or "0" local function gen_shunt_node(rule_name, _node_id) if not rule_name then return nil, nil end if not _node_id then _node_id = node[rule_name] end if _node_id == "_direct" then return "direct" elseif _node_id == "_blackhole" then return "block" elseif _node_id == "_default" and rule_name ~= "default" then return "default" elseif _node_id then local proxy_table = { fragment = singbox_settings.fragment == "1", record_fragment = singbox_settings.record_fragment == "1", run_socks_instance = not no_run, } local preproxy_node_id = node[rule_name .. "_proxy_tag"] if preproxy_node_id == _node_id then preproxy_node_id = nil end if preproxy_node_id then proxy_table.chain_proxy = "2" proxy_table.to_node = _node_id return gen_outbound_get_tag(flag, preproxy_node_id, rule_name, proxy_table) else return gen_outbound_get_tag(flag, _node_id, rule_name, proxy_table) end end return nil end --default_node local default_node_id = node.default_node or "_direct" COMMON.default_outbound_tag = gen_shunt_node("default", default_node_id) if inner_fakedns == "1" and node["default_fakedns"] == "1" then remote_dns_fake = true end --shunt rule uci:foreach(appname, "shunt_rules", function(e) local outboundTag = gen_shunt_node(e[".name"]) if outboundTag and e.remarks then if outboundTag == "default" then outboundTag = COMMON.default_outbound_tag end local protocols = nil if e["protocol"] and e["protocol"] ~= "" then protocols = {} string.gsub(e["protocol"], '[^' .. " " .. ']+', function(w) table.insert(protocols, w) end) end local inboundTag = nil if e["inbound"] and e["inbound"] ~= "" then inboundTag = {} if e["inbound"]:find("tproxy") then if tcp_redir_port then if tcp_proxy_way == "tproxy" then table.insert(inboundTag, "tproxy_tcp") else table.insert(inboundTag, "redirect_tcp") end end if udp_redir_port then table.insert(inboundTag, "tproxy_udp") end end if e["inbound"]:find("socks") then if local_socks_port then table.insert(inboundTag, "socks-in") end end end local rule = { action = "route", inbound = inboundTag, outbound = outboundTag, protocol = protocols } if outboundTag == "block" then rule.action = "reject" rule.outbound = nil end if e.network then local network = {} string.gsub(e.network, '[^' .. "," .. ']+', function(w) table.insert(network, w) end) rule.network = network end if e.source then local source_ip_cidr = {} local is_private = false string.gsub(e.source, '[^' .. " " .. ']+', function(w) if w:find("geoip") == 1 then local _geoip = w:sub(1 + #"geoip:") --适配srs if _geoip == "private" then is_private = true end else table.insert(source_ip_cidr, w) end end) rule.source_ip_is_private = is_private and true or nil rule.source_ip_cidr = #source_ip_cidr > 0 and source_ip_cidr or nil if is_private or #source_ip_cidr > 0 then rule.rule_set_ip_cidr_match_source = true end end --[[ -- Too low usage rate, hidden if e.sourcePort then local source_port = {} local source_port_range = {} string.gsub(e.sourcePort, '[^' .. "," .. ']+', function(w) if tonumber(w) and tonumber(w) >= 1 and tonumber(w) <= 65535 then table.insert(source_port, tonumber(w)) else table.insert(source_port_range, w) end end) rule.source_port = #source_port > 0 and source_port or nil rule.source_port_range = #source_port_range > 0 and source_port_range or nil end ]]-- if e.port then local port = {} local port_range = {} string.gsub(e.port, '[^' .. "," .. ']+', function(w) if tonumber(w) and tonumber(w) >= 1 and tonumber(w) <= 65535 then table.insert(port, tonumber(w)) else table.insert(port_range, w) end end) rule.port = #port > 0 and port or nil rule.port_range = #port_range > 0 and port_range or nil end local rule_set = {} if e.domain_list then local domain_table = { shunt_tag = e[".name"], outboundTag = outboundTag, domain = {}, domain_suffix = {}, domain_keyword = {}, domain_regex = {}, rule_set = {}, fakedns = nil, invert = e.invert == "1" and true or nil } string.gsub(e.domain_list, '[^' .. "\r\n" .. ']+', function(w) w = api.trim(w) if w == "" or w:find("#") == 1 then return end if w:find("geosite:") == 1 then local _geosite = w:sub(1 + #"geosite:") --适配srs local t = geo_rule_set("geosite", _geosite) if t then GEO_VAR.SITE_TAGS[_geosite] = true add_rule_set(t) table.insert(rule_set, t.tag) table.insert(domain_table.rule_set, t.tag) end elseif w:find("regexp:") == 1 then table.insert(domain_table.domain_regex, w:sub(1 + #"regexp:")) elseif w:find("full:") == 1 then table.insert(domain_table.domain, w:sub(1 + #"full:")) elseif w:find("domain:") == 1 then table.insert(domain_table.domain_suffix, w:sub(1 + #"domain:")) elseif w:find("rule-set:", 1, true) == 1 or w:find("rs:") == 1 then w = w:sub(w:find(":") + 1, #w) local t = parse_rule_set(w, true) if t then add_rule_set(t) table.insert(rule_set, t.tag) table.insert(domain_table.rule_set, t.tag) end else table.insert(domain_table.domain_keyword, w) end end) rule.domain = #domain_table.domain > 0 and domain_table.domain or nil rule.domain_suffix = #domain_table.domain_suffix > 0 and domain_table.domain_suffix or nil rule.domain_keyword = #domain_table.domain_keyword > 0 and domain_table.domain_keyword or nil rule.domain_regex = #domain_table.domain_regex > 0 and domain_table.domain_regex or nil rule.rule_set = #domain_table.rule_set > 0 and domain_table.rule_set or nil if inner_fakedns == "1" and node[e[".name"] .. "_fakedns"] == "1" then domain_table.fakedns = true end if outboundTag and (rule.domain or rule.domain_suffix or rule.domain_keyword or rule.domain_regex or rule.rule_set) then table.insert(dns_domain_rules, api.clone(domain_table)) end end if e.ip_list then local ip_cidr = {} local is_private = false string.gsub(e.ip_list, '[^' .. "\r\n" .. ']+', function(w) w = api.trim(w) if w == "" or w:find("#") == 1 then return end if w:find("geoip:") == 1 then local _geoip = w:sub(1 + #"geoip:") --适配srs if _geoip == "private" then is_private = true else local t = geo_rule_set("geoip", _geoip) if t then GEO_VAR.IP_TAGS[_geoip] = true add_rule_set(t) table.insert(rule_set, t.tag) end end elseif w:find("rule-set:", 1, true) == 1 or w:find("rs:") == 1 then w = w:sub(w:find(":") + 1, #w) local t = parse_rule_set(w, true) if t then add_rule_set(t) table.insert(rule_set, t.tag) end else table.insert(ip_cidr, w) end end) rule.ip_is_private = is_private and true or nil rule.ip_cidr = #ip_cidr > 0 and ip_cidr or nil end rule.rule_set = #rule_set > 0 and rule_set or nil --适配srs rule.invert = e.invert == "1" and true or nil table.insert(rules, rule) end end) else COMMON.default_outbound_tag = gen_outbound_get_tag(flag, node or node_id, nil, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run }) end for index, value in ipairs(rules) do table.insert(route.rules, rules[index]) end end table.insert(route.rules, { action = "route", ip_is_private = true, outbound = "direct" }) if COMMON.default_outbound_tag then table.insert(route.rules, { action = "route", port_range = { "0:65535" }, outbound = COMMON.default_outbound_tag }) route.final = COMMON.default_outbound_tag end if dns_listen_port then dns = { servers = {}, rules = {}, disable_cache = (dns_cache and dns_cache == "0") and true or false, disable_expire = false, --禁用 DNS 缓存过期。 independent_cache = false, --使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。 reverse_mapping = true, --在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。 } local default_outTag = COMMON.default_outbound_tag if dns_socks_address and dns_socks_port then default_outTag = "dns_socks_out" table.insert(outbounds, 1, { type = "socks", tag = default_outTag, server = dns_socks_address, server_port = tonumber(dns_socks_port) }) end remote_strategy = "prefer_ipv6" if remote_dns_query_strategy == "UseIPv4" then remote_strategy = "ipv4_only" elseif remote_dns_query_strategy == "UseIPv6" then remote_strategy = "ipv6_only" end local remote_server = { tag = "remote", domain_resolver = "direct", detour = default_outTag, } if remote_dns_udp_server then local server_port = tonumber(remote_dns_udp_port) or 53 remote_server.type = "udp" remote_server.server = remote_dns_udp_server remote_server.server_port = server_port elseif remote_dns_tcp_server then local server_port = tonumber(remote_dns_tcp_port) or 53 remote_server.type = "tcp" remote_server.server = remote_dns_tcp_server remote_server.server_port = server_port elseif remote_dns_doh then local _a = api.parseDoH(remote_dns_doh) if _a then remote_server.type = "https" if remote_dns_http3 then remote_server.type = "h3" end remote_server.server = _a.hostname remote_server.server_port = _a.port or 443 remote_server.path = _a.pathname or "" if api.datatypes.hostname(_a.hostname) then if _a.hostip then if not hosts_predefined then hosts_predefined = {} end hosts_predefined[_a.hostname] = _a.hostip remote_server_domain_resolver = "hosts" else GLOBAL.DNS_HOSTNAME[_a.hostname] = true remote_server_domain_resolver = "direct" end end end end if api.is_local_ip(remote_server.server) then --dns为本地ip,不走代理 remote_server.detour = "direct" end if remote_server_domain_resolver then remote_server.domain_resolver = remote_server_domain_resolver end table.insert(dns.servers, remote_server) fakedns_tag = "remote_fakeip" if remote_dns_fake or inner_fakedns == "1" then table.insert(dns.servers, { tag = fakedns_tag, type = "fakeip", inet4_range = "198.18.0.0/15", inet6_range = "fc00::/18", }) if not experimental then experimental = {} end experimental.cache_file = { enabled = true, store_fakeip = true, path = api.CACHE_PATH .. "/singbox_" .. flag .. ".db" } end if direct_dns_udp_server or direct_dns_tcp_server then local domain = {} local nodes_domain_text = sys.exec([[uci show passwall | sed -n "s/.*\.address='\([^']*\)'/\1/p" | sort -u]]) string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w) w = (w or ""):lower() if not api.vps_domain_exclude(w) and api.datatypes.hostname(w) and not GLOBAL.VPS_EXCLUDE[w] then table.insert(domain, w) end end) if #domain > 0 then table.insert(dns_domain_rules, 1, { outboundTag = "direct", domain = domain }) end direct_strategy = "prefer_ipv6" if direct_dns_query_strategy == "UseIPv4" then direct_strategy = "ipv4_only" elseif direct_dns_query_strategy == "UseIPv6" then direct_strategy = "ipv6_only" end local direct_dns_server, port, type if direct_dns_udp_server then port = tonumber(direct_dns_port) or 53 direct_dns_server = direct_dns_udp_server type = "udp" elseif direct_dns_tcp_server then port = tonumber(direct_dns_port) or 53 direct_dns_server = direct_dns_tcp_server type = "tcp" end table.insert(dns.servers, { tag = "direct", type = type, server = direct_dns_server, server_port = port, detour = "direct", }) end local default_dns_flag = "remote" if dns_socks_address and dns_socks_port then else if node_id and (tcp_redir_port or udp_redir_port) then local node = get_node_by_id(node_id) if node.protocol == "_shunt" then if node.default_node == "_direct" then default_dns_flag = "direct" end end else default_dns_flag = "direct" end end if default_dns_flag == "remote" then if remote_dns_fake then table.insert(dns.rules, { query_type = { "A", "AAAA" }, server = fakedns_tag, disable_cache = true, rewrite_ttl = 30, strategy = remote_strategy }) end end dns.final = default_dns_flag --按分流顺序DNS if dns_domain_rules and #dns_domain_rules > 0 then for index, value in ipairs(dns_domain_rules) do if value.outboundTag and (value.domain or value.domain_suffix or value.domain_keyword or value.domain_regex or value.rule_set) then local dns_rule = { action = "route", server = value.outboundTag, domain = (value.domain and #value.domain > 0) and value.domain or nil, domain_suffix = (value.domain_suffix and #value.domain_suffix > 0) and value.domain_suffix or nil, domain_keyword = (value.domain_keyword and #value.domain_keyword > 0) and value.domain_keyword or nil, domain_regex = (value.domain_regex and #value.domain_regex > 0) and value.domain_regex or nil, rule_set = (value.rule_set and #value.rule_set > 0) and value.rule_set or nil, --适配srs disable_cache = false, invert = value.invert, } if value.outboundTag == "block" then dns_rule.action = "predefined" dns_rule.rcode = "NOERROR" dns_rule.server = nil dns_rule.disable_cache = nil end if value.outboundTag == "direct" then dns_rule.strategy = direct_strategy end if value.outboundTag ~= "block" and value.outboundTag ~= "direct" then dns_rule.server = "remote" dns_rule.rewrite_ttl = 30 dns_rule.strategy = remote_strategy dns_rule.client_subnet = remote_dns_client_ip if value.outboundTag ~= COMMON.default_outbound_tag and (remote_server.address or remote_server.server) then local remote_dns_server = api.clone(remote_server) remote_dns_server.tag = value.shunt_tag local is_local = (remote_server.address and api.is_local_ip(remote_server.address)) or (remote_server.server and api.is_local_ip(remote_server.server)) --dns为本地ip,不走代理 remote_dns_server.detour = is_local and "direct" or value.outboundTag table.insert(dns.servers, remote_dns_server) dns_rule.server = remote_dns_server.tag end if value.fakedns then local fakedns_dns_rule = api.clone(dns_rule) fakedns_dns_rule.query_type = { "A", "AAAA" } fakedns_dns_rule.server = fakedns_tag fakedns_dns_rule.disable_cache = true table.insert(dns.rules, fakedns_dns_rule) end end table.insert(dns.rules, dns_rule) end end end local dns_in_inbound = { type = "direct", tag = "dns-in", listen = "127.0.0.1", listen_port = tonumber(dns_listen_port), } table.insert(inbounds, dns_in_inbound) table.insert(route.rules, { action = "sniff", inbound = dns_in_inbound.tag }) table.insert(route.rules, 1, { action = "hijack-dns", inbound = dns_in_inbound.tag }) end if not dns then dns = { servers = {{ type = "local", tag = "direct" }} } end route.default_domain_resolver = { server = "direct" } if not dns.rules then dns.rules = {} end for i, v in pairs(GLOBAL.DNS_SERVER) do table.insert(dns.servers, v.server) table.insert(dns.rules, 1, { action = "route", server = v.server.tag, disable_cache = false, domain = v.domain, }) end if next(GLOBAL.DNS_HOSTNAME) then local hostname = {} for line, _ in pairs(GLOBAL.DNS_HOSTNAME) do table.insert(hostname, line) end table.insert(dns.rules, 1, { query_type = { "A", "AAAA" }, domain = hostname, server = "direct" }) end if next(ech_domain) ~= nil then table.insert(dns.servers, { tag = "ech-dns", type = "https", server = "223.5.5.5" }) local domain = {} for line, _ in pairs(ech_domain) do domain[#domain+1] = line end table.insert(dns.rules, 1, { domain = domain, server = "ech-dns" }) end table.insert(dns.servers, { tag = "hosts", type = "hosts", predefined = (hosts_predefined and next(hosts_predefined) ~= nil) and hosts_predefined or nil }) if not version_ge_1_14_0 then table.insert(dns.rules, 1, { ip_accept_any = true, server = "hosts" }) else table.insert(dns.rules, 1, { action = "evaluate", server = "hosts" }) table.insert(dns.rules, 2, { match_response = true, ip_accept_any = true, action = "respond" }) end if COMMON.default_outbound_tag == "block" then route.final = nil table.insert(route.rules, { action = "reject" }) end if next(rule_set_table) then route.rule_set = {} for k, v in pairs(rule_set_table) do table.insert(route.rule_set, v) end end if inbounds or outbounds then local config = { log = { disabled = log == "0" and true or false, level = loglevel, timestamp = true, output = logfile, }, -- DNS dns = dns, -- 传入连接 inbounds = inbounds, -- 传出连接 outbounds = outbounds, -- 路由 route = route, --实验性 experimental = experimental, } table.insert(outbounds, { type = "direct", tag = "direct", routing_mark = 255, }) for index, value in ipairs(config.outbounds) do if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and (value.server_port or value.server_ports) and not no_run then sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list")) end if not value.detour and not value.bind_interface and value.server then value.detour = "direct" end if (value.server and not api.datatypes.hostname(value.server)) and not value.realm then value.domain_resolver = nil end for k, v in pairs(config.outbounds[index]) do if k:find("_") == 1 then config.outbounds[index][k] = nil end end end if true then local endpoints = {} for i = #config.outbounds, 1, -1 do local value = config.outbounds[i] if value.type == "wireguard" then -- https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint local endpoint = { type = "wireguard", tag = value.tag, system = value.system_interface, name = value.interface_name, mtu = value.mtu, address = value.local_address, private_key = value.private_key, peers = { { address = value.server, port = value.server_port, public_key = value.peer_public_key, pre_shared_key = value.pre_shared_key, allowed_ips = {"0.0.0.0/0"}, reserved = value.reserved } }, domain_resolver = { server = "direct", strategy = value.domain_strategy }, detour = value.detour } endpoints[#endpoints + 1] = endpoint table.remove(config.outbounds, i) end end if #endpoints > 0 then config.endpoints = endpoints end end return jsonc.stringify(config, 1) end end function gen_proto_config(var) local local_socks_address = var["local_socks_address"] or "0.0.0.0" local local_socks_port = var["local_socks_port"] local local_socks_username = var["local_socks_username"] local local_socks_password = var["local_socks_password"] local local_http_address = var["local_http_address"] or "0.0.0.0" local local_http_port = var["local_http_port"] local local_http_username = var["local_http_username"] local local_http_password = var["local_http_password"] local server_proto = var["server_proto"] local server_address = var["server_address"] local server_port = var["server_port"] local server_username = var["server_username"] local server_password = var["server_password"] local inbounds = {} local outbounds = {} if local_socks_address and local_socks_port then local inbound = { type = "socks", tag = "socks-in", listen = local_socks_address, listen_port = tonumber(local_socks_port), } if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then inbound.users = { username = local_socks_username, password = local_socks_password } end table.insert(inbounds, inbound) end if local_http_address and local_http_port then local inbound = { type = "http", tag = "http-in", tls = nil, listen = local_http_address, listen_port = tonumber(local_http_port), } if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then inbound.users = { { username = local_http_username, password = local_http_password } } end table.insert(inbounds, inbound) end if server_proto ~= "nil" and server_address ~= "nil" and server_port ~= "nil" then local outbound = { type = server_proto, tag = "out", server = server_address, server_port = tonumber(server_port), username = (server_username and server_password) and server_username or nil, password = (server_username and server_password) and server_password or nil, } if outbound then table.insert(outbounds, outbound) end end local config = { log = { disabled = true, level = "warn", timestamp = true, }, -- 传入连接 inbounds = inbounds, -- 传出连接 outbounds = outbounds, } return jsonc.stringify(config, 1) end _G.gen_config = gen_config _G.gen_proto_config = gen_proto_config _G.geo_convert_srs = geo_convert_srs if arg[1] then local func =_G[arg[1]] if func then local var = nil if arg[2] then var = jsonc.parse(arg[2]) end print(func(var)) if (next(GEO_VAR.SITE_TAGS) or next(GEO_VAR.IP_TAGS)) and not no_run then convert_geofile() end end end