module("luci.passwall2.util_sing-box", package.seeall) local api = require "luci.passwall2.api" local uci = api.uci local sys = api.sys local jsonc = api.jsonc local appname = api.appname local fs = api.fs local CACHE_PATH = api.CACHE_PATH local split = api.split local local_version = api.get_app_version("sing-box"):match("[^v]+") local version_ge_1_11_0 = api.compare_versions(local_version, ">=", "1.11.0") local version_ge_1_12_0 = api.compare_versions(local_version, ">=", "1.12.0") 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(0, "!!! Note: Geo rules cannot be used if the Geoview component is missing or the version is too low.") 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" if not fs.access(output_srs_file) then local cmd = string.format("geoview -type %s -action convert -input '%s' -list '%s' -output '%s' -lowmem=true", 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(0, string.format(" - %s:%s convert to 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(0, "V2ray/Xray Geo convert to Sing-Box rule-set:") 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 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 or nil record_fragment = proxy_table.record_fragment 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 } else if proxy_tag then node.detour = proxy_tag end end 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_strategy = node.domain_strategy, detour = node.detour, } local tls = nil 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, -- Do not send the server name in ClientHello. server_name = node.tls_serverName, -- Used to verify the hostname on the returned certificate, unless the settings are insecure. It is also included in ClientHello to support virtual hosts, unless it is an IP address. insecure = (node.tls_allowInsecure == "1") and true or false, -- Accepts any server certificate. alpn = alpn, -- A list of supported application layer protocols, arranged in order of priority. If both peers support ALPN, the protocol selected will be one of these protocols; otherwise, the connection will fail. --max_version = "1.3", fragment = fragment, record_fragment = record_fragment, ech = { enabled = (node.ech == "1") and true or false, config = node.ech_config and split(node.ech_config:gsub("\\n", "\n"), "\n") or {}, pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false, dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false }, utls = { enabled = (node.utls == "1" or node.reality == "1") and true or false, fingerprint = node.fingerprint or "chrome" }, reality = { enabled = (node.reality == "1") and true or false, public_key = node.reality_publicKey, short_id = node.reality_shortId } } 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 -- Simulate X-ray Raw (TCP) transmission 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 is not enforced. If TLS is not configured, plain HTTP 1.1 will be used. 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 is not enforced. If TLS is not configured, plain HTTP 1.1 will be used. 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 -- For compatibility with Xray-core, set it to Sec-WebSocket-Protocol. It needs to be consistent with the server. } 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" } -- There is no additional encryption support: it's essentially re-encryption. And Xray-core is incompatible with v2ray-core here. 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 == "shadowsocksr" then protocol_table = { method = node.method or nil, password = node.password or "", obfs = node.ssr_obfs, obfs_param = node.ssr_obfs_param, protocol = node.ssr_protocol, protocol_param = node.ssr_protocol_param, } 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 packet encoding. (Empty): Disabled. packetaddr: Supported by v2ray 5+. xudp: Supported by 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 packet encoding. (Empty): Disabled. packetaddr: Supported by v2ray 5+. xudp: Supported by 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.bin.b64decode(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 = (node.wireguard_system_interface == "1") and true or false, interface_name = node.wireguard_interface_name, 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 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 "30s"):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), recv_window = tonumber(node.hysteria_recv_window), disable_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false, tls = { enabled = true, server_name = node.tls_serverName, insecure = (node.tls_allowInsecure == "1") and true or false, fragment = fragment, record_fragment = record_fragment, alpn = (node.hysteria_alpn and node.hysteria_alpn ~= "") and { node.hysteria_alpn } or nil, ech = { enabled = (node.ech == "1") and true or false, config = node.ech_config and split(node.ech_config:gsub("\\n", "\n"), "\n") or {}, pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false, dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false } } } 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 = node.tuic_heartbeat .. "s", tls = { enabled = true, server_name = node.tls_serverName, insecure = (node.tls_allowInsecure == "1") and true or false, fragment = fragment, record_fragment = record_fragment, alpn = (node.tuic_alpn and node.tuic_alpn ~= "") and { node.tuic_alpn } or nil, ech = { enabled = (node.ech == "1") and true or false, config = node.ech_config and split(node.ech_config:gsub("\\n", "\n"), "\n") or {}, pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false, dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false } } } 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 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.hysteria2_hop_interval or "30s"):match("^%d+")) return (v and v >= 5) and (v .. "s") or "30s" end)(), 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 = { type = node.hysteria2_obfs_type, password = node.hysteria2_obfs_password }, password = node.hysteria2_auth_password or nil, tls = { enabled = true, server_name = node.tls_serverName, insecure = (node.tls_allowInsecure == "1") and true or false, fragment = fragment, record_fragment = record_fragment, ech = { enabled = (node.ech == "1") and true or false, config = node.ech_config and split(node.ech_config:gsub("\\n", "\n"), "\n") or {}, pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false, dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false } } } 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 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, } 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 split(node.ech_key:gsub("\\n", "\n"), "\n") or {}, pq_signature_schemes_enabled = (node.pq_signature_schemes_enabled == "1") and true or false, dynamic_record_sizing_disabled = (node.dynamic_record_sizing_disabled == "1") and true or false, } 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 -- For compatibility with Xray-core, set it to Sec-WebSocket-Protocol. It needs to be consistent with the server. } 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" } -- There is no additional encryption support: it's essentially re-encryption. And Xray-core is incompatible with v2ray-core here. 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 tls.alpn = (node.hysteria_alpn and node.hysteria_alpn ~= "") and { node.hysteria_alpn } or nil 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, recv_window_client = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil, max_conn_client = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil, disable_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false, tls = tls } end if node.protocol == "tuic" then tls.alpn = (node.tuic_alpn and node.tuic_alpn ~= "") and { node.tuic_alpn } or nil protocol_table = { users = { { name = "user1", uuid = node.uuid, password = node.password } }, congestion_control = node.tuic_congestion_control or "cubic", zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1") and true or false, heartbeat = node.tuic_heartbeat .. "s", tls = tls } 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 = { type = node.hysteria2_obfs_type, password = node.hysteria2_obfs_password }, 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 } 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("passwall2", 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.passwall2.util_sing-box").gen_outbound(nil, outbound_node_t, "outbound") end if outbound then route.final = "outbound" 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, }, 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 redir_port = var["-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_udp_server = var["-direct_dns_udp_server"] local direct_dns_udp_port = var["-direct_dns_udp_port"] local direct_dns_query_strategy = var["-direct_dns_query_strategy"] local direct_ipset = var["-direct_ipset"] local direct_nftset = var["-direct_nftset"] 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_url = var["-remote_dns_doh_url"] local remote_dns_doh_host = var["-remote_dns_doh_host"] local remote_dns_doh_ip = var["-remote_dns_doh_ip"] local remote_dns_doh_port = var["-remote_dns_doh_port"] local remote_dns_detour = var["-remote_dns_detour"] local remote_dns_query_strategy = var["-remote_dns_query_strategy"] local remote_dns_fake = var["-remote_dns_fake"] local remote_dns_client_ip = var["-remote_dns_client_ip"] local dns_cache = var["-dns_cache"] local tags = var["-tags"] local no_run = var["-no_run"] local dns_domain_rules = {} local dns = nil local inbounds = {} local outbounds = {} local rule_set_table = {} local COMMON = {} local CACHE_TEXT_FILE = CACHE_PATH .. "/cache_" .. flag .. ".txt" 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 local node = nil if node_id then node = uci:get_all(appname, node_id) end if local_socks_port then local inbound = { type = "socks", tag = "socks-in", listen = local_socks_address, listen_port = tonumber(local_socks_port), sniff = true } 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_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 redir_port then local inbound_tproxy = { type = "tproxy", tag = "tproxy", listen = "::", listen_port = tonumber(redir_port), sniff = true, sniff_override_destination = (singbox_settings.sniff_override_destination == "1") and true or false } if tcp_proxy_way ~= "tproxy" then local inbound = { type = "redirect", tag = "redirect_tcp", listen = "::", listen_port = tonumber(redir_port), sniff = true, sniff_override_destination = (singbox_settings.sniff_override_destination == "1") and true or false, } table.insert(inbounds, inbound) inbound_tproxy.tag = "tproxy_udp" inbound_tproxy.network = "udp" end table.insert(inbounds, inbound_tproxy) end if node then if server_host and server_port then node.address = server_host node.port = server_port end local function gen_urltest(_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 urltest_tag end end -- new urltest local ut_nodes = _node.urltest_node local valid_nodes = {} for i = 1, #ut_nodes 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 ut_node if ut_node_id:find("Socks_") then local socks_id = ut_node_id:sub(1 + #"Socks_") local socks_node = uci:get_all(appname, socks_id) or nil if socks_node then ut_node = { type = "sing-box", protocol = "socks", address = "127.0.0.1", port = socks_node.port, uot = "1", remarks = "Socks_" .. socks_node.port } end else ut_node = uci:get_all(appname, ut_node_id) end if ut_node then local outbound = gen_outbound(flag, ut_node, 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 outbound then outbound.tag = outbound.tag .. ":" .. ut_node.remarks table.insert(outbounds, outbound) valid_nodes[#valid_nodes + 1] = outbound.tag end 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 } table.insert(outbounds, outbound) return urltest_tag end local function set_outbound_detour(node, outbound, outbounds_table, shunt_rule_name) 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 = uci:get_all(appname, node.preproxy_node) if preproxy_node then local preproxy_outbound = gen_outbound(nil, preproxy_node) if preproxy_outbound then preproxy_outbound.tag = preproxy_node[".name"] .. ":" .. preproxy_node.remarks outbound.tag = preproxy_outbound.tag .. " -> " .. outbound.tag outbound.detour = preproxy_outbound.tag last_insert_outbound = preproxy_outbound default_outTag = outbound.tag end end end end if node.chain_proxy == "2" and node.to_node then local to_node = uci:get_all(appname, node.to_node) if to_node then local to_outbound = gen_outbound(nil, to_node) if to_outbound then if shunt_rule_name then to_outbound.tag = outbound.tag outbound.tag = node[".name"] else to_outbound.tag = outbound.tag .. " -> " .. to_outbound.tag end to_outbound.detour = outbound.tag table.insert(outbounds_table, to_outbound) default_outTag = to_outbound.tag end end end return default_outTag, last_insert_outbound end if node.protocol == "_shunt" then local rules = {} local preproxy_rule_name = node.preproxy_enabled == "1" and "main" or nil local preproxy_tag = preproxy_rule_name local preproxy_node_id = preproxy_rule_name and node["main_node"] or nil inner_fakedns = node.fakedns or "0" local function gen_shunt_node(rule_name, _node_id) if not rule_name then return nil end if not _node_id then _node_id = node[rule_name] end local rule_outboundTag if _node_id == "_direct" then rule_outboundTag = "direct" elseif _node_id == "_blackhole" then rule_outboundTag = "block" elseif _node_id == "_default" and rule_name ~= "default" then rule_outboundTag = "default" elseif _node_id and _node_id:find("Socks_") then local socks_id = _node_id:sub(1 + #"Socks_") local socks_node = uci:get_all(appname, socks_id) or nil if socks_node then local _node = { type = "sing-box", protocol = "socks", address = "127.0.0.1", port = socks_node.port, uot = "1", } local _outbound = gen_outbound(flag, _node, rule_name) if _outbound then table.insert(outbounds, _outbound) rule_outboundTag = _outbound.tag end end elseif _node_id then local _node = uci:get_all(appname, _node_id) if not _node then return nil end if api.is_normal_node(_node) then local use_proxy = preproxy_tag and node[rule_name .. "_proxy_tag"] == preproxy_rule_name and _node_id ~= preproxy_node_id local copied_outbound for index, value in ipairs(outbounds) do if value["_id"] == _node_id and value["_flag_proxy_tag"] == (use_proxy and preproxy_tag or nil) then copied_outbound = api.clone(value) break end end if copied_outbound then copied_outbound.tag = rule_name .. ":" .. _node.remarks table.insert(outbounds, copied_outbound) rule_outboundTag = copied_outbound.tag else if use_proxy then local pre_proxy = nil if _node.type ~= "sing-box" then pre_proxy = true end if pre_proxy then local new_port = api.get_new_port() table.insert(inbounds, { type = "direct", tag = "proxy_" .. rule_name, listen = "127.0.0.1", listen_port = new_port, override_address = _node.address, override_port = tonumber(_node.port), }) if _node.tls_serverName == nil then _node.tls_serverName = _node.address end _node.address = "127.0.0.1" _node.port = new_port table.insert(rules, 1, { inbound = {"proxy_" .. rule_name}, outbound = preproxy_tag, }) end end local proxy_table = { tag = use_proxy and preproxy_tag or nil, run_socks_instance = not no_run } if not proxy_table.tag then if singbox_settings.fragment == "1" then proxy_table.fragment = true end if singbox_settings.record_fragment == "1" then proxy_table.record_fragment = true end end local _outbound = gen_outbound(flag, _node, rule_name, proxy_table) if _outbound then _outbound.tag = _outbound.tag .. ":" .. _node.remarks rule_outboundTag, last_insert_outbound = set_outbound_detour(_node, _outbound, outbounds, rule_name) table.insert(outbounds, _outbound) if last_insert_outbound then table.insert(outbounds, last_insert_outbound) end end end elseif _node.protocol == "_urltest" then rule_outboundTag = gen_urltest(_node) elseif _node.protocol == "_iface" then if _node.iface then local _outbound = { type = "direct", tag = rule_name .. ":" .. _node.remarks, bind_interface = _node.iface, routing_mark = 255, } table.insert(outbounds, _outbound) rule_outboundTag = _outbound.tag sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, _node.iface)) end end end return rule_outboundTag end if preproxy_tag and preproxy_node_id then local preproxy_outboundTag = gen_shunt_node(preproxy_rule_name, preproxy_node_id) if preproxy_outboundTag then preproxy_tag = preproxy_outboundTag end end --default_node local default_node_id = node.default_node or "_direct" COMMON.default_outbound_tag = gen_shunt_node("default", default_node_id) --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 redir_port then if tcp_proxy_way == "tproxy" then table.insert(inboundTag, "tproxy") else table.insert(inboundTag, "redirect_tcp") table.insert(inboundTag, "tproxy_udp") end end end if e["inbound"]:find("socks") then if local_socks_port then table.insert(inboundTag, "socks-in") end end end local rule = { inbound = inboundTag, outbound = outboundTag, protocol = protocols } 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 source_is_private = false string.gsub(e.source, '[^' .. " " .. ']+', function(w) if w:find("geoip:") == 1 then local _geoip = w:sub(1 + #"geoip:") if _geoip == "private" then source_is_private = true end else table.insert(source_ip_cidr, w) end end) rule.source_ip_cidr = #source_ip_cidr > 0 and source_ip_cidr or nil rule.source_ip_is_private = source_is_private and true or nil 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 = { 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) if w:find("#") == 1 then return end if w:find("geosite:") == 1 then local _geosite = w:sub(1 + #"geosite:") 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 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) if w:find("#") == 1 then return end if w:find("geoip:") == 1 then local _geoip = w:sub(1 + #"geoip:") 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 rule.invert = e.invert == "1" and true or nil table.insert(rules, rule) end end) for index, value in ipairs(rules) do table.insert(route.rules, rules[index]) end elseif node.protocol == "_urltest" then if node.urltest_node then COMMON.default_outbound_tag = gen_urltest(node) end elseif node.protocol == "_iface" then if node.iface then local outbound = { type = "direct", tag = node.remarks or node_id, bind_interface = node.iface, routing_mark = 255, } table.insert(outbounds, outbound) COMMON.default_outbound_tag = outbound.tag sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface)) end else local outbound = gen_outbound(flag, node, nil, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run }) if outbound then outbound.tag = outbound.tag .. ":" .. node.remarks COMMON.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 end end end if COMMON.default_outbound_tag then 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, -- Disable DNS cache expiration. independent_cache = false, -- Make each DNS server's cache independent for specific purposes. If enabled, it will slightly reduce performance. reverse_mapping = true, -- After responding to a DNS query, a reverse mapping of the IP address is stored to provide the domain name for routing purposes. fakeip = nil, } table.insert(dns.servers, { tag = "block", address = "rcode://success", }) local 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", address_strategy = "prefer_ipv4", strategy = remote_strategy, address_resolver = "direct", detour = COMMON.default_outbound_tag, client_subnet = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil, } if remote_dns_detour == "direct" then remote_server.detour = "direct" end if remote_dns_udp_server then local server_port = tonumber(remote_dns_udp_port) or 53 remote_server.address = "udp://" .. remote_dns_udp_server .. ":" .. server_port end if remote_dns_tcp_server then local server_port = tonumber(remote_dns_tcp_port) or 53 remote_server.address = "tcp://" .. remote_dns_tcp_server .. ":" .. server_port end if remote_dns_doh_url then remote_server.address = remote_dns_doh_url end if remote_server.address then table.insert(dns.servers, remote_server) end local fakedns_tag = "remote_fakeip" if remote_dns_fake or inner_fakedns == "1" then dns.fakeip = { enabled = true, inet4_range = "198.18.0.0/16", inet6_range = "fc00::/18", } table.insert(dns.servers, { tag = fakedns_tag, address = "fakeip", strategy = remote_strategy, }) if not experimental then experimental = {} end experimental.cache_file = { enabled = true, store_fakeip = true, path = CACHE_PATH .. "/singbox_" .. flag .. ".db" } end if direct_dns_udp_server then local domain = {} local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u') string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w) table.insert(domain, w) end) if #domain > 0 then table.insert(dns_domain_rules, 1, { outboundTag = "direct", domain = domain }) end local 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 port = tonumber(direct_dns_udp_port) or 53 table.insert(dns.servers, { tag = "direct", address = "udp://" .. direct_dns_udp_server .. ":" .. port, address_strategy = "prefer_ipv6", strategy = direct_strategy, detour = "direct", }) end local default_dns_flag = "remote" if node_id and redir_port then local node = uci:get_all(appname, 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 dns.final = default_dns_flag -- DNS in order of shunt 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 = { 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, disable_cache = false, invert = value.invert, } if value.outboundTag ~= "block" and value.outboundTag ~= "direct" then dns_rule.server = "remote" dns_rule.rewrite_ttl = 30 if value.outboundTag ~= COMMON.default_outbound_tag and remote_server.address and remote_dns_detour ~= "direct" then local remote_dns_server = api.clone(remote_server) remote_dns_server.tag = value.outboundTag remote_dns_server.detour = 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 fakedns_dns_rule.client_subnet = nil table.insert(dns.rules, fakedns_dns_rule) end end table.insert(dns.rules, dns_rule) end end end if remote_dns_fake and default_dns_flag == "remote" then -- When default is not direct and enable fakedns, default DNS use FakeDNS. local fakedns_dns_rule = { query_type = { "A", "AAAA" }, server = fakedns_tag, disable_cache = true } table.insert(dns.rules, fakedns_dns_rule) end table.insert(inbounds, { type = "direct", tag = "dns-in", listen = "127.0.0.1", listen_port = tonumber(dns_listen_port), sniff = true, }) table.insert(outbounds, { type = "dns", tag = "dns-out", }) table.insert(route.rules, 1, { protocol = "dns", inbound = { "dns-in" }, outbound = "dns-out" }) local content = flag .. node_id .. jsonc.stringify(route.rules) if api.cacheFileCompareToLogic(CACHE_TEXT_FILE, content) == false then --clear ipset/nftset if direct_ipset then string.gsub(direct_ipset, '[^' .. "," .. ']+', function(w) sys.call("ipset -q -F " .. w) end) local ipset_prefix_name = "passwall2_" .. node_id .. "_" local ipset_list = sys.exec("ipset list | grep 'Name: ' | grep '" .. ipset_prefix_name .. "' | awk '{print $2}'") string.gsub(ipset_list, '[^' .. "\r\n" .. ']+', function(w) sys.call("ipset -q -F " .. w) end) end if direct_nftset then string.gsub(direct_nftset, '[^' .. "," .. ']+', function(w) local split = api.split(w, "#") if #split > 3 then local ip_type = split[1] local family = split[2] local table_name = split[3] local set_name = split[4] sys.call(string.format("nft flush set %s %s %s 2>/dev/null", family, table_name, set_name)) end end) local family = "inet" local table_name = "passwall2" local nftset_prefix_name = "passwall2_" .. node_id .. "_" local nftset_list = sys.exec("nft -a list sets | grep -E '" .. nftset_prefix_name .. "' | awk -F 'set ' '{print $2}' | awk '{print $1}'") string.gsub(nftset_list, '[^' .. "\r\n" .. ']+', function(w) sys.call(string.format("nft flush set %s %s %s 2>/dev/null", family, table_name, w)) end) end end 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, inbounds = inbounds, outbounds = outbounds, route = route, experimental = experimental, } table.insert(outbounds, { type = "direct", tag = "direct", routing_mark = 255, domain_strategy = "prefer_ipv6", }) table.insert(outbounds, { type = "block", tag = "block" }) 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 and not no_run then sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list")) end for k, v in pairs(config.outbounds[index]) do if k:find("_") == 1 then config.outbounds[index][k] = nil end end end if version_ge_1_11_0 then -- Migrate logics -- https://sing-box.sagernet.org/migration/ 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_strategy = value.domain_strategy, detour = value.detour } endpoints[#endpoints + 1] = endpoint table.remove(config.outbounds, i) end if value.type == "block" or value.type == "dns" then -- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions table.remove(config.outbounds, i) end end if #endpoints > 0 then config.endpoints = endpoints end -- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions for i = #config.route.rules, 1, -1 do local value = config.route.rules[i] if value.outbound == "block" then value.action = "reject" value.outbound = nil elseif value.outbound == "dns-out" then value.action = "hijack-dns" value.outbound = nil else value.action = "route" end end -- https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions for i = #config.inbounds, 1, -1 do local value = config.inbounds[i] if value.sniff == true then table.insert(config.route.rules, 1, { inbound = value.tag, action = "sniff" }) value.sniff = nil value.sniff_override_destination = nil end if value.domain_strategy then table.insert(config.route.rules, 1, { inbound = value.tag, action = "resolve", strategy = value.domain_strategy, --server = "" }) value.domain_strategy = nil end end if config.route.final == "block" then config.route.final = nil table.insert(config.route.rules, { action = "reject" }) 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 print(func(api.get_function_args(arg))) if (next(GEO_VAR.SITE_TAGS) or next(GEO_VAR.IP_TAGS)) and not no_run then convert_geofile() end end end