module("luci.passwall2.util_xray", 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 new_port

local function get_new_port()
	if new_port then
		new_port = tonumber(sys.exec(string.format("echo -n $(/usr/share/%s/app.sh get_new_port %s tcp)", appname, new_port + 1)))
	else
		new_port = tonumber(sys.exec(string.format("echo -n $(/usr/share/%s/app.sh get_new_port auto tcp)", appname)))
	end
	return new_port
end

local function get_noise_packets()
	local noises = {}
	uci:foreach(appname, "xray_noise_packets", function(n)
		local noise = (n.enabled == "1") and {
			type = n.type,
			packet = n.packet,
			delay = string.find(n.delay, "-") and n.delay or tonumber(n.delay)
		} or nil
		table.insert(noises, noise)
	end)
	if #noises == 0 then noises = nil end
	return noises
end

local function get_domain_excluded()
	local path = string.format("/usr/share/%s/domains_excluded", appname)
	local content = fs.readfile(path)
	if not content then return nil end
	local hosts = {}
	string.gsub(content, '[^' .. "\n" .. ']+', function(w)
		local s = api.trim(w)
		if s == "" then return end
		if s:find("#") and s:find("#") == 1 then return end
		if not s:find("#") or s:find("#") ~= 1 then table.insert(hosts, s) end
	end)
	if #hosts == 0 then hosts = nil end
	return hosts
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 noise = 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
			noise = proxy_table.noise or nil
			run_socks_instance = proxy_table.run_socks_instance
		end

		if node.type ~= "Xray" then
			local relay_port = node.port
			new_port = get_new_port()
			local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
			if tag and node_id and tag ~= 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 = {}
			node.protocol = "socks"
			node.transport = "tcp"
			node.address = "127.0.0.1"
			node.port = new_port
			node.stream_security = "none"
		else
			if node.flow == "xtls-rprx-vision" then
			else
				if proxy_tag then
					node.proxySettings = {
						tag = proxy_tag,
						transportLayer = true
					}
				end
			end
		end
		
		if node.type == "Xray" then
			if node.tls and node.tls == "1" then
				node.stream_security = "tls"
				if node.reality and node.reality == "1" then
					node.stream_security = "reality"
				end
			end
		end

		if node.protocol == "wireguard" and 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

		result = {
			_id = node_id,
			_flag = flag,
			_flag_proxy_tag = proxy_tag,
			tag = tag,
			proxySettings = node.proxySettings or nil,
			protocol = node.protocol,
			mux = {
				enabled = (node.mux == "1" or node.xmux == "1") and true or false,
				concurrency = (node.mux == "1" and ((node.mux_concurrency) and tonumber(node.mux_concurrency) or 8)) or ((node.xmux == "1") and -1) or nil,
				xudpConcurrency = (node.xmux == "1" and ((node.xudp_concurrency) and tonumber(node.xudp_concurrency) or 8)) or nil
			} or nil,
			-- 底层传输配置
			streamSettings = (node.streamSettings or node.protocol == "vmess" or node.protocol == "vless" or node.protocol == "socks" or node.protocol == "shadowsocks" or node.protocol == "trojan") and {
				sockopt = {
					mark = 255,
					tcpMptcp = (node.tcpMptcp == "1") and true or nil,
					tcpNoDelay = (node.tcpNoDelay == "1") and true or nil,
					dialerProxy = (fragment or noise) and "dialerproxy" or nil
				},
				network = node.transport,
				security = node.stream_security,
				tlsSettings = (node.stream_security == "tls") and {
					serverName = node.tls_serverName,
					allowInsecure = (node.tls_allowInsecure == "1") and true or false,
					fingerprint = (node.type == "Xray" and node.utls == "1" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or nil,
					echConfigList = (node.ech == "1") and node.ech_config or nil
				} or nil,
				realitySettings = (node.stream_security == "reality") and {
					serverName = node.tls_serverName,
					publicKey = node.reality_publicKey,
					shortId = node.reality_shortId or "",
					spiderX = node.reality_spiderX or "/",
					fingerprint = (node.type == "Xray" and node.fingerprint and node.fingerprint ~= "") and node.fingerprint or "chrome",
					mldsa65Verify = (node.use_mldsa65Verify == "1") and node.reality_mldsa65Verify or nil
				} or nil,
				rawSettings = ((node.transport == "raw" or node.transport == "tcp") and node.protocol ~= "socks" and (node.tcp_guise and node.tcp_guise ~= "none")) and {
					header = {
						type = node.tcp_guise,
						request = (node.tcp_guise == "http") and {
							path = node.tcp_guise_http_path or {"/"},
							headers = {
								Host = node.tcp_guise_http_host or {}
							}
						} or nil
					}
				} or nil,
				kcpSettings = (node.transport == "mkcp") and {
					mtu = tonumber(node.mkcp_mtu),
					tti = tonumber(node.mkcp_tti),
					uplinkCapacity = tonumber(node.mkcp_uplinkCapacity),
					downlinkCapacity = tonumber(node.mkcp_downlinkCapacity),
					congestion = (node.mkcp_congestion == "1") and true or false,
					readBufferSize = tonumber(node.mkcp_readBufferSize),
					writeBufferSize = tonumber(node.mkcp_writeBufferSize),
					seed = (node.mkcp_seed and node.mkcp_seed ~= "") and node.mkcp_seed or nil,
					header = {
						type = node.mkcp_guise,
						domain = node.mkcp_domain
					}
				} or nil,
				wsSettings = (node.transport == "ws") and {
					path = node.ws_path or "/",
					host = node.ws_host or nil,
					maxEarlyData = tonumber(node.ws_maxEarlyData) or nil,
					earlyDataHeaderName = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil,
					heartbeatPeriod = tonumber(node.ws_heartbeatPeriod) or nil
				} or nil,
				dsSettings = (node.transport == "ds") and
					{path = node.ds_path} or nil,
				quicSettings = (node.transport == "quic") and {
					security = node.quic_security,
					key = node.quic_key,
					header = {type = node.quic_guise}
				} or nil,
				grpcSettings = (node.transport == "grpc") and {
					serviceName = node.grpc_serviceName,
					multiMode = (node.grpc_mode == "multi") and true or nil,
					idle_timeout = tonumber(node.grpc_idle_timeout) or nil,
					health_check_timeout = tonumber(node.grpc_health_check_timeout) or nil,
					permit_without_stream = (node.grpc_permit_without_stream == "1") and true or nil,
					initial_windows_size = tonumber(node.grpc_initial_windows_size) or nil
				} or nil,
				httpupgradeSettings = (node.transport == "httpupgrade") and {
					path = node.httpupgrade_path or "/",
					host = node.httpupgrade_host
				} or nil,
				xhttpSettings = (node.transport == "xhttp" or node.transport == "splithttp") and {
					mode = node.xhttp_mode or "auto",
					path = node.xhttp_path or node.splithttp_path or "/",
					host = node.xhttp_host or node.splithttp_host,
					-- 如果包含 "extra" 节，取 "extra" 内的内容，否则直接赋值给 extra
					extra = node.xhttp_extra and (function()
						local success, parsed = pcall(jsonc.parse, node.xhttp_extra)
							if success then
								return parsed.extra or parsed
							else
								return nil
							end
						end)() or nil
				} or nil,
			} or nil,
			settings = {
				vnext = (node.protocol == "vmess" or node.protocol == "vless") and {
					{
						address = node.address,
						port = tonumber(node.port),
						users = {
							{
								id = node.uuid,
								level = 0,
								security = (node.protocol == "vmess") and node.security or nil,
								encryption = node.encryption or "none",
								flow = (node.protocol == "vless" and node.tls == "1" and (node.transport == "raw" or node.transport == "tcp") and node.flow and node.flow ~= "") and node.flow or nil

							}
						}
					}
				} or nil,
				servers = (node.protocol == "socks" or node.protocol == "http" or node.protocol == "shadowsocks" or node.protocol == "trojan") and {
					{
						address = node.address,
						port = tonumber(node.port),
						method = (node.method == "chacha20-ietf-poly1305" and "chacha20-poly1305") or
							(node.method == "xchacha20-ietf-poly1305" and "xchacha20-poly1305") or
							(node.method ~= "" and node.method) or nil,
						ivCheck = (node.protocol == "shadowsocks") and node.iv_check == "1" or nil,
						uot = (node.protocol == "shadowsocks") and node.uot == "1" or nil,
						password = node.password or "",
						users = (node.username and node.password) and {
							{
								user = node.username,
								pass = node.password
							}
						} or nil
					}
				} or nil,
				address = (node.protocol == "wireguard" and node.wireguard_local_address) and node.wireguard_local_address or nil,
				secretKey = (node.protocol == "wireguard") and node.wireguard_secret_key or nil,
				peers = (node.protocol == "wireguard") and {
					{
						publicKey = node.wireguard_public_key,
						endpoint = node.address .. ":" .. node.port,
						preSharedKey = node.wireguard_preSharedKey,
						keepAlive = node.wireguard_keepAlive and tonumber(node.wireguard_keepAlive) or nil
					}
				} or nil,
				mtu = (node.protocol == "wireguard" and node.wireguard_mtu) and tonumber(node.wireguard_mtu) or nil,
				reserved = (node.protocol == "wireguard" and node.wireguard_reserved) and node.wireguard_reserved or nil
			}
		}

		if node.protocol == "wireguard" then
			result.settings.kernelMode = false
		end

		local alpn = {}
		if node.alpn and node.alpn ~= "default" then
			string.gsub(node.alpn, '[^' .. "," .. ']+', function(w)
				table.insert(alpn, w)
			end)
		end
		if alpn and #alpn > 0 then
			if result.streamSettings.tlsSettings then
				result.streamSettings.tlsSettings.alpn = alpn
			end
		end

	end
	return result
end

function gen_config_server(node)
	local settings = nil
	local routing = nil
	local outbounds = {
		{protocol = "freedom", tag = "direct"}, {protocol = "blackhole", tag = "blocked"}
	}

	if node.protocol == "vmess" or node.protocol == "vless" then
		if node.uuid then
			local clients = {}
			for i = 1, #node.uuid do
				clients[i] = {
					id = node.uuid[i],
					flow = ("vless" == node.protocol and "1" == node.tls and "raw" == node.transport and node.flow and node.flow ~= "") and node.flow or nil
				}
			end
			settings = {
				clients = clients,
				decryption = node.decryption or "none"
			}
		end
	elseif node.protocol == "socks" then
		settings = {
			udp = ("1" == node.udp_forward) and true or false,
			auth = ("1" == node.auth) and "password" or "noauth",
			accounts = ("1" == node.auth) and {
				{
					user = node.username,
					pass = node.password
				}
			} or nil
		}
	elseif node.protocol == "http" then
		settings = {
			allowTransparent = false,
			accounts = ("1" == node.auth) and {
				{
					user = node.username,
					pass = node.password
				}
			} or nil
		}
		node.transport = "tcp"
		node.tcp_guise = "none"
	elseif node.protocol == "shadowsocks" then
		settings = {
			method = node.method,
			password = node.password,
			ivCheck = ("1" == node.iv_check) and true or false,
			network = node.ss_network or "TCP,UDP"
		}
	elseif node.protocol == "trojan" then
		if node.uuid then
			local clients = {}
			for i = 1, #node.uuid do
				clients[i] = {
					password = node.uuid[i]
				}
			end
			settings = {
				clients = clients
			}
		end
	elseif node.protocol == "dokodemo-door" then
		settings = {
			network = node.d_protocol,
			address = node.d_address,
			port = tonumber(node.d_port)
		}
	end

	if node.fallback and node.fallback == "1" then
		local fallbacks = {}
		for i = 1, #node.fallback_list do
			local fallbackStr = node.fallback_list[i]
			if fallbackStr then
				local tmp = {}
				string.gsub(fallbackStr, '[^,]+', function(w)
					table.insert(tmp, w)
				end)
				local dest = tmp[1] or ""
				local path = tmp[2]
				local xver = tonumber(tmp[3])
				if not dest:find("%.") then
					dest = tonumber(dest)
				end
				fallbacks[i] = {
					path = path,
					dest = dest,
					xver = xver
				}
			end
		end
		settings.fallbacks = fallbacks
	end

	routing = {
		domainStrategy = "IPOnDemand",
		rules = {
			{
				ip = {"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"},
				outboundTag = (node.accept_lan == nil or node.accept_lan == "0") and "blocked" or "direct"
			}
		}
	}

	if node.outbound_node then
		local outbound = nil
		if node.outbound_node == "_iface" and node.outbound_node_iface then
			outbound = {
				protocol = "freedom",
				tag = "outbound",
				streamSettings = {
					sockopt = {
						mark = 255,
						interface = node.outbound_node_iface
					}
				}
			}
			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("_", ""),
					transport = "tcp",
					address = node.outbound_node_address,
					port = 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_xray").gen_outbound(nil, outbound_node_t, "outbound")
		end
		if outbound then
			table.insert(outbounds, 1, outbound)
		end
	end

	local config = {
		log = {
			loglevel = ("1" == node.log) and node.loglevel or "none"
		},
		-- 传入连接
		inbounds = {
			{
				listen = (node.bind_local == "1") and "127.0.0.1" or nil,
				port = tonumber(node.port),
				protocol = node.protocol,
				settings = settings,
				streamSettings = {
					network = node.transport,
					security = "none",
					tlsSettings = ("1" == node.tls) and {
						disableSystemRoot = false,
						certificates = {
							{
								certificateFile = node.tls_certificateFile,
								keyFile = node.tls_keyFile
							}
						},
						echServerKeys = (node.ech == "1") and node.ech_key or nil
					} or nil,
					rawSettings = (node.transport == "raw" or node.transport == "tcp") and {
						header = {
							type = node.tcp_guise,
							request = (node.tcp_guise == "http") and {
								path = node.tcp_guise_http_path or {"/"},
								headers = {
									Host = node.tcp_guise_http_host or {}
								}
							} or nil
						}
					} or nil,
					kcpSettings = (node.transport == "mkcp") and {
						mtu = tonumber(node.mkcp_mtu),
						tti = tonumber(node.mkcp_tti),
						uplinkCapacity = tonumber(node.mkcp_uplinkCapacity),
						downlinkCapacity = tonumber(node.mkcp_downlinkCapacity),
						congestion = (node.mkcp_congestion == "1") and true or false,
						readBufferSize = tonumber(node.mkcp_readBufferSize),
						writeBufferSize = tonumber(node.mkcp_writeBufferSize),
						seed = (node.mkcp_seed and node.mkcp_seed ~= "") and node.mkcp_seed or nil,
						header = {
							type = node.mkcp_guise,
							domain = node.mkcp_domain
						}
					} or nil,
					wsSettings = (node.transport == "ws") and {
						host = node.ws_host or nil,
						path = node.ws_path
					} or nil,
					dsSettings = (node.transport == "ds") and {
						path = node.ds_path
					} or nil,
					quicSettings = (node.transport == "quic") and {
						security = node.quic_security,
						key = node.quic_key,
						header = {type = node.quic_guise}
					} or nil,
					grpcSettings = (node.transport == "grpc") and {
						serviceName = node.grpc_serviceName
					} or nil,
					httpupgradeSettings = (node.transport == "httpupgrade") and {
						path = node.httpupgrade_path or "/",
						host = node.httpupgrade_host
					} or nil,
					xhttpSettings = (node.transport == "xhttp") and {
						path = node.xhttp_path or "/",
						host = node.xhttp_host,
						maxUploadSize = node.xhttp_maxuploadsize,
						maxConcurrentUploads = node.xhttp_maxconcurrentuploads
					} or nil,
					sockopt = {
						acceptProxyProtocol = (node.acceptProxyProtocol and node.acceptProxyProtocol == "1") and true or false
					}
				}
			}
		},
		-- 传出连接
		outbounds = outbounds,
		routing = routing
	}

	local alpn = {}
	if node.alpn then
		string.gsub(node.alpn, '[^' .. "," .. ']+', function(w)
			table.insert(alpn, w)
		end)
	end
	if alpn and #alpn > 0 then
		if config.inbounds[1].streamSettings.tlsSettings then
			config.inbounds[1].streamSettings.tlsSettings.alpn = alpn
		end
	end

	if "1" == node.tls then
		config.inbounds[1].streamSettings.security = "tls"
		if "1" == node.reality then
			config.inbounds[1].streamSettings.tlsSettings = nil
			config.inbounds[1].streamSettings.security = "reality"
			config.inbounds[1].streamSettings.realitySettings = {
				show = false,
				dest = node.reality_dest,
				serverNames = node.reality_serverNames or {},
				privateKey = node.reality_private_key,
				shortIds = node.reality_shortId or "",
				mldsa65Seed = (node.use_mldsa65Seed == "1") and node.reality_mldsa65Seed or nil
			} or nil
		end
	end

	return config
end

function gen_config(var)
	local flag = var["-flag"]
	local loglevel = var["-loglevel"] or "warning"
	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_fake = var["-remote_dns_fake"]
	local remote_dns_query_strategy = var["-remote_dns_query_strategy"]
	local remote_dns_detour = var["-remote_dns_detour"]
	local dns_cache = var["-dns_cache"]
	local no_run = var["-no_run"]

	local dns_domain_rules = {}
	local dns = nil
	local fakedns = nil
	local inbounds = {}
	local outbounds = {}
	local routing = nil
	local burstObservatory = nil
 	local strategy = nil
	local COMMON = {}

	local CACHE_TEXT_FILE = CACHE_PATH .. "/cache_" .. flag .. ".txt"

	local xray_settings = uci:get_all(appname, "@global_xray[0]") or {}

	local node = node_id and uci:get_all(appname, node_id) or nil
	local balancers = {}
	local rules = {}

	if local_socks_port then
		local inbound = {
			tag = "socks-in",
			listen = local_socks_address,
			port = tonumber(local_socks_port),
			protocol = "socks",
			settings = {auth = "noauth", udp = true},
			sniffing = {
				enabled = xray_settings.sniffing_override_dest == "1" or node.protocol == "_shunt"
			}
		}
		if inbound.sniffing.enabled == true then
			inbound.sniffing.destOverride = {"http", "tls", "quic"}
			inbound.sniffing.routeOnly = xray_settings.sniffing_override_dest ~= "1" or nil
			inbound.sniffing.domainsExcluded = xray_settings.sniffing_override_dest == "1" and get_domain_excluded() or nil
		end
		if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then
			inbound.settings.auth = "password"
			inbound.settings.accounts = {
				{
					user = local_socks_username,
					pass = local_socks_password
				}
			}
		end
		table.insert(inbounds, inbound)
	end

	if local_http_port then
		local inbound = {
			listen = local_http_address,
			port = tonumber(local_http_port),
			protocol = "http",
			settings = {allowTransparent = false}
		}
		if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then
			inbound.settings.accounts = {
				{
					user = local_http_username,
					pass = local_http_password
				}
			}
		end
		table.insert(inbounds, inbound)
	end

	if redir_port then
		local inbound = {
			port = tonumber(redir_port),
			protocol = "dokodemo-door",
			settings = {network = "tcp,udp", followRedirect = true},
			streamSettings = {sockopt = {tproxy = "tproxy"}},
			sniffing = {
				enabled = xray_settings.sniffing_override_dest == "1" or node.protocol == "_shunt"
			}
		}
		if inbound.sniffing.enabled == true then
			inbound.sniffing.destOverride = {"http", "tls", "quic"}
			inbound.sniffing.metadataOnly = false
			inbound.sniffing.routeOnly = xray_settings.sniffing_override_dest ~= "1" or nil
			inbound.sniffing.domainsExcluded = xray_settings.sniffing_override_dest == "1" and get_domain_excluded() or nil
		end
		if remote_dns_fake then
			inbound.sniffing.enabled = true
			if not inbound.sniffing.destOverride then
				inbound.sniffing.destOverride = {"fakedns"}
				inbound.sniffing.metadataOnly = true
			else
				table.insert(inbound.sniffing.destOverride, "fakedns")
				inbound.sniffing.metadataOnly = false
			end
		end

		local tcp_inbound = api.clone(inbound)
		tcp_inbound.tag = "tcp_redir"
		tcp_inbound.settings.network = "tcp"
		tcp_inbound.streamSettings.sockopt.tproxy = tcp_proxy_way
		table.insert(inbounds, tcp_inbound)

		local udp_inbound = api.clone(inbound)
		udp_inbound.tag = "udp_redir"
		udp_inbound.settings.network = "udp"
		table.insert(inbounds, udp_inbound)
	end

	local function get_balancer_tag(_node_id)
		return "balancer-" .. _node_id
	end

	local function gen_loopback(outboundTag, dst_node_id)
		if not outboundTag then return nil end
		local inboundTag = dst_node_id and "loop-in-" .. dst_node_id or outboundTag .. "-lo"
		table.insert(outbounds, {
			protocol = "loopback",
			tag = outboundTag,
			settings = { inboundTag = inboundTag }
		})
		return inboundTag
	end

	local function gen_balancer(_node, loopback_tag)
		local balancer_id = _node[".name"]
		local balancer_tag = "balancer-" .. balancer_id
		local loopback_dst = balancer_id -- route destination for the loopback outbound
		if not loopback_tag or loopback_tag == "" then loopback_tag = balancer_id end
		-- existing balancer
		for _, v in ipairs(balancers) do
			if v.tag == balancer_tag then
				gen_loopback(loopback_tag, loopback_dst)
				return balancer_tag
			end
		end
		-- new balancer
		local blc_nodes = _node.balancing_node
		local valid_nodes = {}
		for i = 1, #blc_nodes do
			local blc_node_id = blc_nodes[i]
			local blc_node_tag = "blc-" .. blc_node_id
			local is_new_blc_node = true
			for _, outbound in ipairs(outbounds) do
				if string.sub(outbound.tag, 1, #blc_node_tag) == blc_node_tag then
					is_new_blc_node = false
					valid_nodes[#valid_nodes + 1] = outbound.tag
					break
				end
			end
			if is_new_blc_node then
				local blc_node = uci:get_all(appname, blc_node_id)
				local outbound = gen_outbound(flag, blc_node, blc_node_tag, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
				if outbound then
					outbound.tag = outbound.tag .. ":" .. blc_node.remarks
					table.insert(outbounds, outbound)
					valid_nodes[#valid_nodes + 1] = outbound.tag
				end
			end
		end
		if #valid_nodes == 0 then return nil end

		-- fallback node
		local fallback_node_tag = nil
		local fallback_node_id = _node.fallback_node
		if not fallback_node_id or fallback_node_id == "" then fallback_node_id = nil end
		if fallback_node_id then
			local is_new_node = true
			for _, outbound in ipairs(outbounds) do
				if string.sub(outbound.tag, 1, #fallback_node_id) == fallback_node_id then
					is_new_node = false
					fallback_node_tag = outbound.tag
					break
				end
			end
			if is_new_node then
				local fallback_node = uci:get_all(appname, fallback_node_id)
				if fallback_node.protocol ~= "_balancing" then
					local outbound = gen_outbound(flag, fallback_node, fallback_node_id, { fragment = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "1" or nil, run_socks_instance = not no_run })
					if outbound then
						outbound.tag = outbound.tag .. ":" .. fallback_node.remarks
						table.insert(outbounds, outbound)
						fallback_node_tag = outbound.tag
					end
				end
			end
		end
		if _node.balancingStrategy == "leastLoad" then
			strategy = {
				type = _node.balancingStrategy,
				settings = {
					expected = _node.expected and tonumber(_node.expected) and tonumber(_node.expected) or 2,
					maxRTT = "1s"
				}
			}
		else
			strategy = { type = _node.balancingStrategy or "random" }
		end
		table.insert(balancers, {
			tag = balancer_tag,
			selector = valid_nodes,
			fallbackTag = fallback_node_tag,
			strategy = strategy
		})
		if _node.balancingStrategy == "leastPing" or _node.balancingStrategy == "leastLoad" or fallback_node_tag then
			if not burstObservatory then
				burstObservatory = {
					subjectSelector = { "blc-" },
					pingConfig = {
						destination = _node.useCustomProbeUrl and _node.probeUrl or nil,
						interval = (api.format_go_time(_node.probeInterval) ~= "0s") and api.format_go_time(_node.probeInterval) or "1m",
						sampling = 3,
						timeout = "5s"
					}
				}
			end
		end
		local inbound_tag = gen_loopback(loopback_tag, loopback_dst)
		table.insert(rules, { inboundTag = { inbound_tag }, balancerTag = balancer_tag })
		return balancer_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.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.proxySettings = {
							tag = preproxy_outbound.tag,
							transportLayer = true
						}
						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.proxySettings = {
						tag = outbound.tag,
						transportLayer = true
					}
					table.insert(outbounds_table, to_outbound)
					default_outTag = to_outbound.tag
				end
			end
		end
		return default_outTag, last_insert_outbound
	end

	if node then
		if server_host and server_port then
			node.address = server_host
			node.port = server_port
		end
		if node.protocol == "_shunt" then
			local preproxy_rule_name = node.preproxy_enabled == "1" and "main" or nil
			local preproxy_tag = preproxy_rule_name
			local preproxy_node_id = node["main_node"]
			local preproxy_outbound_tag, preproxy_balancer_tag
			local preproxy_nodes

			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", nil
				elseif _node_id == "_blackhole" then
					return "blackhole", nil
				elseif _node_id == "_default" then
					return "default", nil
				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
					local socks_tag
					if socks_node then
						local _node = {
							type = "Xray",
							protocol = "socks",
							address = "127.0.0.1",
							port = socks_node.port,
							transport = "tcp",
							stream_security = "none"
						}
						local outbound = gen_outbound(flag, _node, rule_name)
						if outbound then
							if rule_name == "default" then
								table.insert(outbounds, 1, outbound)
							else
								table.insert(outbounds, outbound)
							end
							socks_tag = outbound.tag
						end
					end
					return socks_tag, nil
				elseif _node_id then
					local _node = uci:get_all(appname, _node_id)
					if not _node then return nil, 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
						if use_proxy and preproxy_balancer_tag and preproxy_nodes[_node_id] then use_proxy = false end
						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)
							return copied_outbound.tag, nil
						else
							if use_proxy and (_node.type ~= "Xray" or _node.flow == "xtls-rprx-vision") then
								new_port = get_new_port()
								table.insert(inbounds, {
									tag = "proxy_" .. rule_name,
									listen = "127.0.0.1",
									port = new_port,
									protocol = "dokodemo-door",
									settings = {network = "tcp,udp", address = _node.address, 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, {
									inboundTag = {"proxy_" .. rule_name},
									outboundTag = not preproxy_balancer_tag and preproxy_tag or nil,
									balancerTag = preproxy_balancer_tag
								})
							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 xray_settings.fragment == "1" then
									proxy_table.fragment = true
								end
								if xray_settings.noise == "1" then
									proxy_table.noise = true
								end
							end
							local outbound = gen_outbound(flag, _node, rule_name, proxy_table)
							local outbound_tag
							if outbound then
								outbound.tag = outbound.tag .. ":" .. _node.remarks
								outbound_tag, last_insert_outbound = set_outbound_detour(_node, outbound, outbounds, rule_name)
								if rule_name == "default" then
									table.insert(outbounds, 1, outbound)
								else
									table.insert(outbounds, outbound)
								end
								if last_insert_outbound then
									table.insert(outbounds, last_insert_outbound)
								end
							end
							return outbound_tag, nil
						end
					elseif _node.protocol == "_balancing" then
						return nil, gen_balancer(_node, rule_name)
					elseif _node.protocol == "_iface" then
						local outbound_tag
						if _node.iface then
							local outbound = {
								protocol = "freedom",
								tag = rule_name,
								streamSettings = {
									sockopt = {
										mark = 255,
										interface = _node.iface
									}
								}
							}
							outbound_tag = outbound.tag
							table.insert(outbounds, outbound)
							sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, _node.iface))
						end
						return outbound_tag, nil
					end
				end
			end

			if preproxy_tag and preproxy_node_id then
				preproxy_outbound_tag, preproxy_balancer_tag = gen_shunt_node(preproxy_rule_name, preproxy_node_id)
				if preproxy_balancer_tag then
					local _node_id = preproxy_node_id
					preproxy_nodes = {}
					while _node_id do
						_node = uci:get_all(appname, _node_id)
						if not _node then break end
						if _node.protocol ~= "_balancing" then
							preproxy_nodes[_node_id] = true
							break
						end
						local _blc_nodes = _node.balancing_node
						for i = 1, #_blc_nodes do preproxy_nodes[_blc_nodes[i]] = true end
						_node_id = _node.fallback_node
					end
				elseif preproxy_outbound_tag then
					preproxy_tag = preproxy_outbound_tag
				end
			end
			--default_node
			local default_node_id = node.default_node or "_direct"
			local default_outboundTag, default_balancerTag = gen_shunt_node("default", default_node_id)
			COMMON.default_outbound_tag = default_outboundTag
			COMMON.default_balancer_tag = default_balancerTag
			--shunt rule
			uci:foreach(appname, "shunt_rules", function(e)
				local outboundTag, balancerTag = gen_shunt_node(e[".name"])
				if outboundTag or balancerTag and e.remarks then
					if outboundTag == "default" then
						outboundTag = default_outboundTag
						balancerTag = default_balancerTag
					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
								table.insert(inboundTag, "tcp_redir")
								table.insert(inboundTag, "udp_redir")
							end
						end
						if e["inbound"]:find("socks") then
							if local_socks_port then
								table.insert(inboundTag, "socks-in")
							end
						end
					end
					local domains = nil
					if e.domain_list then
						local domain_table = {
							shunt_rule_name = e[".name"],
							outboundTag = outboundTag,
							balancerTag = balancerTag,
							domain = {},
						}
						domains = {}
						string.gsub(e.domain_list, '[^' .. "\r\n" .. ']+', function(w)
							if w:find("#") == 1 then return end
							table.insert(domains, w)
							table.insert(domain_table.domain, w)
						end)
						if outboundTag or balancerTag then
							table.insert(dns_domain_rules, api.clone(domain_table))
						end
						if #domains == 0 then domains = nil end
					end
					local ip = nil
					if e.ip_list then
						ip = {}
						string.gsub(e.ip_list, '[^' .. "\r\n" .. ']+', function(w)
							if w:find("#") == 1 then return end
							table.insert(ip, w)
						end)
						if #ip == 0 then ip = nil end
					end
					local source = nil
					if e.source then
						source = {}
						string.gsub(e.source, '[^' .. " " .. ']+', function(w)
							table.insert(source, w)
						end)
					end
					local rule = {
						ruleTag = e.remarks,
						inboundTag = inboundTag,
						outboundTag = outboundTag,
						balancerTag = balancerTag,
						network = e["network"] or "tcp,udp",
						source = source,
						sourcePort = e["sourcePort"] ~= "" and e["sourcePort"] or nil,
						port = e["port"] ~= "" and e["port"] or nil,
						protocol = protocols
					}
					if domains then
						local _rule = api.clone(rule)
						_rule.ruleTag = _rule.ruleTag .. " Domains"
						_rule.domains = domains
						table.insert(rules, _rule)
					end
					if ip then
						local _rule = api.clone(rule)
						_rule.ruleTag = _rule.ruleTag .. " IP"
						_rule.ip = ip
						table.insert(rules, _rule)
					end
					if not domains and not ip then
						table.insert(rules, rule)
					end
				end
			end)

			if default_balancerTag then
				table.insert(rules, {
					ruleTag = "default",
					balancerTag = default_balancerTag,
					network = "tcp,udp"
				})
			end

			routing = {
				domainStrategy = node.domainStrategy or "AsIs",
				domainMatcher = node.domainMatcher or "hybrid",
				balancers = #balancers > 0 and balancers or nil,
				rules = rules
			}
		elseif node.protocol == "_balancing" then
			if node.balancing_node then
				local balancer_tag = gen_balancer(node)
				if balancer_tag then
					table.insert(rules, { network = "tcp,udp", balancerTag = balancer_tag })
				end
				routing = {
					balancers = balancers,
					rules = rules
				}
				COMMON.default_balancer_tag = balancer_tag
			end
		elseif node.protocol == "_iface" then
			if node.iface then
				local outbound = {
					protocol = "freedom",
					tag = node.remarks or node_id,
					streamSettings = {
						sockopt = {
							mark = 255,
							interface = node.iface
						}
					}
				}
				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 = xray_settings.fragment == "1" or nil, noise = xray_settings.noise == "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
				routing = {
					domainStrategy = "AsIs",
					domainMatcher = "hybrid",
					rules = {}
				}
				table.insert(routing.rules, {
					ruleTag = "default",
					outboundTag = COMMON.default_outbound_tag,
					network = "tcp,udp"
				})
			end
		end
	end
	
	if dns_listen_port then
		local direct_dns_tag = "dns-in-direct"
		local remote_dns_tag = "dns-in-remote"
		local remote_fakedns_tag = "dns-in-remote-fakedns"
		local default_dns_tag = "dns-in-default"
		local dns_servers = {}

		local _remote_dns_proto = "tcp"

		if not routing then
			routing = {
				domainStrategy = "IPOnDemand",
				rules = {}
			}
		end
	
		dns = {
			tag = "dns-global",
			hosts = {},
			disableCache = (dns_cache and dns_cache == "0") and true or false,
			disableFallback = true,
			disableFallbackIfMatch = true,
			servers = {},
			queryStrategy = "UseIP"
		}
	
		local dns_host = ""
		if flag == "global" then
			dns_host = uci:get(appname, "@global[0]", "dns_hosts") or ""
		else
			flag = flag:gsub("acl_", "")
			local dns_hosts_mode = uci:get(appname, flag, "dns_hosts_mode") or "default"
			if dns_hosts_mode == "default" then
				dns_host = uci:get(appname, "@global[0]", "dns_hosts") or ""
			elseif dns_hosts_mode == "disable" then
				dns_host = ""
			elseif dns_hosts_mode == "custom" then
				dns_host = uci:get(appname, flag, "dns_hosts") or ""
			end
		end
		if #dns_host > 0 then
			string.gsub(dns_host, '[^' .. "\r\n" .. ']+', function(w)
				local host = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $1}')", w))
				local key = sys.exec(string.format("echo -n $(echo %s | awk -F ' ' '{print $2}')", w))
				if host ~= "" and key ~= "" then
					dns.hosts[host] = key
				end
			end)
		end

		local _remote_dns_ip = nil
	
		local _remote_dns = {
			tag = remote_dns_tag,
			queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4"
		}

		if remote_dns_udp_server then
			_remote_dns.address = remote_dns_udp_server
			_remote_dns.port = tonumber(remote_dns_udp_port) or 53
			_remote_dns_proto = "udp"
			_remote_dns_ip = remote_dns_udp_server
		end

		if remote_dns_tcp_server then
			_remote_dns.address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
			_remote_dns.port = tonumber(remote_dns_tcp_port) or 53
			_remote_dns_proto = "tcp"
			_remote_dns_ip = remote_dns_tcp_server
		end

		if remote_dns_doh_url and remote_dns_doh_host then
			if remote_dns_doh_ip and remote_dns_doh_host ~= remote_dns_doh_ip and not api.is_ip(remote_dns_doh_host) then
				dns.hosts[remote_dns_doh_host] = remote_dns_doh_ip
			end
			_remote_dns.address = remote_dns_doh_url
			_remote_dns.port = tonumber(remote_dns_doh_port) or 443
			_remote_dns_ip = remote_dns_doh_ip
		end

		if _remote_dns.address then
			table.insert(dns_servers, {
				outboundTag = remote_dns_detour == "direct" and "direct" or nil,
				server = _remote_dns
			})
		end

		local _remote_fakedns = nil
		if remote_dns_fake then
			fakedns = {}
			local fakedns4 = {
				ipPool = "198.18.0.0/16",
				poolSize = 65535
			}
			local fakedns6 = {
				ipPool = "fc00::/18",
				poolSize = 65535
			}
			if remote_dns_query_strategy == "UseIP" then
				table.insert(fakedns, fakedns4)
				table.insert(fakedns, fakedns6)
			elseif remote_dns_query_strategy == "UseIPv4" then
				table.insert(fakedns, fakedns4)
			elseif remote_dns_query_strategy == "UseIPv6" then
				table.insert(fakedns, fakedns6)
			end
			_remote_fakedns = {
				tag = remote_fakedns_tag,
				address = "fakedns",
			}
			table.insert(dns_servers, {
				server = _remote_fakedns
			})
		end
	
		local _direct_dns = nil
		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, "full:" .. w)
			end)
			if #domain > 0 then
				table.insert(dns_domain_rules, 1, {
					shunt_rule_name = "logic-vpslist",
					outboundTag = "direct",
					domain = domain
				})
			end

			_direct_dns = {
				tag = direct_dns_tag,
				address = direct_dns_udp_server,
				port = tonumber(direct_dns_udp_port) or 53,
				queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP",
			}

			if _direct_dns.address then
				table.insert(dns_servers, {
					outboundTag = "direct",
					server = _direct_dns
				})
			end
		end
	
		if dns_listen_port then
			table.insert(inbounds, {
				listen = "127.0.0.1",
				port = tonumber(dns_listen_port),
				protocol = "dokodemo-door",
				tag = "dns-in",
				settings = {
					address = "0.0.0.0",
					network = "tcp,udp"
				}
			})
			local direct_type_dns = {
				settings = {
					address = direct_dns_udp_server,
					port = tonumber(direct_dns_udp_port) or 53,
					network = "udp",
					nonIPQuery = "skip",
					blockTypes = {
						65
					}
				},
				proxySettings = {
					tag = "direct"
				}
			}
			local remote_type_dns = {
				settings = {
					address = remote_dns_udp_server,
					port = tonumber(remote_dns_udp_port) or 53,
					network = _remote_dns_proto or "tcp",
					nonIPQuery = "drop"
				}
			}
			local type_dns = direct_type_dns
			table.insert(outbounds, {
				tag = "dns-out",
				protocol = "dns",
				proxySettings = type_dns.proxySettings,
				settings = type_dns.settings
			})
			table.insert(routing.rules, 1, {
				inboundTag = {
					"dns-in"
				},
				outboundTag = "dns-out"
			})
		end
	
		local default_dns_tag_name = remote_dns_tag
		if (not COMMON.default_balancer_tag and not COMMON.default_outbound_tag) or COMMON.default_outbound_tag == "direct" then
			default_dns_tag_name = direct_dns_tag
		end
	
		if dns_servers and #dns_servers > 0 then
			-- Default DNS logic
			local default_dns_server = nil
			for index, value in ipairs(dns_servers) do
				if not default_dns_server and value.server.tag == default_dns_tag_name then
					default_dns_server = api.clone(value)
					default_dns_server.server.tag = default_dns_tag
					if value.server.tag == remote_dns_tag then
						if remote_dns_fake then
							default_dns_server.server = api.clone(_remote_fakedns)
							default_dns_server.server.tag = default_dns_tag
						else
							default_dns_server.outboundTag = value.outboundTag or COMMON.default_outbound_tag
							default_dns_server.balancerTag = COMMON.default_balancer_tag
						end
					end
					table.insert(dns_servers, 1, default_dns_server)
					break
				end
			end

			-- Shunt rule DNS logic
			if dns_domain_rules and #dns_domain_rules > 0 then
				for index, value in ipairs(dns_domain_rules) do
					if value.domain and (value.outboundTag or value.balancerTag) then
						local dns_server = nil
						local dns_outboundTag = value.outboundTag
						local dns_balancerTag = value.balancerTag
						if value.outboundTag == "direct" then
							dns_server = api.clone(_direct_dns)
						else
							if remote_dns_fake then
								dns_server = api.clone(_remote_fakedns)
							else
								dns_server = api.clone(_remote_dns)
								if remote_dns_detour == "direct" then
									dns_outboundTag = "direct"
									dns_balancerTag = nil
								end
							end
						end
						dns_server.domains = value.domain
						if value.shunt_rule_name then
							dns_server.tag = "dns-in-" .. value.shunt_rule_name
						end

						if dns_server then
							table.insert(dns_servers, {
								outboundTag = dns_outboundTag,
								balancerTag = dns_balancerTag,
								server = dns_server
							})
						end
					end
				end
			end

			for i = #dns_servers, 1, -1 do
				local value = dns_servers[i]
				if value.server.tag ~= direct_dns_tag and value.server.tag ~= remote_dns_tag then
					-- DNS rule must be at the front, prevents being matched by rules.
					if (value.outboundTag or value.balancerTag) and value.server.address ~= "fakedns" then
						table.insert(routing.rules, 1, {
							inboundTag = {
								value.server.tag
							},
							outboundTag = value.outboundTag,
							balancerTag = value.balancerTag
						})
					end
					if (value.server.domains and #value.server.domains > 0) or value.server.tag == default_dns_tag then
						-- Only keep default DNS server or has domains DNS server.
						table.insert(dns.servers, 1, value.server)
					end
				end
			end
		end

		local default_rule_index = nil
		for index, value in ipairs(routing.rules) do
			if value.ruleTag == "default" then
				default_rule_index = index
				break
			end
		end
		if default_rule_index then
			local default_rule = api.clone(routing.rules[default_rule_index])
			table.remove(routing.rules, default_rule_index)
			table.insert(routing.rules, default_rule)
		end

		local dns_hosts_len = 0
		for key, value in pairs(dns.hosts) do
			dns_hosts_len = dns_hosts_len + 1
		end

		if dns_hosts_len == 0 then
			dns.hosts = nil
		end

		local content = flag .. node_id .. jsonc.stringify(routing.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 inbounds or outbounds then
		local config = {
			log = {
				--access = string.format("/tmp/etc/%s/%s_access.log", appname, "global"),
				--error = string.format("/tmp/etc/%s/%s_error.log", appname, "global"),
				--dnsLog = true,
				loglevel = loglevel
			},
			-- DNS
			dns = dns,
			fakedns = fakedns,
			-- 传入连接
			inbounds = inbounds,
			-- 传出连接
			outbounds = outbounds,
			-- 连接观测
			burstObservatory = burstObservatory,
			-- 路由
			routing = routing,
			-- 本地策略
			policy = {
				levels = {
					[0] = {
						-- handshake = 4,
						-- connIdle = 300,
						-- uplinkOnly = 2,
						-- downlinkOnly = 5,
						bufferSize = xray_settings.buffer_size and tonumber(xray_settings.buffer_size) or nil,
						statsUserUplink = false,
						statsUserDownlink = false
					}
				},
				-- system = {
				--     statsInboundUplink = false,
				--     statsInboundDownlink = false
				-- }
			}
		}

		if xray_settings.fragment == "1" or xray_settings.noise == "1" then
			table.insert(outbounds, {
				protocol = "freedom",
				tag = "dialerproxy",
				settings = {
					domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP",
					fragment = (xray_settings.fragment == "1") and {
						packets = (xray_settings.fragment_packets and xray_settings.fragment_packets ~= "") and xray_settings.fragment_packets,
						length = (xray_settings.fragment_length and xray_settings.fragment_length ~= "") and xray_settings.fragment_length,
						interval = (xray_settings.fragment_interval and xray_settings.fragment_interval ~= "") and xray_settings.fragment_interval
					} or nil,
					noises = (xray_settings.noise == "1") and get_noise_packets() or nil
				},
				streamSettings = {
					sockopt = {
						mark = 255,
						tcpNoDelay = true
					}
				}
			})
		end

		local direct_outbound = {
			protocol = "freedom",
			tag = "direct",
			settings = {
				domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
			},
			streamSettings = {
				sockopt = {
					mark = 255
				}
			}
		}
		if COMMON.default_outbound_tag == "direct" then
			table.insert(outbounds, 1, direct_outbound)
		else
			table.insert(outbounds, direct_outbound)
		end

		local blackhole_outbound = {
			protocol = "blackhole",
			tag = "blackhole"
		}
		if COMMON.default_outbound_tag == "blackhole" then
			table.insert(outbounds, 1, blackhole_outbound)
		else
			table.insert(outbounds, blackhole_outbound)
		end

		for index, value in ipairs(config.outbounds) do
			if not value["_flag_proxy_tag"] 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
		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 = {}
	local routing = nil

	if local_socks_address and local_socks_port then
		local inbound = {
			listen = local_socks_address,
			port = tonumber(local_socks_port),
			protocol = "socks",
			settings = {
				udp = true,
				auth = "noauth"
			}
		}
		if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then
			inbound.settings.auth = "password"
			inbound.settings.accounts = {
				{
					user = local_socks_username,
					pass = local_socks_password
				}
			}
		end
		table.insert(inbounds, inbound)
	end
	
	if local_http_address and local_http_port then
		local inbound = {
			listen = local_http_address,
			port = tonumber(local_http_port),
			protocol = "http",
			settings = {
				allowTransparent = false
			}
		}
		if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then
			inbound.settings.accounts = {
				{
					user = local_http_username,
					pass = local_http_password
				}
			}
		end
		table.insert(inbounds, inbound)
	end
	
	if server_proto ~= "nil" and server_address ~= "nil" and server_port ~= "nil" then
		local outbound = {
			protocol = server_proto,
			streamSettings = {
				network = "tcp",
				security = "none"
			},
			settings = {
				servers = {
					{
						address = server_address,
						port = tonumber(server_port),
						users = (server_username and server_password) and {
							{
								user = server_username,
								pass = server_password
							}
						} or nil
					}
				}
			}
		}
		if outbound then table.insert(outbounds, outbound) end
	end
	
	-- 额外传出连接
	table.insert(outbounds, {
		protocol = "freedom", tag = "direct", settings = {keep = ""}
	})
	
	local config = {
		log = {
			loglevel = "warning"
		},
		-- 传入连接
		inbounds = inbounds,
		-- 传出连接
		outbounds = outbounds,
		-- 路由
		routing = routing
	}
	return jsonc.stringify(config, 1)
end

function gen_dns_config(var)
	local dns_listen_port = var["-dns_listen_port"]
	local dns_out_tag = var["-dns_out_tag"]
	local direct_dns_udp_server = var["-direct_dns_udp_server"]
	local direct_dns_udp_port = var["-direct_dns_udp_port"]
	local direct_dns_tcp_server = var["-direct_dns_tcp_server"]
	local direct_dns_tcp_port = var["-direct_dns_tcp_port"]
	local direct_dns_doh_url = var["-direct_dns_doh_url"]
	local direct_dns_doh_host = var["-direct_dns_doh_host"]
	local direct_dns_doh_ip = var["-direct_dns_doh_ip"]
	local direct_dns_doh_port = var["-direct_dns_doh_port"]
	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_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_query_strategy = var["-remote_dns_query_strategy"]
	local remote_dns_detour = var["-remote_dns_detour"]
	local remote_dns_client_ip = var["-remote_dns_client_ip"]
	local remote_dns_outbound_socks_address = var["-remote_dns_outbound_socks_address"]
	local remote_dns_outbound_socks_port = var["-remote_dns_outbound_socks_port"]
	local dns_cache = var["-dns_cache"]
	local loglevel = var["-loglevel"] or "warning"
	
	local inbounds = {}
	local outbounds = {}
	local dns = nil
	local routing = nil

	if dns_listen_port then
		routing = {
			domainStrategy = "IPOnDemand",
			rules = {}
		}
	
		dns = {
			tag = "dns-global",
			hosts = {},
			disableCache = (dns_cache == "1") and false or true,
			disableFallback = true,
			disableFallbackIfMatch = true,
			servers = {},
			clientIp = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil,
		}
	
		local other_type_dns_proto, other_type_dns_server, other_type_dns_port
	
		if dns_out_tag == "remote" then
			dns.queryStrategy = (remote_dns_query_strategy and remote_dns_query_strategy ~= "") and remote_dns_query_strategy or "UseIPv4"
			if remote_dns_detour == "direct" then
				dns_out_tag = "direct"
				table.insert(outbounds, 1, {
					tag = dns_out_tag,
					protocol = "freedom",
					settings = {
						domainStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
					},
					streamSettings = {
						sockopt = {
							mark = 255
						}
					}
				})
			else
				if remote_dns_outbound_socks_address and remote_dns_outbound_socks_port then
					table.insert(outbounds, 1, {
						tag = dns_out_tag,
						protocol = "socks",
						streamSettings = {
							network = "tcp",
							security = "none"
						},
						settings = {
							servers = {
								{
									address = remote_dns_outbound_socks_address,
									port = tonumber(remote_dns_outbound_socks_port)
								}
							}
						}
					})
				end
			end

			local _remote_dns = {
				tag = "dns-in-remote"
			}
	
			if remote_dns_udp_server then
				_remote_dns.address = remote_dns_udp_server
				_remote_dns.port = tonumber(remote_dns_udp_port) or 53
				
				other_type_dns_proto = "udp"
				other_type_dns_server = remote_dns_udp_server
				other_type_dns_port = _remote_dns.port
			end
	
			if remote_dns_tcp_server then
				_remote_dns.address = "tcp://" .. remote_dns_tcp_server .. ":" .. tonumber(remote_dns_tcp_port) or 53
				_remote_dns.port = tonumber(remote_dns_tcp_port) or 53
				
				other_type_dns_proto = "tcp"
				other_type_dns_server = remote_dns_tcp_server
				other_type_dns_port = _remote_dns.port
			end
	
			if remote_dns_doh_url and remote_dns_doh_host then
				if remote_dns_doh_ip and remote_dns_doh_host ~= remote_dns_doh_ip and not api.is_ip(remote_dns_doh_host) then
					dns.hosts[remote_dns_doh_host] = remote_dns_doh_ip
				end
				_remote_dns.address = remote_dns_doh_url
				_remote_dns.port = tonumber(remote_dns_doh_port) or 443
			end
	
			table.insert(dns.servers, _remote_dns)
		elseif dns_out_tag == "direct" then
			dns.queryStrategy = (direct_dns_query_strategy and direct_dns_query_strategy ~= "") and direct_dns_query_strategy or "UseIP"
			table.insert(outbounds, 1, {
				tag = dns_out_tag,
				protocol = "freedom",
				settings = {
					domainStrategy = dns.queryStrategy
				},
				streamSettings = {
					sockopt = {
						mark = 255
					}
				}
			})

			local _direct_dns = {
				tag = "dns-in-direct"
			}
	
			if direct_dns_udp_server then
				_direct_dns.address = direct_dns_udp_server
				_direct_dns.port = tonumber(direct_dns_udp_port) or 53
				table.insert(routing.rules, 1, {
					ip = {
						direct_dns_udp_server
					},
					port = tonumber(direct_dns_udp_port) or 53,
					network = "udp",
					outboundTag = "direct"
				})

				other_type_dns_proto = "udp"
				other_type_dns_server = direct_dns_udp_server
				other_type_dns_port = _direct_dns.port
			end
	
			if direct_dns_tcp_server then
				_direct_dns.address = "tcp+local://" .. direct_dns_tcp_server
				_direct_dns.port = tonumber(direct_dns_tcp_port) or 53
				
				other_type_dns_proto = "tcp"
				other_type_dns_server = direct_dns_tcp_server
				other_type_dns_port = _direct_dns.port
			end
	
			if direct_dns_doh_url and direct_dns_doh_host then
				if direct_dns_doh_ip and direct_dns_doh_host ~= direct_dns_doh_ip and not api.is_ip(direct_dns_doh_host) then
					dns.hosts[direct_dns_doh_host] = direct_dns_doh_ip
				end
				_direct_dns.address = direct_dns_doh_url:gsub("https://", "https+local://")
				_direct_dns.port = tonumber(direct_dns_doh_port) or 443
			end
	
			table.insert(dns.servers, _direct_dns)
		end
	
		local dns_hosts_len = 0
		for key, value in pairs(dns.hosts) do
			dns_hosts_len = dns_hosts_len + 1
		end
	
		if dns_hosts_len == 0 then
			dns.hosts = nil
		end
	
		table.insert(inbounds, {
			listen = "127.0.0.1",
			port = tonumber(dns_listen_port),
			protocol = "dokodemo-door",
			tag = "dns-in",
			settings = {
				address = other_type_dns_server or "1.1.1.1",
				port = other_type_dns_port or 53,
				network = "tcp,udp"
			}
		})
	
		table.insert(outbounds, {
			tag = "dns-out",
			protocol = "dns",
			proxySettings = {
				tag = dns_out_tag
			},
			settings = {
				address = other_type_dns_server or "1.1.1.1",
				port = other_type_dns_port or 53,
				network = other_type_dns_proto or "tcp",
				nonIPQuery = "drop"
			}
		})
	
		table.insert(routing.rules, 1, {
			inboundTag = {
				"dns-in"
			},
			outboundTag = "dns-out"
		})
	
		table.insert(routing.rules, {
			inboundTag = {
				"dns-global"
			},
			outboundTag = dns_out_tag
		})
	end
	
	if inbounds or outbounds then
		local config = {
			log = {
				--dnsLog = true,
				loglevel = loglevel
			},
			-- DNS
			dns = dns,
			-- 传入连接
			inbounds = inbounds,
			-- 传出连接
			outbounds = outbounds,
			-- 路由
			routing = routing
		}
		return jsonc.stringify(config, 1)
	end
	
end

_G.gen_config = gen_config
_G.gen_proto_config = gen_proto_config
_G.gen_dns_config = gen_dns_config

if arg[1] then
	local func =_G[arg[1]]
	if func then
		print(func(api.get_function_args(arg)))
	end
end
