diff --git a/doc/lucky1.png b/doc/lucky1.png deleted file mode 100644 index a5b41b6e..00000000 Binary files a/doc/lucky1.png and /dev/null differ diff --git a/doc/lucky2.png b/doc/lucky2.png deleted file mode 100644 index ba4c026f..00000000 Binary files a/doc/lucky2.png and /dev/null differ diff --git a/doc/lucky3.png b/doc/lucky3.png deleted file mode 100644 index 6fa7982f..00000000 Binary files a/doc/lucky3.png and /dev/null differ diff --git a/doc/taskplan1.png b/doc/taskplan1.png new file mode 100644 index 00000000..1f8db7be Binary files /dev/null and b/doc/taskplan1.png differ diff --git a/doc/taskplan2.png b/doc/taskplan2.png new file mode 100644 index 00000000..edb45d6d Binary files /dev/null and b/doc/taskplan2.png differ diff --git a/doc/taskplan3.png b/doc/taskplan3.png new file mode 100644 index 00000000..d4794702 Binary files /dev/null and b/doc/taskplan3.png differ diff --git a/doc/view.png b/doc/view.png new file mode 100644 index 00000000..169e112f Binary files /dev/null and b/doc/view.png differ diff --git a/doc/view2.png b/doc/view2.png new file mode 100644 index 00000000..9058c632 Binary files /dev/null and b/doc/view2.png differ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_list.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_list.lua index debfae77..4ba553c5 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_list.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_list.lua @@ -25,25 +25,25 @@ o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)") o.default = o.keylist[3] if true then - m:appendTemplate("/node_list/node_list") + m:appendTemplate("/node_list/node_list") - if luci.http.formvalue("cbi.submit") == "1" then - local group_order = {} - group_order = luci.http.formvaluetable("group.order") - if group_order then - for k, v in pairs(group_order) do - if v and v~= "" then - local new_order = {} - string.gsub(v, "[^" .. " " .. "]+", function(w) - new_order[#new_order + 1] = w - end) - for idx, name in ipairs(new_order) do - m.uci:reorder(m.config, name, idx - 1) - end - end - end - end - end + if luci.http.formvalue("cbi.submit") == "1" then + local group_order = {} + group_order = luci.http.formvaluetable("group.order") + if group_order then + for k, v in pairs(group_order) do + if v and v~= "" then + local new_order = {} + string.gsub(v, "[^" .. " " .. "]+", function(w) + new_order[#new_order + 1] = w + end) + for idx, name in ipairs(new_order) do + m.uci:reorder(m.config, name, idx - 1) + end + end + end + end + end end return api.return_map(m) diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua index d6827d49..2904eab2 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua @@ -117,9 +117,9 @@ end o = s:option(DummyValue, "_update", translate("Manual subscription All")) o.rawhtml = true o.cfgvalue = function(self, section) - return string.format([[ - ]], - translate("Manual subscription All")) + return string.format([[ + ]], + translate("Manual subscription All")) end s = m:section(TypedSection, "subscribe_list", "", "" .. translate("When adding a new subscription, please save and apply before manually subscribing. If you only change the subscription URL, you can subscribe manually, and the system will save it automatically.") .. "") @@ -215,9 +215,9 @@ end o = s:option(DummyValue, "_update", translate("Manual subscription")) o.rawhtml = true o.cfgvalue = function(self, section) - return string.format([[ - ]], - section, translate("Manual subscription")) + return string.format([[ + ]], + section, translate("Manual subscription")) end m:appendTemplate("/cbi/sortable", {sectiontype = s.sectiontype}) diff --git a/luci-app-passwall/luasrc/passwall/server_app.lua b/luci-app-passwall/luasrc/passwall/server_app.lua index 3c2d0197..2d0b5757 100644 --- a/luci-app-passwall/luasrc/passwall/server_app.lua +++ b/luci-app-passwall/luasrc/passwall/server_app.lua @@ -161,6 +161,7 @@ local function start() end) if firewall_num > 0 then api.uci_save(uci, "firewall", true, true) + cmd("/etc/init.d/firewall reload >/dev/null 2>&1") end end @@ -176,6 +177,7 @@ local function stop() end) if num > 0 then api.uci_save(uci, "firewall", true, true) + cmd("/etc/init.d/firewall reload >/dev/null 2>&1") end end cmd(string.format("rm -rf %s %s", CONFIG_PATH, LOG_APP_FILE)) diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index c2037d48..cf8fa84b 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -916,12 +916,12 @@ function gen_config(var) if xray_settings.fragment == "1" then local lengths, delays = {}, {} api.trim(xray_settings.fragment_lengths):gsub("[^,]+", function(w) - w = w:gsub("%s+", "") - if w ~= "" then lengths[#lengths+1] = w end + w = w:gsub("%s+", "") + if w ~= "" then lengths[#lengths+1] = w end end) api.trim(xray_settings.fragment_delays):gsub("[^,]+", function(w) - w = w:gsub("%s+", "") - if w ~= "" then delays[#delays+1] = w end + w = w:gsub("%s+", "") + if w ~= "" then delays[#delays+1] = w end end) fragment_table = { type = "fragment", diff --git a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value_com.htm b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value_com.htm index d470ea4b..3aa8f39f 100644 --- a/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value_com.htm +++ b/luci-app-passwall/luasrc/view/passwall/cbi/nodes_value_com.htm @@ -534,7 +534,7 @@ //自定义框 function v_customEnter(cbid, labelSpan, hiddenInput, searchInput, panel, listContainer, customInput) { let inputValue = customInput.value.trim(); - if (!inputValue) { + if (!inputValue) { return; } const existingItems = listContainer.querySelectorAll('li[data-key="' + inputValue + '"]'); diff --git a/luci-app-passwall/luasrc/view/passwall/global/faq.htm b/luci-app-passwall/luasrc/view/passwall/global/faq.htm index db4d1e31..6dad6239 100644 --- a/luci-app-passwall/luasrc/view/passwall/global/faq.htm +++ b/luci-app-passwall/luasrc/view/passwall/global/faq.htm @@ -3,42 +3,42 @@ local map = self.map local api = map.api -%>
@@ -58,7 +58,7 @@ local api = map.api } var dom = document.getElementById("faq_reset"); - if (dom) { + if (dom) { var li = ""; li += "" + "<%: Hide in main menu:%>"+ "" + "
" + "<%: Browser access: %>" + "" + hide_url + "" + "
"; li += "" + "<%: Show in main menu:%>"+ "" + "
" +"<%: Browser access: %>" + "" + show_url + "" + "
"; diff --git a/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm b/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm index 899540c9..3a425c5a 100644 --- a/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm +++ b/luci-app-passwall/luasrc/view/passwall/node_list/node_list.htm @@ -43,7 +43,7 @@ table td, .table .td { z-index: 99; text-align: center; background: white; - box-shadow: darkgrey 10px 10px 30px 5px; + box-shadow: darkgrey 10px 10px 30px 5px; text-align: center; padding: 0.5em; } @@ -55,8 +55,8 @@ table td, .table .td { .ping a, .ping_value a, .tcping_value a { - text-decoration: none; - cursor: pointer; + text-decoration: none; + cursor: pointer; } @media (prefers-color-scheme: dark) { diff --git a/luci-app-passwall/luasrc/view/passwall/rule/shunt_rule_list.htm b/luci-app-passwall/luasrc/view/passwall/rule/shunt_rule_list.htm index 2fbbd8c8..c41e24eb 100644 --- a/luci-app-passwall/luasrc/view/passwall/rule/shunt_rule_list.htm +++ b/luci-app-passwall/luasrc/view/passwall/rule/shunt_rule_list.htm @@ -15,8 +15,8 @@ table th, .table .th { table td, .table .td { text-align: center; - /* white-space: nowrap; */ - word-break: keep-all; + /* white-space: nowrap; */ + word-break: keep-all; } .td.cbi-section-actions { diff --git a/luci-app-passwall/luasrc/view/passwall/rule_list/geoview.htm b/luci-app-passwall/luasrc/view/passwall/rule_list/geoview.htm index 7ea144ad..f15a1860 100644 --- a/luci-app-passwall/luasrc/view/passwall/rule_list/geoview.htm +++ b/luci-app-passwall/luasrc/view/passwall/rule_list/geoview.htm @@ -75,7 +75,7 @@ local api = map.api if (!value) { alert("<%:Please enter query content!%>"); return; - } + } lookup_btn.disabled = true; extract_btn.disabled = true; btn.value = '<%:Querying%>'; diff --git a/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall b/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall index 983eeaf9..a1298483 100755 --- a/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall +++ b/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall @@ -1,7 +1,7 @@ #!/bin/sh if [ -e "/etc/config/ucitrack" ]; then - uci -q batch <<-EOF + uci -q batch <<-EOF delete ucitrack.@passwall[-1] add ucitrack passwall set ucitrack.@passwall[-1].init=passwall diff --git a/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall_server b/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall_server index 9aeb328f..86e618db 100755 --- a/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall_server +++ b/luci-app-passwall/root/etc/uci-defaults/luci-app-passwall_server @@ -1,7 +1,7 @@ #!/bin/sh if [ -e "/etc/config/ucitrack" ]; then - uci -q batch <<-EOF + uci -q batch <<-EOF delete ucitrack.@passwall_server[-1] add ucitrack passwall_server set ucitrack.@passwall_server[-1].init=passwall_server diff --git a/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua b/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua index 00272008..f85274db 100644 --- a/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua +++ b/luci-app-passwall/root/usr/share/passwall/helper_dnsmasq.lua @@ -1,814 +1,814 @@ -local api = require "luci.passwall.api" -local sys = api.sys -local fs = api.fs -local datatypes = api.datatypes -local TMP = {} - -local function tinsert(table_name, val) - if table_name and type(table_name) == "table" then - if not TMP[table_name] then - TMP[table_name] = {} - end - if TMP[table_name][val] then - return false - end - table.insert(table_name, val) - TMP[table_name][val] = true - return true - end - return false -end - -local function backup_servers() - local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server") - if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then - api.uci_set_c("@global[0]", "dnsmasq_servers", DNSMASQ_DNS) - api.uci_save_c(true) - end -end - -local function restore_servers() - local dns_table = {} - local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server") - if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then - for k, v in ipairs(DNSMASQ_DNS) do - tinsert(dns_table, v) - end - end - local OLD_SERVER = api.uci_get(api.c_config, "@global[0]", "dnsmasq_servers") - if OLD_SERVER and #OLD_SERVER > 0 then - for k, v in ipairs(OLD_SERVER) do - tinsert(dns_table, v) - end - api.uci_del_c("@global[0]", "dnsmasq_servers") - api.uci_save_c(true) - end - if dns_table and #dns_table > 0 then - api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table) - api.uci_save(nil, "dhcp", true) - end -end - -function stretch() - local dnsmasq_server = api.uci_get("dhcp", "@dnsmasq[0]", "server") - local dnsmasq_noresolv = api.uci_get("dhcp", "@dnsmasq[0]", "noresolv") - local _flag - if dnsmasq_server and #dnsmasq_server > 0 then - for k, v in ipairs(dnsmasq_server) do - if not v:find("/") then - _flag = true - end - end - end - if not _flag and dnsmasq_noresolv == "1" then - api.uci_del("dhcp", "@dnsmasq[0]", "noresolv") - local RESOLVFILE = "/tmp/resolv.conf.d/resolv.conf.auto" - local file = io.open(RESOLVFILE, "r") - if not file then - RESOLVFILE = "/tmp/resolv.conf.auto" - else - local size = file:seek("end") - file:close() - if size == 0 then - RESOLVFILE = "/tmp/resolv.conf.auto" - end - end - api.uci_set("dhcp", "@dnsmasq[0]", "resolvfile", RESOLVFILE) - api.uci_save(nil, "dhcp", true) - end -end - -function restart(var) - local LOG = var["-LOG"] - sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1") - if LOG == "1" then - api.log("重启 dnsmasq 服务") - end -end - -function logic_restart(var) - local LOG = var["-LOG"] - local DEFAULT_DNS = api.get_cache_var("DEFAULT_DNS") - if DEFAULT_DNS then - backup_servers() - --sys.call("sed -i '/list server/d' /etc/config/dhcp >/dev/null 2>&1") - local dns_table = {} - local dnsmasq_server = api.uci_get("dhcp", "@dnsmasq[0]", "server") - if dnsmasq_server and #dnsmasq_server > 0 then - for k, v in ipairs(dnsmasq_server) do - if v:find("/") then - tinsert(dns_table, v) - end - end - api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table) - api.uci_save(nil, "dhcp", true) - end - sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1") - restore_servers() - else - sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1") - end - if LOG == "1" then - api.log("重启 dnsmasq 服务") - end -end - -function copy_instance(var) - local LISTEN_PORT = var["-LISTEN_PORT"] - local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"] - local conf_lines = {} - local DEFAULT_DNSMASQ_CFGID = sys.exec("echo -n $(uci -q show dhcp.@dnsmasq[0] | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')") - for line in io.lines("/tmp/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) do - local filter - if line:find("passwall") then filter = true end - if line:find("ubus") then filter = true end - if line:find("dhcp") then filter = true end - if line:find("server=") == 1 then filter = true end - if line:find("port=") == 1 then filter = true end - if line:find("conf%-dir=") == 1 then - filter = true - if TMP_DNSMASQ_PATH then - local tmp_path = line:sub(1 + #"conf-dir=") - sys.call(string.format("cp -r %s/* %s/ 2>/dev/null", tmp_path, TMP_DNSMASQ_PATH)) - end - end - if line:find("address=") == 1 or (line:find("server=") == 1 and line:find("/")) then filter = nil end - if not filter then - tinsert(conf_lines, line) - end - end - tinsert(conf_lines, "port=" .. LISTEN_PORT) - if TMP_DNSMASQ_PATH then - sys.call("rm -rf " .. TMP_DNSMASQ_PATH .. "/*passwall*") - end - if var["-return"] == "1" then - return conf_lines - end - if #conf_lines > 0 then - local DNSMASQ_CONF = var["-DNSMASQ_CONF"] - local conf_out = io.open(DNSMASQ_CONF, "a") - conf_out:write(table.concat(conf_lines, "\n")) - conf_out:write("\n") - conf_out:close() - end -end - -function add_rule(var) - local FLAG = var["-FLAG"] - local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"] - local DNSMASQ_CONF_FILE = var["-DNSMASQ_CONF_FILE"] - local LISTEN_PORT = var["-LISTEN_PORT"] - local DEFAULT_DNS = var["-DEFAULT_DNS"] - local LOCAL_DNS = var["-LOCAL_DNS"] - local TUN_DNS = var["-TUN_DNS"] - local USE_DEFAULT_DNS = var["-USE_DEFAULT_DNS"] - local CHINADNS_DNS = var["-CHINADNS_DNS"] - local NODE = var["-NODE"] - local USE_DIRECT_LIST = var["-USE_DIRECT_LIST"] - local USE_PROXY_LIST = var["-USE_PROXY_LIST"] - local USE_BLOCK_LIST = var["-USE_BLOCK_LIST"] - local USE_GFW_LIST = var["-USE_GFW_LIST"] - local CHN_LIST = var["-CHN_LIST"] - local DEFAULT_PROXY_MODE = var["-DEFAULT_PROXY_MODE"] - local NO_PROXY_IPV6 = var["-NO_PROXY_IPV6"] - local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"] - local NFTFLAG = var["-NFTFLAG"] - local CACHE_PATH = api.CACHE_PATH - local CACHE_FLAG = "dnsmasq_" .. FLAG - local CACHE_DNS_PATH = CACHE_PATH .. "/" .. CACHE_FLAG - local CACHE_TEXT_FILE = CACHE_DNS_PATH .. ".txt" - local USE_CHINADNS_NG = "0" - local IS_SHUNT_NODE = api.uci_get_c(NODE, "protocol") == "_shunt" - local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview") - - local list1 = {} - local excluded_domain = {} - - if not api.is_finded("geoview") then - USE_GEOVIEW = "0" - end - - local function log(...) - if NO_LOGIC_LOG == "1" then - return - end - api.log(...) - end - - local function check_dns(domain, dns) - if domain == "" or domain:find("#") then - return false - end - if not dns then - return - end - for k,v in ipairs(list1[domain].dns) do - if dns == v then - return true - end - end - return false - end - - local function check_ipset(domain, ipset) - if domain == "" or domain:find("#") then - return false - end - if not ipset then - return - end - for k,v in ipairs(list1[domain].ipsets) do - if ipset == v then - return true - end - end - return false - end - - local function set_domain_address(domain, address) - if domain == "" or domain:find("#") then - return - end - if not list1[domain] then - list1[domain] = { - dns = {}, - ipsets = {} - } - end - if not list1[domain].address then - list1[domain].address = address - end - end - - local function set_domain_dns(domain, dns) - if domain == "" or domain:find("#") then - return - end - if not dns then - return - end - if not list1[domain] then - list1[domain] = { - dns = {}, - ipsets = {} - } - end - for line in string.gmatch(dns, '[^' .. "," .. ']+') do - if not check_dns(domain, line) then - table.insert(list1[domain].dns, line) - end - end - end - - local function set_domain_ipset(domain, ipset) - if domain == "" or domain:find("#") then - return - end - if not ipset then - return - end - if not list1[domain] then - list1[domain] = { - dns = {}, - ipsets = {} - } - end - for line in string.gmatch(ipset, '[^' .. "," .. ']+') do - if not check_ipset(domain, line) then - table.insert(list1[domain].ipsets, line) - end - end - end - - local function add_excluded_domain(domain) - if domain == "" or domain:find("#") then - return - end - excluded_domain[domain] = true - end - - local function check_excluded_domain(domain) - if domain == "" or domain:find("#") then - return false - end - if excluded_domain[domain] then - return true - end - local pos = domain:find(".", 1, true) - while pos do - if excluded_domain[domain:sub(pos + 1)] then - return true - end - pos = domain:find(".", pos + 1, true) - end - return false - end - - local function foreach_geosite(list_arg, callback) - local geosite_path = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" - geosite_path = geosite_path:match("^(.*)/") .. "/geosite.dat" - if not fs.access(geosite_path) then return end - local bin = api.finded_com("geoview") - if not (bin and list_arg) then return end - local cmd = string.format("%q -type geosite -action extract -input %q -list %q -lowmem=true", bin, geosite_path, list_arg) - local pipe = io.popen(cmd) - if not pipe then return end - for line in pipe:lines() do - if line ~= "" then callback(line) end - end - pipe:close() - end - - local cache_text = "" - local nodes_address_md5 = sys.exec("echo -n $(uci show passwall | grep '\\.address') | md5sum") - local new_rules = sys.exec("echo -n $(find /usr/share/passwall/rules -type f | xargs md5sum)") - local new_text = TMP_DNSMASQ_PATH .. DNSMASQ_CONF_FILE .. DEFAULT_DNS .. LOCAL_DNS .. TUN_DNS .. USE_DEFAULT_DNS .. CHINADNS_DNS .. USE_DIRECT_LIST .. USE_PROXY_LIST .. USE_BLOCK_LIST .. USE_GFW_LIST .. CHN_LIST .. DEFAULT_PROXY_MODE .. NO_PROXY_IPV6 .. nodes_address_md5 .. new_rules .. NFTFLAG - if fs.access(CACHE_TEXT_FILE) then - for line in io.lines(CACHE_TEXT_FILE) do - cache_text = line - end - end - - if cache_text ~= new_text then - api.remove(CACHE_DNS_PATH .. "*") - end - - local dnsmasq_default_dns - if USE_DEFAULT_DNS ~= "nil" then - if USE_DEFAULT_DNS == "direct" then - dnsmasq_default_dns = LOCAL_DNS - end - if USE_DEFAULT_DNS == "remote" then - dnsmasq_default_dns = TUN_DNS - end - if USE_DEFAULT_DNS == "remote" and CHN_LIST == "direct" then - dnsmasq_default_dns = TUN_DNS - end - end - - local only_global - if DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0" then - --没有启用中国列表和GFW列表时 - dnsmasq_default_dns = TUN_DNS - only_global = 1 - end - if USE_DEFAULT_DNS == "chinadns_ng" and CHINADNS_DNS ~= "0" then - dnsmasq_default_dns = CHINADNS_DNS - USE_CHINADNS_NG = "1" - end - - local setflag_4= (NFTFLAG == "1") and "4#inet#passwall#" or "" - local setflag_6= (NFTFLAG == "1") and "6#inet#passwall#" or "" - - if not fs.access(CACHE_DNS_PATH) then - fs.mkdir(CACHE_DNS_PATH) - - --屏蔽列表 - if USE_CHINADNS_NG == "0" and USE_BLOCK_LIST == "1" then - local geosite_arg = "" - local f = io.open("/usr/share/passwall/rules/block_host") - if f then - for line in f:lines() do - if not line:find("#") and line:find("geosite:") then - line = string.match(line, ":([^:]+)$") - geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line - else - line = api.get_std_domain(line) - if line ~= "" and not line:find("#") and not line:find(":") then - set_domain_address(line, "") - end - end - end - f:close() - end - if USE_GEOVIEW == "1" and geosite_arg ~= "" then - foreach_geosite(geosite_arg, function(line) - set_domain_address(line, "") - end) - log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成") - end - end - - local fwd_dns - local no_ipv6 - - --始终用国内DNS解析节点域名 - if true then - fwd_dns = LOCAL_DNS - if USE_CHINADNS_NG == "1" then - fwd_dns = nil - else - local sets = { - setflag_4 .. "psw_vps", - setflag_6 .. "psw_vps6" - } - local function process_address(address) - address = (address or ""):lower() - if api.vps_domain_exclude(address) then return end - if datatypes.hostname(address) then - set_domain_dns(address, fwd_dns) - set_domain_ipset(address, table.concat(sets, ",")) - end - end - api.uci_foreach_c("nodes", function(t) - process_address(t.address) - process_address(t.download_address) - local dns, _ = api.get_domain_port_from_url(t.domain_resolver_dns or t.domain_resolver_dns_https or "") - if dns and dns ~= "" then - process_address(dns) - end - end) - api.uci_foreach_c("subscribe_list", function(t) --订阅链接 - local url, _ = api.get_domain_port_from_url(t.url or "") - if url and url ~= "" then - process_address(url) - end - end) - log(string.format(" - 节点列表中的域名(vpslist):%s", fwd_dns or "默认")) - end - end - - --直连(白名单)列表 - if USE_DIRECT_LIST == "1" then - fwd_dns = LOCAL_DNS - if USE_CHINADNS_NG == "1" then - fwd_dns = nil - end - if fwd_dns then - local sets = { - setflag_4 .. "psw_white", - setflag_6 .. "psw_white6" - } - --始终用国内DNS解析直连(白名单)列表 - local geosite_arg = "" - local f = io.open("/usr/share/passwall/rules/direct_host") - if f then - for line in f:lines() do - if not line:find("#") and line:find("geosite:") then - line = string.match(line, ":([^:]+)$") - geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line - else - line = api.get_std_domain(line) - if line ~= "" and not line:find("#") and not line:find(":") then - add_excluded_domain(line) - set_domain_dns(line, fwd_dns) - set_domain_ipset(line, table.concat(sets, ",")) - end - end - end - f:close() - log(string.format(" - 域名白名单(whitelist):%s", fwd_dns or "默认")) - end - if USE_GEOVIEW == "1" and geosite_arg ~= "" then - foreach_geosite(geosite_arg, function(line) - add_excluded_domain(line) - set_domain_dns(line, fwd_dns) - set_domain_ipset(line, table.concat(sets, ",")) - end) - log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)完成") - end - end - end - - --代理(黑名单)列表 - if USE_PROXY_LIST == "1" then - fwd_dns = TUN_DNS - if USE_CHINADNS_NG == "1" then - fwd_dns = nil - end - if fwd_dns then - local set_name = "psw_black" - local set6_name = "psw_black6" - if FLAG ~= "default" then - set_name = "psw_" .. FLAG .. "_black" - set6_name = "psw_" .. FLAG .. "_black6" - end - local sets = { - setflag_4 .. set_name - } - if NO_PROXY_IPV6 ~= "1" then - table.insert(sets, setflag_6 .. set6_name) - end - --始终使用远程DNS解析代理(黑名单)列表 - local geosite_arg = "" - local f = io.open("/usr/share/passwall/rules/proxy_host") - if f then - for line in f:lines() do - if not line:find("#") and line:find("geosite:") then - line = string.match(line, ":([^:]+)$") - geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line - else - line = api.get_std_domain(line) - if line ~= "" and not line:find("#") and not line:find(":") then - add_excluded_domain(line) - if NO_PROXY_IPV6 == "1" then - set_domain_address(line, "::") - end - set_domain_dns(line, fwd_dns) - set_domain_ipset(line, table.concat(sets, ",")) - end - end - end - f:close() - log(string.format(" - 代理域名表(blacklist):%s", fwd_dns or "默认")) - end - if USE_GEOVIEW == "1" and geosite_arg ~= "" then - foreach_geosite(geosite_arg, function(line) - add_excluded_domain(line) - if NO_PROXY_IPV6 == "1" then - set_domain_address(line, "::") - end - set_domain_dns(line, fwd_dns) - set_domain_ipset(line, table.concat(sets, ",")) - end) - log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)完成") - end - end - end - - --GFW列表 - if USE_GFW_LIST == "1" then - fwd_dns = TUN_DNS - if USE_CHINADNS_NG == "1" then - fwd_dns = nil - end - if fwd_dns then - local set_name = "psw_gfw" - local set6_name = "psw_gfw6" - if FLAG ~= "default" then - set_name = "psw_" .. FLAG .. "_gfw" - set6_name = "psw_" .. FLAG .. "_gfw6" - end - local sets = { - setflag_4 .. set_name - } - if NO_PROXY_IPV6 ~= "1" then - table.insert(sets, setflag_6 .. set6_name) - end - local f = io.open("/usr/share/passwall/rules/gfwlist") - if f then - for line in f:lines() do - if line ~= "" and not line:find("#") and not check_excluded_domain(line) then - if NO_PROXY_IPV6 == "1" then - set_domain_address(line, "::") - end - if dnsmasq_default_dns == fwd_dns then - fwd_dns = nil - else - set_domain_dns(line, fwd_dns) - end - set_domain_ipset(line, table.concat(sets, ",")) - end - end - f:close() - log(string.format(" - 防火墙域名表(gfwlist):%s", fwd_dns or "默认")) - end - end - end - - --中国列表 - if CHN_LIST ~= "0" then - fwd_dns = nil - if CHN_LIST == "direct" then - fwd_dns = LOCAL_DNS - end - if CHN_LIST == "proxy" then - fwd_dns = TUN_DNS - end - if USE_CHINADNS_NG == "1" then - fwd_dns = nil - end - if fwd_dns then - local sets = { - setflag_4 .. "psw_chn", - setflag_6 .. "psw_chn6" - } - if CHN_LIST == "proxy" then - if NO_PROXY_IPV6 == "1" then - sets = { - setflag_4 .. "psw_chn" - } - end - end - local f = io.open("/usr/share/passwall/rules/chnlist") - if f then - for line in f:lines() do - if line ~= "" and not line:find("#") and not check_excluded_domain(line) then - if CHN_LIST == "proxy" and NO_PROXY_IPV6 == "1" then - set_domain_address(line, "::") - end - if dnsmasq_default_dns == fwd_dns then - fwd_dns = nil - else - set_domain_dns(line, fwd_dns) - end - set_domain_ipset(line, table.concat(sets, ",")) - end - end - f:close() - log(string.format(" - 中国域名表(chnroute):%s", fwd_dns or "默认")) - end - end - end - - --分流规则 - if IS_SHUNT_NODE and USE_CHINADNS_NG == "0" then - local t = api.uci_get_c(NODE) - local default_node_id = t["default_node"] or "_direct" - api.uci_foreach_c("shunt_rules", function(s) - local _node_id = t[s[".name"]] - if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then - if _node_id == "_default" then - _node_id = default_node_id - end - - fwd_dns = nil - no_ipv6 = nil - - local sets = {} - - if _node_id == "_direct" then - fwd_dns = LOCAL_DNS - if USE_DIRECT_LIST == "1" then - table.insert(sets, setflag_4 .. "psw_white") - table.insert(sets, setflag_6 .. "psw_white6") - else - local set_name = "psw_shunt" - local set6_name = "psw_shunt6" - if FLAG ~= "default" then - set_name = "psw_" .. FLAG .. "_shunt" - set6_name = "psw_" .. FLAG .. "_shunt6" - end - table.insert(sets, setflag_4 .. set_name) - table.insert(sets, setflag_6 .. set6_name) - end - else - local set_name = "psw_shunt" - local set6_name = "psw_shunt6" - if FLAG ~= "default" then - set_name = "psw_" .. FLAG .. "_shunt" - set6_name = "psw_" .. FLAG .. "_shunt6" - end - fwd_dns = TUN_DNS - table.insert(sets, setflag_4 .. set_name) - if NO_PROXY_IPV6 ~= "1" then - table.insert(sets, setflag_6 .. set6_name) - else - no_ipv6 = true - end - end - - local domain_list = s.domain_list or "" - local geosite_arg = "" - for line in string.gmatch(domain_list, "[^\r\n]+") do - if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") and not line:find("rule-set:") and not line:find("rs:") then - if line:find("geosite:") then - line = string.match(line, ":([^:]+)$") - geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line - else - if line:find("domain:") or line:find("full:") then - line = string.match(line, ":([^:]+)$") - end - line = api.get_std_domain(line) - add_excluded_domain(line) - - if no_ipv6 then - set_domain_address(line, "::") - end - set_domain_dns(line, fwd_dns) - set_domain_ipset(line, table.concat(sets, ",")) - end - end - end - - if USE_GFW_LIST == "1" and CHN_LIST == "0" and USE_GEOVIEW == "1" and geosite_arg ~= "" then --仅GFW模式解析geosite - foreach_geosite(geosite_arg, function(line) - add_excluded_domain(line) - if no_ipv6 then - set_domain_address(line, "::") - end - set_domain_dns(line, fwd_dns) - set_domain_ipset(line, table.concat(sets, ",")) - end) - log(string.format(" - 解析分流规则(%s) Geosite 完成", s.remarks)) - end - - if _node_id ~= "_direct" then - log(string.format(" - Sing-Box/Xray分流规则(%s):%s", s.remarks, fwd_dns or "默认")) - end - end - end) - elseif only_global == 1 and NO_PROXY_IPV6 == "1" then - --节点:固定节点 - --代理模式:全局模式 - --过滤代理域名 IPv6:启用 - --禁止解析所有IPv6记录 - list1["#"] = { - dns = {}, - ipsets = {}, - address = "::" - } - end - - if list1 and next(list1) then - local address_out = io.open(CACHE_DNS_PATH .. "/000-address.conf", "a") - local server_out = io.open(CACHE_DNS_PATH .. "/001-server.conf", "a") - local ipset_out = io.open(CACHE_DNS_PATH .. "/ipset.conf", "a") - local set_name = "ipset" - if NFTFLAG == "1" then - set_name = "nftset" - end - for key, value in pairs(list1) do - if value.address then - local domain = "." .. key - if key == "#" then - domain = key - end - address_out:write(string.format("address=/%s/%s", domain, value.address) .. "\n") - end - if value.dns and #value.dns > 0 then - for i, dns in ipairs(value.dns) do - server_out:write(string.format("server=/.%s/%s", key, dns) .. "\n") - end - end - if value.ipsets and #value.ipsets > 0 then - local ipsets_str = "" - for i, ipset in ipairs(value.ipsets) do - ipsets_str = ipsets_str .. ipset .. "," - end - ipsets_str = ipsets_str:sub(1, #ipsets_str - 1) - ipset_out:write(string.format("%s=/.%s/%s", set_name, key, ipsets_str) .. "\n") - end - end - address_out:close() - server_out:close() - ipset_out:close() - end - - local f_out = io.open(CACHE_TEXT_FILE, "a") - f_out:write(new_text) - f_out:close() - end - - if USE_CHINADNS_NG == "0" then - api.remove(TMP_DNSMASQ_PATH) - fs.symlink(CACHE_DNS_PATH, TMP_DNSMASQ_PATH) - end - - if DNSMASQ_CONF_FILE ~= "nil" then - local conf_lines = {} - if LISTEN_PORT then - --Copy dnsmasq instance - conf_lines = copy_instance({["-LISTEN_PORT"] = LISTEN_PORT, ["-TMP_DNSMASQ_PATH"] = TMP_DNSMASQ_PATH, ["-return"] = "1"}) - --dhcp.leases to hosts - local hosts = api.CACHE_PATH .. "/dhcp-hosts" - sys.call("touch " .. hosts) - tinsert(conf_lines, "addn-hosts=" .. hosts) - else - --Modify the default dnsmasq service - end - if USE_CHINADNS_NG == "0" then - tinsert(conf_lines, string.format("conf-dir=%s", TMP_DNSMASQ_PATH)) - end - if dnsmasq_default_dns then - for s in string.gmatch(dnsmasq_default_dns, '[^' .. "," .. ']+') do - tinsert(conf_lines, string.format("server=%s", s)) - end - tinsert(conf_lines, "all-servers") - tinsert(conf_lines, "no-poll") - tinsert(conf_lines, "no-resolv") - if USE_CHINADNS_NG == "0" then - log(string.format(" - 默认:%s", dnsmasq_default_dns)) - end - - if FLAG == "default" then - api.set_cache_var("DEFAULT_DNS", DEFAULT_DNS) - end - end - if #conf_lines > 0 then - local conf_out = io.open(DNSMASQ_CONF_FILE, "a") - if conf_out then - conf_out:write(table.concat(conf_lines, "\n")) - conf_out:write("\n") - conf_out:close() - end - end - end - - if USE_CHINADNS_NG == "0" then - log(" - PassWall必须依赖于Dnsmasq,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理域名)分流失效!!!") - end -end - -_G.stretch = stretch -_G.restart = restart -_G.logic_restart = logic_restart -_G.copy_instance = copy_instance -_G.add_rule = add_rule - -if arg[1] then - local func =_G[arg[1]] - if func then - func(api.get_function_args(arg)) - end -end +local api = require "luci.passwall.api" +local sys = api.sys +local fs = api.fs +local datatypes = api.datatypes +local TMP = {} + +local function tinsert(table_name, val) + if table_name and type(table_name) == "table" then + if not TMP[table_name] then + TMP[table_name] = {} + end + if TMP[table_name][val] then + return false + end + table.insert(table_name, val) + TMP[table_name][val] = true + return true + end + return false +end + +local function backup_servers() + local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server") + if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then + api.uci_set_c("@global[0]", "dnsmasq_servers", DNSMASQ_DNS) + api.uci_save_c(true) + end +end + +local function restore_servers() + local dns_table = {} + local DNSMASQ_DNS = api.uci_get("dhcp", "@dnsmasq[0]", "server") + if DNSMASQ_DNS and #DNSMASQ_DNS > 0 then + for k, v in ipairs(DNSMASQ_DNS) do + tinsert(dns_table, v) + end + end + local OLD_SERVER = api.uci_get(api.c_config, "@global[0]", "dnsmasq_servers") + if OLD_SERVER and #OLD_SERVER > 0 then + for k, v in ipairs(OLD_SERVER) do + tinsert(dns_table, v) + end + api.uci_del_c("@global[0]", "dnsmasq_servers") + api.uci_save_c(true) + end + if dns_table and #dns_table > 0 then + api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table) + api.uci_save(nil, "dhcp", true) + end +end + +function stretch() + local dnsmasq_server = api.uci_get("dhcp", "@dnsmasq[0]", "server") + local dnsmasq_noresolv = api.uci_get("dhcp", "@dnsmasq[0]", "noresolv") + local _flag + if dnsmasq_server and #dnsmasq_server > 0 then + for k, v in ipairs(dnsmasq_server) do + if not v:find("/") then + _flag = true + end + end + end + if not _flag and dnsmasq_noresolv == "1" then + api.uci_del("dhcp", "@dnsmasq[0]", "noresolv") + local RESOLVFILE = "/tmp/resolv.conf.d/resolv.conf.auto" + local file = io.open(RESOLVFILE, "r") + if not file then + RESOLVFILE = "/tmp/resolv.conf.auto" + else + local size = file:seek("end") + file:close() + if size == 0 then + RESOLVFILE = "/tmp/resolv.conf.auto" + end + end + api.uci_set("dhcp", "@dnsmasq[0]", "resolvfile", RESOLVFILE) + api.uci_save(nil, "dhcp", true) + end +end + +function restart(var) + local LOG = var["-LOG"] + sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1") + if LOG == "1" then + api.log("重启 dnsmasq 服务") + end +end + +function logic_restart(var) + local LOG = var["-LOG"] + local DEFAULT_DNS = api.get_cache_var("DEFAULT_DNS") + if DEFAULT_DNS then + backup_servers() + --sys.call("sed -i '/list server/d' /etc/config/dhcp >/dev/null 2>&1") + local dns_table = {} + local dnsmasq_server = api.uci_get("dhcp", "@dnsmasq[0]", "server") + if dnsmasq_server and #dnsmasq_server > 0 then + for k, v in ipairs(dnsmasq_server) do + if v:find("/") then + tinsert(dns_table, v) + end + end + api.uci_set("dhcp", "@dnsmasq[0]", "server", dns_table) + api.uci_save(nil, "dhcp", true) + end + sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1") + restore_servers() + else + sys.call("/etc/init.d/dnsmasq restart >/dev/null 2>&1") + end + if LOG == "1" then + api.log("重启 dnsmasq 服务") + end +end + +function copy_instance(var) + local LISTEN_PORT = var["-LISTEN_PORT"] + local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"] + local conf_lines = {} + local DEFAULT_DNSMASQ_CFGID = sys.exec("echo -n $(uci -q show dhcp.@dnsmasq[0] | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')") + for line in io.lines("/tmp/etc/dnsmasq.conf." .. DEFAULT_DNSMASQ_CFGID) do + local filter + if line:find("passwall") then filter = true end + if line:find("ubus") then filter = true end + if line:find("dhcp") then filter = true end + if line:find("server=") == 1 then filter = true end + if line:find("port=") == 1 then filter = true end + if line:find("conf%-dir=") == 1 then + filter = true + if TMP_DNSMASQ_PATH then + local tmp_path = line:sub(1 + #"conf-dir=") + sys.call(string.format("cp -r %s/* %s/ 2>/dev/null", tmp_path, TMP_DNSMASQ_PATH)) + end + end + if line:find("address=") == 1 or (line:find("server=") == 1 and line:find("/")) then filter = nil end + if not filter then + tinsert(conf_lines, line) + end + end + tinsert(conf_lines, "port=" .. LISTEN_PORT) + if TMP_DNSMASQ_PATH then + sys.call("rm -rf " .. TMP_DNSMASQ_PATH .. "/*passwall*") + end + if var["-return"] == "1" then + return conf_lines + end + if #conf_lines > 0 then + local DNSMASQ_CONF = var["-DNSMASQ_CONF"] + local conf_out = io.open(DNSMASQ_CONF, "a") + conf_out:write(table.concat(conf_lines, "\n")) + conf_out:write("\n") + conf_out:close() + end +end + +function add_rule(var) + local FLAG = var["-FLAG"] + local TMP_DNSMASQ_PATH = var["-TMP_DNSMASQ_PATH"] + local DNSMASQ_CONF_FILE = var["-DNSMASQ_CONF_FILE"] + local LISTEN_PORT = var["-LISTEN_PORT"] + local DEFAULT_DNS = var["-DEFAULT_DNS"] + local LOCAL_DNS = var["-LOCAL_DNS"] + local TUN_DNS = var["-TUN_DNS"] + local USE_DEFAULT_DNS = var["-USE_DEFAULT_DNS"] + local CHINADNS_DNS = var["-CHINADNS_DNS"] + local NODE = var["-NODE"] + local USE_DIRECT_LIST = var["-USE_DIRECT_LIST"] + local USE_PROXY_LIST = var["-USE_PROXY_LIST"] + local USE_BLOCK_LIST = var["-USE_BLOCK_LIST"] + local USE_GFW_LIST = var["-USE_GFW_LIST"] + local CHN_LIST = var["-CHN_LIST"] + local DEFAULT_PROXY_MODE = var["-DEFAULT_PROXY_MODE"] + local NO_PROXY_IPV6 = var["-NO_PROXY_IPV6"] + local NO_LOGIC_LOG = var["-NO_LOGIC_LOG"] + local NFTFLAG = var["-NFTFLAG"] + local CACHE_PATH = api.CACHE_PATH + local CACHE_FLAG = "dnsmasq_" .. FLAG + local CACHE_DNS_PATH = CACHE_PATH .. "/" .. CACHE_FLAG + local CACHE_TEXT_FILE = CACHE_DNS_PATH .. ".txt" + local USE_CHINADNS_NG = "0" + local IS_SHUNT_NODE = api.uci_get_c(NODE, "protocol") == "_shunt" + local USE_GEOVIEW = api.uci_get_c("@global_rules[0]", "enable_geoview") + + local list1 = {} + local excluded_domain = {} + + if not api.is_finded("geoview") then + USE_GEOVIEW = "0" + end + + local function log(...) + if NO_LOGIC_LOG == "1" then + return + end + api.log(...) + end + + local function check_dns(domain, dns) + if domain == "" or domain:find("#") then + return false + end + if not dns then + return + end + for k,v in ipairs(list1[domain].dns) do + if dns == v then + return true + end + end + return false + end + + local function check_ipset(domain, ipset) + if domain == "" or domain:find("#") then + return false + end + if not ipset then + return + end + for k,v in ipairs(list1[domain].ipsets) do + if ipset == v then + return true + end + end + return false + end + + local function set_domain_address(domain, address) + if domain == "" or domain:find("#") then + return + end + if not list1[domain] then + list1[domain] = { + dns = {}, + ipsets = {} + } + end + if not list1[domain].address then + list1[domain].address = address + end + end + + local function set_domain_dns(domain, dns) + if domain == "" or domain:find("#") then + return + end + if not dns then + return + end + if not list1[domain] then + list1[domain] = { + dns = {}, + ipsets = {} + } + end + for line in string.gmatch(dns, '[^' .. "," .. ']+') do + if not check_dns(domain, line) then + table.insert(list1[domain].dns, line) + end + end + end + + local function set_domain_ipset(domain, ipset) + if domain == "" or domain:find("#") then + return + end + if not ipset then + return + end + if not list1[domain] then + list1[domain] = { + dns = {}, + ipsets = {} + } + end + for line in string.gmatch(ipset, '[^' .. "," .. ']+') do + if not check_ipset(domain, line) then + table.insert(list1[domain].ipsets, line) + end + end + end + + local function add_excluded_domain(domain) + if domain == "" or domain:find("#") then + return + end + excluded_domain[domain] = true + end + + local function check_excluded_domain(domain) + if domain == "" or domain:find("#") then + return false + end + if excluded_domain[domain] then + return true + end + local pos = domain:find(".", 1, true) + while pos do + if excluded_domain[domain:sub(pos + 1)] then + return true + end + pos = domain:find(".", pos + 1, true) + end + return false + end + + local function foreach_geosite(list_arg, callback) + local geosite_path = api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/" + geosite_path = geosite_path:match("^(.*)/") .. "/geosite.dat" + if not fs.access(geosite_path) then return end + local bin = api.finded_com("geoview") + if not (bin and list_arg) then return end + local cmd = string.format("%q -type geosite -action extract -input %q -list %q -lowmem=true", bin, geosite_path, list_arg) + local pipe = io.popen(cmd) + if not pipe then return end + for line in pipe:lines() do + if line ~= "" then callback(line) end + end + pipe:close() + end + + local cache_text = "" + local nodes_address_md5 = sys.exec("echo -n $(uci show passwall | grep '\\.address') | md5sum") + local new_rules = sys.exec("echo -n $(find /usr/share/passwall/rules -type f | xargs md5sum)") + local new_text = TMP_DNSMASQ_PATH .. DNSMASQ_CONF_FILE .. DEFAULT_DNS .. LOCAL_DNS .. TUN_DNS .. USE_DEFAULT_DNS .. CHINADNS_DNS .. USE_DIRECT_LIST .. USE_PROXY_LIST .. USE_BLOCK_LIST .. USE_GFW_LIST .. CHN_LIST .. DEFAULT_PROXY_MODE .. NO_PROXY_IPV6 .. nodes_address_md5 .. new_rules .. NFTFLAG + if fs.access(CACHE_TEXT_FILE) then + for line in io.lines(CACHE_TEXT_FILE) do + cache_text = line + end + end + + if cache_text ~= new_text then + api.remove(CACHE_DNS_PATH .. "*") + end + + local dnsmasq_default_dns + if USE_DEFAULT_DNS ~= "nil" then + if USE_DEFAULT_DNS == "direct" then + dnsmasq_default_dns = LOCAL_DNS + end + if USE_DEFAULT_DNS == "remote" then + dnsmasq_default_dns = TUN_DNS + end + if USE_DEFAULT_DNS == "remote" and CHN_LIST == "direct" then + dnsmasq_default_dns = TUN_DNS + end + end + + local only_global + if DEFAULT_PROXY_MODE == "proxy" and CHN_LIST == "0" and USE_GFW_LIST == "0" then + --没有启用中国列表和GFW列表时 + dnsmasq_default_dns = TUN_DNS + only_global = 1 + end + if USE_DEFAULT_DNS == "chinadns_ng" and CHINADNS_DNS ~= "0" then + dnsmasq_default_dns = CHINADNS_DNS + USE_CHINADNS_NG = "1" + end + + local setflag_4= (NFTFLAG == "1") and "4#inet#passwall#" or "" + local setflag_6= (NFTFLAG == "1") and "6#inet#passwall#" or "" + + if not fs.access(CACHE_DNS_PATH) then + fs.mkdir(CACHE_DNS_PATH) + + --屏蔽列表 + if USE_CHINADNS_NG == "0" and USE_BLOCK_LIST == "1" then + local geosite_arg = "" + local f = io.open("/usr/share/passwall/rules/block_host") + if f then + for line in f:lines() do + if not line:find("#") and line:find("geosite:") then + line = string.match(line, ":([^:]+)$") + geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line + else + line = api.get_std_domain(line) + if line ~= "" and not line:find("#") and not line:find(":") then + set_domain_address(line, "") + end + end + end + f:close() + end + if USE_GEOVIEW == "1" and geosite_arg ~= "" then + foreach_geosite(geosite_arg, function(line) + set_domain_address(line, "") + end) + log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成") + end + end + + local fwd_dns + local no_ipv6 + + --始终用国内DNS解析节点域名 + if true then + fwd_dns = LOCAL_DNS + if USE_CHINADNS_NG == "1" then + fwd_dns = nil + else + local sets = { + setflag_4 .. "psw_vps", + setflag_6 .. "psw_vps6" + } + local function process_address(address) + address = (address or ""):lower() + if api.vps_domain_exclude(address) then return end + if datatypes.hostname(address) then + set_domain_dns(address, fwd_dns) + set_domain_ipset(address, table.concat(sets, ",")) + end + end + api.uci_foreach_c("nodes", function(t) + process_address(t.address) + process_address(t.download_address) + local dns, _ = api.get_domain_port_from_url(t.domain_resolver_dns or t.domain_resolver_dns_https or "") + if dns and dns ~= "" then + process_address(dns) + end + end) + api.uci_foreach_c("subscribe_list", function(t) --订阅链接 + local url, _ = api.get_domain_port_from_url(t.url or "") + if url and url ~= "" then + process_address(url) + end + end) + log(string.format(" - 节点列表中的域名(vpslist):%s", fwd_dns or "默认")) + end + end + + --直连(白名单)列表 + if USE_DIRECT_LIST == "1" then + fwd_dns = LOCAL_DNS + if USE_CHINADNS_NG == "1" then + fwd_dns = nil + end + if fwd_dns then + local sets = { + setflag_4 .. "psw_white", + setflag_6 .. "psw_white6" + } + --始终用国内DNS解析直连(白名单)列表 + local geosite_arg = "" + local f = io.open("/usr/share/passwall/rules/direct_host") + if f then + for line in f:lines() do + if not line:find("#") and line:find("geosite:") then + line = string.match(line, ":([^:]+)$") + geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line + else + line = api.get_std_domain(line) + if line ~= "" and not line:find("#") and not line:find(":") then + add_excluded_domain(line) + set_domain_dns(line, fwd_dns) + set_domain_ipset(line, table.concat(sets, ",")) + end + end + end + f:close() + log(string.format(" - 域名白名单(whitelist):%s", fwd_dns or "默认")) + end + if USE_GEOVIEW == "1" and geosite_arg ~= "" then + foreach_geosite(geosite_arg, function(line) + add_excluded_domain(line) + set_domain_dns(line, fwd_dns) + set_domain_ipset(line, table.concat(sets, ",")) + end) + log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)完成") + end + end + end + + --代理(黑名单)列表 + if USE_PROXY_LIST == "1" then + fwd_dns = TUN_DNS + if USE_CHINADNS_NG == "1" then + fwd_dns = nil + end + if fwd_dns then + local set_name = "psw_black" + local set6_name = "psw_black6" + if FLAG ~= "default" then + set_name = "psw_" .. FLAG .. "_black" + set6_name = "psw_" .. FLAG .. "_black6" + end + local sets = { + setflag_4 .. set_name + } + if NO_PROXY_IPV6 ~= "1" then + table.insert(sets, setflag_6 .. set6_name) + end + --始终使用远程DNS解析代理(黑名单)列表 + local geosite_arg = "" + local f = io.open("/usr/share/passwall/rules/proxy_host") + if f then + for line in f:lines() do + if not line:find("#") and line:find("geosite:") then + line = string.match(line, ":([^:]+)$") + geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line + else + line = api.get_std_domain(line) + if line ~= "" and not line:find("#") and not line:find(":") then + add_excluded_domain(line) + if NO_PROXY_IPV6 == "1" then + set_domain_address(line, "::") + end + set_domain_dns(line, fwd_dns) + set_domain_ipset(line, table.concat(sets, ",")) + end + end + end + f:close() + log(string.format(" - 代理域名表(blacklist):%s", fwd_dns or "默认")) + end + if USE_GEOVIEW == "1" and geosite_arg ~= "" then + foreach_geosite(geosite_arg, function(line) + add_excluded_domain(line) + if NO_PROXY_IPV6 == "1" then + set_domain_address(line, "::") + end + set_domain_dns(line, fwd_dns) + set_domain_ipset(line, table.concat(sets, ",")) + end) + log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)完成") + end + end + end + + --GFW列表 + if USE_GFW_LIST == "1" then + fwd_dns = TUN_DNS + if USE_CHINADNS_NG == "1" then + fwd_dns = nil + end + if fwd_dns then + local set_name = "psw_gfw" + local set6_name = "psw_gfw6" + if FLAG ~= "default" then + set_name = "psw_" .. FLAG .. "_gfw" + set6_name = "psw_" .. FLAG .. "_gfw6" + end + local sets = { + setflag_4 .. set_name + } + if NO_PROXY_IPV6 ~= "1" then + table.insert(sets, setflag_6 .. set6_name) + end + local f = io.open("/usr/share/passwall/rules/gfwlist") + if f then + for line in f:lines() do + if line ~= "" and not line:find("#") and not check_excluded_domain(line) then + if NO_PROXY_IPV6 == "1" then + set_domain_address(line, "::") + end + if dnsmasq_default_dns == fwd_dns then + fwd_dns = nil + else + set_domain_dns(line, fwd_dns) + end + set_domain_ipset(line, table.concat(sets, ",")) + end + end + f:close() + log(string.format(" - 防火墙域名表(gfwlist):%s", fwd_dns or "默认")) + end + end + end + + --中国列表 + if CHN_LIST ~= "0" then + fwd_dns = nil + if CHN_LIST == "direct" then + fwd_dns = LOCAL_DNS + end + if CHN_LIST == "proxy" then + fwd_dns = TUN_DNS + end + if USE_CHINADNS_NG == "1" then + fwd_dns = nil + end + if fwd_dns then + local sets = { + setflag_4 .. "psw_chn", + setflag_6 .. "psw_chn6" + } + if CHN_LIST == "proxy" then + if NO_PROXY_IPV6 == "1" then + sets = { + setflag_4 .. "psw_chn" + } + end + end + local f = io.open("/usr/share/passwall/rules/chnlist") + if f then + for line in f:lines() do + if line ~= "" and not line:find("#") and not check_excluded_domain(line) then + if CHN_LIST == "proxy" and NO_PROXY_IPV6 == "1" then + set_domain_address(line, "::") + end + if dnsmasq_default_dns == fwd_dns then + fwd_dns = nil + else + set_domain_dns(line, fwd_dns) + end + set_domain_ipset(line, table.concat(sets, ",")) + end + end + f:close() + log(string.format(" - 中国域名表(chnroute):%s", fwd_dns or "默认")) + end + end + end + + --分流规则 + if IS_SHUNT_NODE and USE_CHINADNS_NG == "0" then + local t = api.uci_get_c(NODE) + local default_node_id = t["default_node"] or "_direct" + api.uci_foreach_c("shunt_rules", function(s) + local _node_id = t[s[".name"]] + if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then + if _node_id == "_default" then + _node_id = default_node_id + end + + fwd_dns = nil + no_ipv6 = nil + + local sets = {} + + if _node_id == "_direct" then + fwd_dns = LOCAL_DNS + if USE_DIRECT_LIST == "1" then + table.insert(sets, setflag_4 .. "psw_white") + table.insert(sets, setflag_6 .. "psw_white6") + else + local set_name = "psw_shunt" + local set6_name = "psw_shunt6" + if FLAG ~= "default" then + set_name = "psw_" .. FLAG .. "_shunt" + set6_name = "psw_" .. FLAG .. "_shunt6" + end + table.insert(sets, setflag_4 .. set_name) + table.insert(sets, setflag_6 .. set6_name) + end + else + local set_name = "psw_shunt" + local set6_name = "psw_shunt6" + if FLAG ~= "default" then + set_name = "psw_" .. FLAG .. "_shunt" + set6_name = "psw_" .. FLAG .. "_shunt6" + end + fwd_dns = TUN_DNS + table.insert(sets, setflag_4 .. set_name) + if NO_PROXY_IPV6 ~= "1" then + table.insert(sets, setflag_6 .. set6_name) + else + no_ipv6 = true + end + end + + local domain_list = s.domain_list or "" + local geosite_arg = "" + for line in string.gmatch(domain_list, "[^\r\n]+") do + if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") and not line:find("rule-set:") and not line:find("rs:") then + if line:find("geosite:") then + line = string.match(line, ":([^:]+)$") + geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line + else + if line:find("domain:") or line:find("full:") then + line = string.match(line, ":([^:]+)$") + end + line = api.get_std_domain(line) + add_excluded_domain(line) + + if no_ipv6 then + set_domain_address(line, "::") + end + set_domain_dns(line, fwd_dns) + set_domain_ipset(line, table.concat(sets, ",")) + end + end + end + + if USE_GFW_LIST == "1" and CHN_LIST == "0" and USE_GEOVIEW == "1" and geosite_arg ~= "" then --仅GFW模式解析geosite + foreach_geosite(geosite_arg, function(line) + add_excluded_domain(line) + if no_ipv6 then + set_domain_address(line, "::") + end + set_domain_dns(line, fwd_dns) + set_domain_ipset(line, table.concat(sets, ",")) + end) + log(string.format(" - 解析分流规则(%s) Geosite 完成", s.remarks)) + end + + if _node_id ~= "_direct" then + log(string.format(" - Sing-Box/Xray分流规则(%s):%s", s.remarks, fwd_dns or "默认")) + end + end + end) + elseif only_global == 1 and NO_PROXY_IPV6 == "1" then + --节点:固定节点 + --代理模式:全局模式 + --过滤代理域名 IPv6:启用 + --禁止解析所有IPv6记录 + list1["#"] = { + dns = {}, + ipsets = {}, + address = "::" + } + end + + if list1 and next(list1) then + local address_out = io.open(CACHE_DNS_PATH .. "/000-address.conf", "a") + local server_out = io.open(CACHE_DNS_PATH .. "/001-server.conf", "a") + local ipset_out = io.open(CACHE_DNS_PATH .. "/ipset.conf", "a") + local set_name = "ipset" + if NFTFLAG == "1" then + set_name = "nftset" + end + for key, value in pairs(list1) do + if value.address then + local domain = "." .. key + if key == "#" then + domain = key + end + address_out:write(string.format("address=/%s/%s", domain, value.address) .. "\n") + end + if value.dns and #value.dns > 0 then + for i, dns in ipairs(value.dns) do + server_out:write(string.format("server=/.%s/%s", key, dns) .. "\n") + end + end + if value.ipsets and #value.ipsets > 0 then + local ipsets_str = "" + for i, ipset in ipairs(value.ipsets) do + ipsets_str = ipsets_str .. ipset .. "," + end + ipsets_str = ipsets_str:sub(1, #ipsets_str - 1) + ipset_out:write(string.format("%s=/.%s/%s", set_name, key, ipsets_str) .. "\n") + end + end + address_out:close() + server_out:close() + ipset_out:close() + end + + local f_out = io.open(CACHE_TEXT_FILE, "a") + f_out:write(new_text) + f_out:close() + end + + if USE_CHINADNS_NG == "0" then + api.remove(TMP_DNSMASQ_PATH) + fs.symlink(CACHE_DNS_PATH, TMP_DNSMASQ_PATH) + end + + if DNSMASQ_CONF_FILE ~= "nil" then + local conf_lines = {} + if LISTEN_PORT then + --Copy dnsmasq instance + conf_lines = copy_instance({["-LISTEN_PORT"] = LISTEN_PORT, ["-TMP_DNSMASQ_PATH"] = TMP_DNSMASQ_PATH, ["-return"] = "1"}) + --dhcp.leases to hosts + local hosts = api.CACHE_PATH .. "/dhcp-hosts" + sys.call("touch " .. hosts) + tinsert(conf_lines, "addn-hosts=" .. hosts) + else + --Modify the default dnsmasq service + end + if USE_CHINADNS_NG == "0" then + tinsert(conf_lines, string.format("conf-dir=%s", TMP_DNSMASQ_PATH)) + end + if dnsmasq_default_dns then + for s in string.gmatch(dnsmasq_default_dns, '[^' .. "," .. ']+') do + tinsert(conf_lines, string.format("server=%s", s)) + end + tinsert(conf_lines, "all-servers") + tinsert(conf_lines, "no-poll") + tinsert(conf_lines, "no-resolv") + if USE_CHINADNS_NG == "0" then + log(string.format(" - 默认:%s", dnsmasq_default_dns)) + end + + if FLAG == "default" then + api.set_cache_var("DEFAULT_DNS", DEFAULT_DNS) + end + end + if #conf_lines > 0 then + local conf_out = io.open(DNSMASQ_CONF_FILE, "a") + if conf_out then + conf_out:write(table.concat(conf_lines, "\n")) + conf_out:write("\n") + conf_out:close() + end + end + end + + if USE_CHINADNS_NG == "0" then + log(" - PassWall必须依赖于Dnsmasq,如果你自行配置了错误的DNS流程,将会导致域名(直连/代理域名)分流失效!!!") + end +end + +_G.stretch = stretch +_G.restart = restart +_G.logic_restart = logic_restart +_G.copy_instance = copy_instance +_G.add_rule = add_rule + +if arg[1] then + local func =_G[arg[1]] + if func then + func(api.get_function_args(arg)) + end +end diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/geoview.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/geoview.lua index bc11424e..13d17fb0 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/geoview.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/geoview.lua @@ -6,13 +6,13 @@ local geo_dir = (api.uci_get_c("@global_rules[0]", "v2ray_location_asset") or "/ local geosite_path = geo_dir .. "/geosite.dat" local geoip_path = geo_dir .. "/geoip.dat" if fs.access(geosite_path) and fs.access(geoip_path) then - f = SimpleForm(api.c_config) - f.reset = false - f.submit = false + f = SimpleForm(api.c_config) + f.reset = false + f.submit = false - local obj = Template(api.appname .. "/rule/geoview") - obj.api = api - f:append(obj) + local obj = Template(api.appname .. "/rule/geoview") + obj.api = api + f:append(obj) end return f diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua index af15398a..8c8382ce 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_list.lua @@ -25,25 +25,25 @@ o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)") o.default = o.keylist[3] if true then - m:appendTemplate("/node_list/node_list") + m:appendTemplate("/node_list/node_list") - if luci.http.formvalue("cbi.submit") == "1" then - local group_order = {} - group_order = luci.http.formvaluetable("group.order") - if group_order then - for k, v in pairs(group_order) do - if v and v~= "" then - local new_order = {} - string.gsub(v, "[^" .. " " .. "]+", function(w) - new_order[#new_order + 1] = w - end) - for idx, name in ipairs(new_order) do - m.uci:reorder(m.config, name, idx - 1) - end - end - end - end - end + if luci.http.formvalue("cbi.submit") == "1" then + local group_order = {} + group_order = luci.http.formvaluetable("group.order") + if group_order then + for k, v in pairs(group_order) do + if v and v~= "" then + local new_order = {} + string.gsub(v, "[^" .. " " .. "]+", function(w) + new_order[#new_order + 1] = w + end) + for idx, name in ipairs(new_order) do + m.uci:reorder(m.config, name, idx - 1) + end + end + end + end + end end return api.return_map(m) diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua index dd4ba20a..18b568ec 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua @@ -112,8 +112,8 @@ end o = s:option(DummyValue, "_update", translate("Manual subscription All")) o.rawhtml = true o.cfgvalue = function(self, section) - return string.format([[ - ]], + return string.format([[ + ]], translate("Manual subscription All")) end @@ -210,8 +210,8 @@ end o = s:option(DummyValue, "_update", translate("Manual subscription")) o.rawhtml = true o.cfgvalue = function(self, section) - return string.format([[ - ]], + return string.format([[ + ]], section, translate("Manual subscription")) end diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua index c2fc0347..5f5e293a 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua @@ -1,976 +1,976 @@ -local singbox_bin = api.finded_com("sing-box") - -if not singbox_bin then - return -end - --- [[ sing-box ]] -local m, s1 = ... -local type_name = "sing-box" - -s1.fields["type"]:value(type_name, "Sing-Box") -if not s1.fields["type"].default then - s1.fields["type"].default = type_name -end - -if s1.val["type"] ~= type_name then - return -end - -local s = NamedSection(m, arg[1], "server") -s.type_name = type_name -s.option_prefix = "singbox_" - -local formvalue_proto = luci.http.formvalue(formvalue_key .. "protocol") - -if formvalue_proto then s1.val["protocol"] = formvalue_proto end - -local arg_select_proto = luci.http.formvalue("select_proto") or "" - -local ss_method_new_list = { - "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" -} - -local ss_method_old_list = { - "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5", "chacha20-ietf", "xchacha20", -} - -local security_list = { "none", "auto", "aes-128-gcm", "chacha20-poly1305", "zero" } - -local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'") - -local singbox_version = api.get_app_version("sing-box"):match("[^v]+") - -o = s:option(ListValue, "protocol", translate("Protocol")) -o:value("socks", "Socks") -o:value("http", "HTTP") -o:value("shadowsocks", "Shadowsocks") -if singbox_tags:find("with_shadowsocksr") then - o:value("shadowsocksr", "ShadowsocksR") -end -o:value("vmess", "Vmess") -o:value("trojan", "Trojan") -if singbox_tags:find("with_wireguard") then - o:value("wireguard", "WireGuard") -end -if singbox_tags:find("with_quic") then - o:value("hysteria", "Hysteria") -end -o:value("vless", "VLESS") -if singbox_tags:find("with_quic") then - o:value("tuic", "TUIC") -end -if singbox_tags:find("with_quic") then - o:value("hysteria2", "Hysteria2") -end -o:value("anytls", "AnyTLS") -o:value("ssh", "SSH") -if singbox_tags:find("with_naive_outbound") then - o:value("naive", "NaïveProxy") -end -o:value("_urltest", translate("URLTest")) -o:value("_shunt", translate("Shunt")) -o:value("_iface", translate("Custom Interface")) -function o.custom_cfgvalue(self, section) - if arg_select_proto ~= "" then - return arg_select_proto - else - return m:get(section, self.config_option) - end -end - -local load_urltest_options = s1.val["protocol"] == "_urltest" or arg_select_proto == "_urltest" -local load_shunt_options = s1.val["protocol"] == "_shunt" or arg_select_proto == "_shunt" -local load_iface_options = s1.val["protocol"] == "_iface" or arg_select_proto == "_iface" -local load_normal_options = true -if load_urltest_options or load_shunt_options or load_iface_options then - load_normal_options = nil -end -if not arg_select_proto:find("_") then - load_normal_options = true -end - -local netdev_list = api.get_network_devices() -local node_list = api.get_node_list() - -if load_urltest_options then -- [[ URLTest Start ]] - o = s:option(ListValue, "node_add_mode", translate("Node Addition Method")) - o:depends({ protocol = "_urltest" }) - o.default = "manual" - o:value("manual", translate("Manual")) - o:value("batch", translate("Batch")) - - o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, document")) - o:depends({ node_add_mode = "manual" }) - o.widget = "checkbox" - o.template = m:template_path("/cbi/nodes_multivalue") - o.group = {} - for k1, v1 in pairs(node_list) do - if k1 == "socks_list" or k1 == "normal_list" then - for i, v in ipairs(v1) do - o:value(v.id, v.remark) - o.group[#o.group+1] = v.group or "" - end - end - end - -- Reading the old DynamicList - function o.custom_cfgvalue(self, section) - return table.concat(m:get(section, "urltest_node") or {}, " ") - end - -- Write-and-hold DynamicList - function o.custom_write(self, section, value) - local old = m:get(section, "urltest_node") or {} - local new, set = {}, {} - for v in value:gmatch("%S+") do - new[#new + 1] = v - set[v] = 1 - end - for _, v in ipairs(old) do - if not set[v] then - m:set(section, "urltest_node", new) - return - end - set[v] = nil - end - for _ in pairs(set) do - m:set(section, "urltest_node", new) - return - end - end - - o = s:option(MultiValue, "node_group", translate("Select Group")) - o:depends({ node_add_mode = "batch" }) - o.widget = "checkbox" - o:value("default", translate("default")) - for k, v in pairs(groups) do - o:value(api.UrlEncode(k), k) - end - - o = s:option(Value, "node_match_rule", translate("Node Matching Rules")) - o:depends({ node_add_mode = "batch" }) - local descrStr = "Example: ^A && B && !C && D$
" - descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
" - descrStr = descrStr .. "Conditions are joined by && (AND), and their order does not affect the result.
" - descrStr = descrStr .. "Multiple groups can be separated by || (OR), matching succeeds if any group matches.
" - descrStr = descrStr .. "Example: A && B || C && D means (A AND B) OR (C AND D)." - o.description = translate(descrStr) - - o = s:option(Value, "urltest_url", translate("Probe URL")) - o:depends({ protocol = "_urltest" }) - o:value("https://cp.cloudflare.com/", "Cloudflare") - o:value("https://www.gstatic.com/generate_204", "Gstatic") - o:value("https://www.google.com/generate_204", "Google") - o:value("https://www.youtube.com/generate_204", "YouTube") - o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)") - o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)") - o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)") - o.default = o.keylist[3] - o.description = translate("The URL used to detect the connection status.") - - o = s:option(Value, "urltest_interval", translate("Test interval")) - o:depends({ protocol = "_urltest" }) - o.default = "3m" - o.placeholder = "3m" - o.description = translate("The interval between initiating probes.") .. "
" .. - translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. - translate("When the unit is not filled in, it defaults to seconds.") .. "
" .. - translate("Test interval must be less or equal than idle timeout.") - - o = s:option(Value, "urltest_tolerance", translate("Test tolerance"), translate("The test tolerance in milliseconds.")) - o:depends({ protocol = "_urltest" }) - o.datatype = "uinteger" - o.placeholder = "50" - o.default = "50" - - o = s:option(Value, "urltest_idle_timeout", translate("Idle timeout")) - o:depends({ protocol = "_urltest" }) - o.placeholder = "30m" - o.default = "30m" - o.description = translate("The idle timeout.") .. "
" .. - translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. - translate("When the unit is not filled in, it defaults to seconds.") - - o = s:option(Flag, "urltest_interrupt_exist_connections", translate("Interrupt existing connections")) - o:depends({ protocol = "_urltest" }) - o.default = "0" - o.description = translate("Interrupt existing connections when the selected outbound has changed.") -end -- [[ URLTest End ]] - -if load_iface_options then -- [[ Custom Interface Start ]] - o = s:option(Value, "iface", translate("Interface")) - o:depends({ protocol = "_iface" }) - for _, d in ipairs(netdev_list) do - o:value(d.name, d.label) - end -end -- [[ Custom Interface End ]] - - --- [[ Normal single node Start ]] -if load_normal_options then - -o = s:option(Value, "address", translate("Address (Support Domain Name)")) - -o = s:option(Value, "port", translate("Port")) -o.datatype = "port" - -o = s:option(Value, "uuid", translate("ID")) -o.password = true -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "tuic" }) - -o = s:option(Value, "username", translate("Username")) -o:depends({ protocol = "http" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "ssh" }) -o:depends({ protocol = "naive" }) - -o = s:option(Value, "password", translate("Password")) -o.password = true -o:depends({ protocol = "http" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "shadowsocksr" }) -o:depends({ protocol = "trojan" }) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "anytls" }) -o:depends({ protocol = "ssh" }) -o:depends({ protocol = "naive" }) - -o = s:option(ListValue, "security", translate("Encrypt Method")) -for a, t in ipairs(security_list) do o:value(t) end -o:depends({ protocol = "vmess" }) - -o = s:option(ListValue, "ss_method", translate("Encrypt Method")) -for a, t in ipairs(ss_method_new_list) do o:value(t) end -for a, t in ipairs(ss_method_old_list) do o:value(t) end -o:depends({ protocol = "shadowsocks" }) - -if singbox_tags:find("with_shadowsocksr") then - o = s:option(ListValue, "ssr_method", translate("Encrypt Method")) - for a, t in ipairs(ss_method_old_list) do o:value(t) end - o:depends({ protocol = "shadowsocksr" }) - - local ssr_protocol_list = { - "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", - "auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5", - "auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c", - "auth_chain_d", "auth_chain_e", "auth_chain_f" - } - - o = s:option(ListValue, "ssr_protocol", translate("Protocol")) - for a, t in ipairs(ssr_protocol_list) do o:value(t) end - o:depends({ protocol = "shadowsocksr" }) - - o = s:option(Value, "ssr_protocol_param", translate("Protocol_param")) - o:depends({ protocol = "shadowsocksr" }) - - local ssr_obfs_list = { - "plain", "http_simple", "http_post", "random_head", "tls_simple", - "tls1.0_session_auth", "tls1.2_ticket_auth" - } - - o = s:option(ListValue, "ssr_obfs", translate("Obfs")) - for a, t in ipairs(ssr_obfs_list) do o:value(t) end - o:depends({ protocol = "shadowsocksr" }) - - o = s:option(Value, "ssr_obfs_param", translate("Obfs_param")) - o:depends({ protocol = "shadowsocksr" }) -end - -o = s:option(Flag, "uot", translate("UDP over TCP")) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "naive" }) - -o = s:option(Value, "alter_id", "Alter ID") -o.datatype = "uinteger" -o.default = "0" -o:depends({ protocol = "vmess" }) - -o = s:option(Flag, "global_padding", "global_padding", translate("Protocol parameter. Will waste traffic randomly if enabled.")) -o.default = "0" -o:depends({ protocol = "vmess" }) - -o = s:option(Flag, "authenticated_length", "authenticated_length", translate("Protocol parameter. Enable length block encryption.")) -o.default = "0" -o:depends({ protocol = "vmess" }) - -o = s:option(ListValue, "flow", translate("flow")) -o.default = "" -o:value("", translate("Disable")) -o:value("xtls-rprx-vision") -o:depends({ protocol = "vless", tls = true }) - -if singbox_tags:find("with_quic") then - o = s:option(Value, "hysteria_hop", translate("Port hopping range")) - o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_hop_interval", translate("Hop Interval(second)"), translate("Example:") .. "30 (≥5)") - o.datatype = "uinteger" - o.placeholder = "30" - o.default = "30" - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_obfs", translate("Obfs Password")) - o:depends({ protocol = "hysteria" }) - - o = s:option(ListValue, "hysteria_auth_type", translate("Auth Type")) - o:value("disable", translate("Disable")) - o:value("string", translate("STRING")) - o:value("base64", translate("BASE64")) - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_auth_password", translate("Auth Password")) - o.password = true - o:depends({ protocol = "hysteria", hysteria_auth_type = "string"}) - o:depends({ protocol = "hysteria", hysteria_auth_type = "base64"}) - - o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps")) - o.default = "10" - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps")) - o.default = "50" - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window")) - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_recv_window", translate("QUIC connection receive window")) - o:depends({ protocol = "hysteria" }) - - o = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection")) - o:depends({ protocol = "hysteria" }) -end - -if singbox_tags:find("with_quic") then - o = s:option(ListValue, "tuic_congestion_control", translate("Congestion control algorithm")) - o.default = "cubic" - o:value("bbr", translate("BBR")) - o:value("cubic", translate("CUBIC")) - o:value("new_reno", translate("New Reno")) - o:depends({ protocol = "tuic" }) - - o = s:option(ListValue, "tuic_udp_relay_mode", translate("UDP relay mode")) - o.default = "native" - o:value("native", translate("native")) - o:value("quic", translate("QUIC")) - o:depends({ protocol = "tuic" }) - - --[[ - o = s:option(Flag, "tuic_udp_over_stream", translate("UDP over stream")) - o:depends({ protocol = "tuic" }) - ]]-- - - o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake")) - o.default = 0 - o:depends({ protocol = "tuic" }) - - o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)")) - o.datatype = "uinteger" - o.default = "3" - o:depends({ protocol = "tuic" }) - - o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN")) - o.default = "default" - o:value("default", translate("Default")) - o:value("h3") - o:value("h2") - o:value("h3,h2") - o:value("http/1.1") - o:value("h2,http/1.1") - o:value("h3,h2,http/1.1") - o:value("spdy/3.1") - o:value("h3,spdy/3.1") - o:depends({ protocol = "tuic" }) -end - -if singbox_tags:find("with_quic") then - o = s:option(Value, "hysteria2_hop", translate("Port hopping range")) - o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") - o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - - o = s:option(Value, "hysteria2_hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) - o.datatype = "or(uinteger,portrange)" - o.placeholder = "30" - o.default = "30" - o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - - o = s:option(Flag, "hysteria2_realms", translate("Realms")) - o.default = "0" - if api.compare_versions(singbox_version, ">=", "1.14.0") then - o:depends({ protocol = "hysteria2"}) - else - o:depends({ protocol = "__hide"}) - end - - o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") - o:depends({ hysteria2_realms = "1" }) - o.validate = function(self, value) - value = api.trim(value) - local realm = api.parse_realm_uri(value) - if realm then return value end - return nil, translate("Invalid Realm URL.") - end - - o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) - o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } - o:depends({ hysteria2_realms = "1" }) - - o = s:option(Value, "hysteria2_auth_password", translate("Auth Password")) - o.password = true - o:depends({ protocol = "hysteria2"}) - - o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) - o:value("", translate("Disable")) - o:value("salamander") - o:value("gecko") - o:depends({ protocol = "hysteria2" }) - - o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) - o:depends({ hysteria2_obfs_type = "salamander" }) - o:depends({ hysteria2_obfs_type = "gecko" }) - - o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) - o.datatype = "uinteger" - o.placeholder = "512" - o.default = "512" - o:depends({ hysteria2_obfs_type = "gecko" }) - - o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) - o.datatype = "uinteger" - o.placeholder = "1200" - o.default = "1200" - o:depends({ hysteria2_obfs_type = "gecko" }) - - o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) - o:depends({ protocol = "hysteria2" }) - - o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) - o:depends({ protocol = "hysteria2" }) - - o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") - o.datatype = "range(4,120)" - o:depends({ protocol = "hysteria2"}) - - o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") - o.datatype = "range(2,60)" - o:depends({ protocol = "hysteria2"}) - - o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection")) - o.default = "0" - o:depends({ protocol = "hysteria2"}) -end - --- [[ SSH config start ]] -- -o = s:option(TextValue, "ssh_priv_key", translate("Private Key")) -o.rows = 5 -o.wrap = "off" -o:depends({ protocol = "ssh" }) -o.validate = function(self, value) - value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") - return value -end - -o = s:option(Value, "ssh_priv_key_pp", translate("Private Key Passphrase")) -o.password = true -o:depends({ protocol = "ssh" }) - -o = s:option(DynamicList, "ssh_host_key", translate("Host Key"), translate("Accept any if empty.")) -o:depends({ protocol = "ssh" }) - -o = s:option(DynamicList, "ssh_host_key_algo", translate("Host Key Algorithms")) -o:depends({ protocol = "ssh" }) - -o = s:option(Value, "ssh_client_version", translate("Client Version"), translate("Random version will be used if empty.")) -o:depends({ protocol = "ssh" }) --- [[ SSH config end ]] -- - --- [[ naive start ]] -- -o = s:option(Value, "naive_insecure_concurrency", translate("Concurrent Tunnels")) -o.datatype = "uinteger" -o.placeholder = "0" -o.default = "0" -o:depends({ protocol = "naive" }) - -o = s:option(Flag, "naive_quic", translate("QUIC")) -o.default = 0 -o:depends({ protocol = "naive" }) - -o = s:option(ListValue, "naive_congestion_control", translate("Congestion control algorithm")) -o.default = "bbr" -o:value("bbr", translate("BBR")) -o:value("bbr2", translate("BBRv2")) -o:value("cubic", translate("CUBIC")) -o:value("reno", translate("New Reno")) -o:depends({ naive_quic = "1" }) --- [[ naive end ]] -- - -o = s:option(Flag, "tls", translate("TLS")) -o.default = 0 -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "http" }) -o:depends({ protocol = "trojan" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "anytls" }) - -o = s:option(ListValue, "alpn", translate("ALPN")) -o.default = "default" -o:value("default", translate("Default")) -o:value("h3") -o:value("h2") -o:value("h3,h2") -o:value("http/1.1") -o:value("h2,http/1.1") -o:value("h3,h2,http/1.1") -o:depends({ tls = true }) -o:depends({ protocol = "hysteria" }) - -o = s:option(Flag, "tls_disable_sni", translate("Disable SNI"), translate("Do not send server name in ClientHello.")) -o.default = "0" -o:depends({ tls = true }) -o:depends({ protocol = "hysteria"}) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria2" }) - -o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain")) -o:depends({ tls = true }) -o:depends({ protocol = "hysteria"}) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria2" }) -o:depends({ protocol = "naive" }) - -o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) -o.default = "0" -o:depends({ tls = true }) -o:depends({ protocol = "hysteria"}) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria2" }) - -o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)")) -o.default = "0" -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "hysteria"}) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria2" }) -o:depends({ protocol = "naive" }) - -o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format.")) -o.default = "" -o.rows = 5 -o.wrap = "off" -o:depends({ tls_certificate = true }) -o.validate = function(self, value) - value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") - return value -end - -o = s:option(DynamicList, "cipherSuites", translate("Cipher Suites"), '***' .. " " .. translate("Configures the list of supported cipher suites.")) -o:value("TLS_AES_128_GCM_SHA256") -o:value("TLS_AES_256_GCM_SHA384") -o:value("TLS_CHACHA20_POLY1305_SHA256") -o:value("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA") -o:value("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA") -o:value("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA") -o:value("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA") -o:value("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") -o:value("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384") -o:value("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") -o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") -o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") -o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") -o:depends({ tls = true }) - -o = s:option(Flag, "ech", translate("ECH")) -o.default = "0" -o:depends({ tls = true, flow = "", reality = false }) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria" }) -o:depends({ protocol = "hysteria2", hysteria2_realms = false }) -o:depends({ protocol = "naive" }) - -o = s:option(TextValue, "ech_config", translate("ECH Config")) -o.default = "" -o.rows = 5 -o.wrap = "off" -o:depends({ ech = true }) -o.validate = function(self, value) - value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") - return value -end - -o = s:option(Value, "ech_query_server_name", translate("ECH Query Domain"), translate("Overrides the domain name used for ECH HTTPS record queries.")) -o:depends({ ech = true }) - -if singbox_tags:find("with_utls") then - o = s:option(Flag, "utls", translate("uTLS")) - o.default = "0" - o:depends({ tls = true }) - - o = s:option(ListValue, "fingerprint", translate("Finger Print")) - o:value("chrome") - o:value("firefox") - o:value("edge") - o:value("safari") - o:value("360") - o:value("qq") - o:value("ios") - o:value("android") - o:value("random") - o:value("randomized") - o.default = "chrome" - o:depends({ utls = true }) - - -- [[ REALITY ]] -- - o = s:option(Flag, "reality", translate("REALITY")) - o.default = 0 - o:depends({ protocol = "vless", tls = true }) - o:depends({ protocol = "vmess", tls = true }) - o:depends({ protocol = "shadowsocks", tls = true }) - o:depends({ protocol = "socks", tls = true }) - o:depends({ protocol = "trojan", tls = true }) - o:depends({ protocol = "anytls", tls = true }) - - o = s:option(Value, "reality_publicKey", translate("Public Key")) - o:depends({ reality = true }) - - o = s:option(Value, "reality_shortId", translate("Short Id")) - o:depends({ reality = true }) -end - -o = s:option(Flag, "anytls_disable_reuse", translate("Disable TLS Reuse")) -o.default = 0 -o:depends({ protocol = "anytls" }) - -o = s:option(ListValue, "transport", translate("Transport")) -o:value("tcp", "TCP") -o:value("http", "HTTP") -o:value("ws", "WebSocket") -o:value("httpupgrade", "HTTPUpgrade") -if singbox_tags:find("with_quic") then - o:value("quic", "QUIC") -end -if singbox_tags:find("with_grpc") then - o:value("grpc", "gRPC") -else o:value("grpc", "gRPC-lite") -end -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "trojan" }) - -if singbox_tags:find("with_wireguard") then - o = s:option(Value, "wireguard_public_key", translate("Public Key")) - o:depends({ protocol = "wireguard" }) - - o = s:option(Value, "wireguard_secret_key", translate("Private Key")) - o:depends({ protocol = "wireguard" }) - - o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key")) - o:depends({ protocol = "wireguard" }) - - o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) - o:depends({ protocol = "wireguard" }) - - o = s:option(Value, "wireguard_mtu", translate("MTU")) - o.default = "1420" - o:depends({ protocol = "wireguard" }) - - o = s:option(Flag, "wireguard_system_interface", translate("System interface")) - o.default = 0 - o:depends({ protocol = "wireguard" }) - - o = s:option(Value, "wireguard_interface_name", translate("System interface name")) - o:depends({ protocol = "wireguard" }) - - o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) - o:depends({ protocol = "wireguard" }) -end - --- [[ TCP ]]-- -o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) -o:value("none", "none") -o:value("http", "http") -o:depends({ transport = "tcp" }) - -o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) -o:depends({ tcp_guise = "http" }) - -o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) -o.placeholder = "/" -o:depends({ tcp_guise = "http" }) - --- [[ HTTP ]]-- -o = s:option(DynamicList, "http_host", translate("HTTP Host")) -o:depends({ transport = "http" }) - -o = s:option(Value, "http_path", translate("HTTP Path")) -o.placeholder = "/" -o:depends({ transport = "http" }) - -o = s:option(Flag, "http_h2_health_check", translate("Health check")) -o:depends({ tls = true, transport = "http" }) - -o = s:option(Value, "http_h2_read_idle_timeout", translate("Idle timeout")) -o.default = "15" -o:depends({ http_h2_health_check = true }) - -o = s:option(Value, "http_h2_health_check_timeout", translate("Health check timeout")) -o.default = "15" -o:depends({ http_h2_health_check = true }) - --- [[ WebSocket ]]-- -o = s:option(Value, "ws_host", translate("WebSocket Host")) -o:depends({ transport = "ws" }) - -o = s:option(Value, "ws_path", translate("WebSocket Path")) -o.placeholder = "/" -o:depends({ transport = "ws" }) - -o = s:option(Flag, "ws_enableEarlyData", translate("Enable early data")) -o:depends({ transport = "ws" }) - -o = s:option(Value, "ws_maxEarlyData", translate("Early data length")) -o.default = "1024" -o:depends({ ws_enableEarlyData = true }) - -o = s:option(Value, "ws_earlyDataHeaderName", translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol")) -o:depends({ ws_enableEarlyData = true }) - --- [[ HTTPUpgrade ]]-- -o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host")) -o:depends({ transport = "httpupgrade" }) - -o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path")) -o.placeholder = "/" -o:depends({ transport = "httpupgrade" }) - --- [[ gRPC ]]-- -o = s:option(Value, "grpc_serviceName", "ServiceName") -o:depends({ transport = "grpc" }) - -o = s:option(Flag, "grpc_health_check", translate("Health check")) -o:depends({ transport = "grpc" }) - -o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout")) -o.default = "15" -o:depends({ grpc_health_check = true }) - -o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout")) -o.default = "15" -o:depends({ grpc_health_check = true }) - -o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream")) -o.default = "0" -o:depends({ grpc_health_check = true }) - --- [[ User-Agent ]]-- -o = s:option(Value, "user_agent", translate("User-Agent")) -o.default = "" -o:value("", translate("default")) -o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") -o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox") -o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari") -o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") -o:value("Go-http-client/1.1", "golang") -o:value("curl/7.68.0", "curl") -o:depends({ tcp_guise = "http" }) -o:depends({ transport = "http" }) -o:depends({ transport = "ws" }) -o:depends({ transport = "httpupgrade" }) -o:depends({ protocol = "naive" }) - --- [[ Mux ]]-- -o = s:option(Flag, "mux", translate("Mux")) -o.rmempty = false -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless", flow = "" }) -o:depends({ protocol = "shadowsocks", uot = "" }) -o:depends({ protocol = "trojan" }) - -o = s:option(ListValue, "mux_type", translate("Mux")) -o:value("smux") -o:value("yamux") -o:value("h2mux") -o:depends({ mux = true }) - -o = s:option(Value, "mux_concurrency", translate("Mux concurrency")) -o.default = 4 -o:depends({ mux = true, tcpbrutal = false }) - -o = s:option(Flag, "mux_padding", translate("Padding")) -o.default = 0 -o:depends({ mux = true }) - --- [[ TCP Brutal ]]-- -o = s:option(Flag, "tcpbrutal", translate("TCP Brutal")) -o.default = 0 -o:depends({ mux = true }) - -o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps")) -o.default = "10" -o:depends({ tcpbrutal = true }) - -o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps")) -o.default = "50" -o:depends({ tcpbrutal = true }) - -o = s:option(Flag, "shadowtls", "ShadowTLS") -o.default = 0 -o:depends({ protocol = "vmess", tls = false }) -o:depends({ protocol = "shadowsocks", tls = false }) - -o = s:option(ListValue, "shadowtls_version", "ShadowTLS " .. translate("Version")) -o.default = "1" -o:value("1", "ShadowTLS v1") -o:value("2", "ShadowTLS v2") -o:value("3", "ShadowTLS v3") -o:depends({ shadowtls = true }) - -o = s:option(Value, "shadowtls_password", "ShadowTLS " .. translate("Password")) -o.password = true -o:depends({ shadowtls = true, shadowtls_version = "2" }) -o:depends({ shadowtls = true, shadowtls_version = "3" }) - -o = s:option(Value, "shadowtls_serverName", "ShadowTLS " .. translate("Domain")) -o:depends({ shadowtls = true }) - -if singbox_tags:find("with_utls") then - o = s:option(Flag, "shadowtls_utls", "ShadowTLS " .. translate("uTLS")) - o.default = "0" - o:depends({ shadowtls = true }) - - o = s:option(ListValue, "shadowtls_fingerprint", "ShadowTLS " .. translate("Finger Print")) - o:value("chrome") - o:value("firefox") - o:value("edge") - o:value("safari") - -- o:value("360") - o:value("qq") - o:value("ios") - -- o:value("android") - o:value("random") - -- o:value("randomized") - o.default = "chrome" - o:depends({ shadowtls = true, shadowtls_utls = true }) -end - --- [[ SIP003 plugin ]]-- -o = s:option(Flag, "plugin_enabled", translate("plugin")) -o.default = 0 -o:depends({ protocol = "shadowsocks" }) - -o = s:option(ListValue, "plugin", "SIP003 " .. translate("plugin")) -o.default = "obfs-local" -o:depends({ plugin_enabled = true }) -o:value("obfs-local") -o:value("v2ray-plugin") - -o = s:option(Value, "plugin_opts", translate("opts")) -o:depends({ plugin_enabled = true }) - -o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution.")) -o:value("", translate("Auto")) -o:value("tcp", "TCP") -o:value("udp", "UDP") -o:value("https", "HTTPS") - -o = s:option(Value, "domain_resolver_dns", "DNS") -o.datatype = "or(ipaddr,ipaddrport)" -o:value("114.114.114.114") -o:value("223.5.5.5:53") -o.default = o.keylist[1] -o:depends({ domain_resolver = "tcp" }) -o:depends({ domain_resolver = "udp" }) - -o = s:option(Value, "domain_resolver_dns_https", "DNS") -o:value("https://120.53.53.53/dns-query", "DNSPod") -o:value("https://223.5.5.5/dns-query", "AliDNS") -o.default = o.keylist[1] -o:depends({ domain_resolver = "https" }) - -o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) -o.default = "" -o:value("", translate("Auto")) -o:value("prefer_ipv4", translate("Prefer IPv4")) -o:value("prefer_ipv6", translate("Prefer IPv6")) -o:value("ipv4_only", translate("IPv4 Only")) -o:value("ipv6_only", translate("IPv6 Only")) - -local protocols = s.fields["protocol"].keylist -if #protocols > 0 then - for i, v in ipairs(protocols) do - if not v:find("^_") then - local depends_condition = { protocol = v } - if v == "hysteria2" then - depends_condition["hysteria2_realms"] = false - end - s.fields["address"]:depends(depends_condition) - s.fields["port"]:depends(depends_condition) - s.fields["domain_resolver"]:depends(depends_condition) - s.fields["domain_strategy"]:depends(depends_condition) - end - end -end -end --- [[ Normal single node End ]] - -if not load_shunt_options then - o = s:option(ListValue, "chain_proxy", translate("Chain Proxy")) - o:value("", translate("Close(Not use)")) - if not (load_iface_options or load_urltest_options) then - -- Special node cannot be use pre-proxy. - o:value("1", translate("Preproxy Node")) - o:value("3", translate("Outbound Interface")) - end - o:value("2", translate("Landing Node")) - - o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"), translate("Only support a layer of proxy.")) - o1:depends({ chain_proxy = "1", hysteria2_realms = false }) - o1.template = m:template_path("/cbi/nodes_listvalue") - o1.group = {} - - o3 = s:option(Value, "outbound_iface", translate("Outbound Interface")) - o3:depends({ chain_proxy = "3" }) - o3:value("", translate("All")) - for _, d in ipairs(netdev_list) do - o3:value(d.name, d.label) - end - - o2 = s:option(ListValue, "to_node", translate("Landing Node"), translate("Only support a layer of proxy.")) - o2:depends({ chain_proxy = "2", hysteria2_realms = false }) - o2.template = m:template_path("/cbi/nodes_listvalue") - o2.group = {} - - for k1, v1 in pairs(node_list) do - if k1 ~= "shunt_list" and k1 ~= "iface_list" then - for i, v in ipairs(v1) do - if v.id ~= arg[1] then - o1:value(v.id, v.remark) - o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default") - if k1 == "normal_list" then - -- Landing Node not support use special node. - o2:value(v.id, v.remark) - o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default") - end - end - end - end - end -end - -api.luci_types(s1, s) - -if load_shunt_options then - local current_node = m:get(arg[1]) or {} - local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua") - setfenv(shunt_lua, getfenv(1))(m, s1, { - node_id = arg[1], - node = current_node, - node_list = node_list, - }) -end +local singbox_bin = api.finded_com("sing-box") + +if not singbox_bin then + return +end + +-- [[ sing-box ]] +local m, s1 = ... +local type_name = "sing-box" + +s1.fields["type"]:value(type_name, "Sing-Box") +if not s1.fields["type"].default then + s1.fields["type"].default = type_name +end + +if s1.val["type"] ~= type_name then + return +end + +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "singbox_" + +local formvalue_proto = luci.http.formvalue(formvalue_key .. "protocol") + +if formvalue_proto then s1.val["protocol"] = formvalue_proto end + +local arg_select_proto = luci.http.formvalue("select_proto") or "" + +local ss_method_new_list = { + "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" +} + +local ss_method_old_list = { + "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "rc4-md5", "chacha20-ietf", "xchacha20", +} + +local security_list = { "none", "auto", "aes-128-gcm", "chacha20-poly1305", "zero" } + +local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'") + +local singbox_version = api.get_app_version("sing-box"):match("[^v]+") + +o = s:option(ListValue, "protocol", translate("Protocol")) +o:value("socks", "Socks") +o:value("http", "HTTP") +o:value("shadowsocks", "Shadowsocks") +if singbox_tags:find("with_shadowsocksr") then + o:value("shadowsocksr", "ShadowsocksR") +end +o:value("vmess", "Vmess") +o:value("trojan", "Trojan") +if singbox_tags:find("with_wireguard") then + o:value("wireguard", "WireGuard") +end +if singbox_tags:find("with_quic") then + o:value("hysteria", "Hysteria") +end +o:value("vless", "VLESS") +if singbox_tags:find("with_quic") then + o:value("tuic", "TUIC") +end +if singbox_tags:find("with_quic") then + o:value("hysteria2", "Hysteria2") +end +o:value("anytls", "AnyTLS") +o:value("ssh", "SSH") +if singbox_tags:find("with_naive_outbound") then + o:value("naive", "NaïveProxy") +end +o:value("_urltest", translate("URLTest")) +o:value("_shunt", translate("Shunt")) +o:value("_iface", translate("Custom Interface")) +function o.custom_cfgvalue(self, section) + if arg_select_proto ~= "" then + return arg_select_proto + else + return m:get(section, self.config_option) + end +end + +local load_urltest_options = s1.val["protocol"] == "_urltest" or arg_select_proto == "_urltest" +local load_shunt_options = s1.val["protocol"] == "_shunt" or arg_select_proto == "_shunt" +local load_iface_options = s1.val["protocol"] == "_iface" or arg_select_proto == "_iface" +local load_normal_options = true +if load_urltest_options or load_shunt_options or load_iface_options then + load_normal_options = nil +end +if not arg_select_proto:find("_") then + load_normal_options = true +end + +local netdev_list = api.get_network_devices() +local node_list = api.get_node_list() + +if load_urltest_options then -- [[ URLTest Start ]] + o = s:option(ListValue, "node_add_mode", translate("Node Addition Method")) + o:depends({ protocol = "_urltest" }) + o.default = "manual" + o:value("manual", translate("Manual")) + o:value("batch", translate("Batch")) + + o = s:option(MultiValue, "urltest_node", translate("URLTest node list"), translate("List of nodes to test, document")) + o:depends({ node_add_mode = "manual" }) + o.widget = "checkbox" + o.template = m:template_path("/cbi/nodes_multivalue") + o.group = {} + for k1, v1 in pairs(node_list) do + if k1 == "socks_list" or k1 == "normal_list" then + for i, v in ipairs(v1) do + o:value(v.id, v.remark) + o.group[#o.group+1] = v.group or "" + end + end + end + -- Reading the old DynamicList + function o.custom_cfgvalue(self, section) + return table.concat(m:get(section, "urltest_node") or {}, " ") + end + -- Write-and-hold DynamicList + function o.custom_write(self, section, value) + local old = m:get(section, "urltest_node") or {} + local new, set = {}, {} + for v in value:gmatch("%S+") do + new[#new + 1] = v + set[v] = 1 + end + for _, v in ipairs(old) do + if not set[v] then + m:set(section, "urltest_node", new) + return + end + set[v] = nil + end + for _ in pairs(set) do + m:set(section, "urltest_node", new) + return + end + end + + o = s:option(MultiValue, "node_group", translate("Select Group")) + o:depends({ node_add_mode = "batch" }) + o.widget = "checkbox" + o:value("default", translate("default")) + for k, v in pairs(groups) do + o:value(api.UrlEncode(k), k) + end + + o = s:option(Value, "node_match_rule", translate("Node Matching Rules")) + o:depends({ node_add_mode = "batch" }) + local descrStr = "Example: ^A && B && !C && D$
" + descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
" + descrStr = descrStr .. "Conditions are joined by && (AND), and their order does not affect the result.
" + descrStr = descrStr .. "Multiple groups can be separated by || (OR), matching succeeds if any group matches.
" + descrStr = descrStr .. "Example: A && B || C && D means (A AND B) OR (C AND D)." + o.description = translate(descrStr) + + o = s:option(Value, "urltest_url", translate("Probe URL")) + o:depends({ protocol = "_urltest" }) + o:value("https://cp.cloudflare.com/", "Cloudflare") + o:value("https://www.gstatic.com/generate_204", "Gstatic") + o:value("https://www.google.com/generate_204", "Google") + o:value("https://www.youtube.com/generate_204", "YouTube") + o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)") + o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)") + o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)") + o.default = o.keylist[3] + o.description = translate("The URL used to detect the connection status.") + + o = s:option(Value, "urltest_interval", translate("Test interval")) + o:depends({ protocol = "_urltest" }) + o.default = "3m" + o.placeholder = "3m" + o.description = translate("The interval between initiating probes.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") .. "
" .. + translate("Test interval must be less or equal than idle timeout.") + + o = s:option(Value, "urltest_tolerance", translate("Test tolerance"), translate("The test tolerance in milliseconds.")) + o:depends({ protocol = "_urltest" }) + o.datatype = "uinteger" + o.placeholder = "50" + o.default = "50" + + o = s:option(Value, "urltest_idle_timeout", translate("Idle timeout")) + o:depends({ protocol = "_urltest" }) + o.placeholder = "30m" + o.default = "30m" + o.description = translate("The idle timeout.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") + + o = s:option(Flag, "urltest_interrupt_exist_connections", translate("Interrupt existing connections")) + o:depends({ protocol = "_urltest" }) + o.default = "0" + o.description = translate("Interrupt existing connections when the selected outbound has changed.") +end -- [[ URLTest End ]] + +if load_iface_options then -- [[ Custom Interface Start ]] + o = s:option(Value, "iface", translate("Interface")) + o:depends({ protocol = "_iface" }) + for _, d in ipairs(netdev_list) do + o:value(d.name, d.label) + end +end -- [[ Custom Interface End ]] + + +-- [[ Normal single node Start ]] +if load_normal_options then + +o = s:option(Value, "address", translate("Address (Support Domain Name)")) + +o = s:option(Value, "port", translate("Port")) +o.datatype = "port" + +o = s:option(Value, "uuid", translate("ID")) +o.password = true +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "tuic" }) + +o = s:option(Value, "username", translate("Username")) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "ssh" }) +o:depends({ protocol = "naive" }) + +o = s:option(Value, "password", translate("Password")) +o.password = true +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "shadowsocksr" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "anytls" }) +o:depends({ protocol = "ssh" }) +o:depends({ protocol = "naive" }) + +o = s:option(ListValue, "security", translate("Encrypt Method")) +for a, t in ipairs(security_list) do o:value(t) end +o:depends({ protocol = "vmess" }) + +o = s:option(ListValue, "ss_method", translate("Encrypt Method")) +for a, t in ipairs(ss_method_new_list) do o:value(t) end +for a, t in ipairs(ss_method_old_list) do o:value(t) end +o:depends({ protocol = "shadowsocks" }) + +if singbox_tags:find("with_shadowsocksr") then + o = s:option(ListValue, "ssr_method", translate("Encrypt Method")) + for a, t in ipairs(ss_method_old_list) do o:value(t) end + o:depends({ protocol = "shadowsocksr" }) + + local ssr_protocol_list = { + "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", + "auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5", + "auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c", + "auth_chain_d", "auth_chain_e", "auth_chain_f" + } + + o = s:option(ListValue, "ssr_protocol", translate("Protocol")) + for a, t in ipairs(ssr_protocol_list) do o:value(t) end + o:depends({ protocol = "shadowsocksr" }) + + o = s:option(Value, "ssr_protocol_param", translate("Protocol_param")) + o:depends({ protocol = "shadowsocksr" }) + + local ssr_obfs_list = { + "plain", "http_simple", "http_post", "random_head", "tls_simple", + "tls1.0_session_auth", "tls1.2_ticket_auth" + } + + o = s:option(ListValue, "ssr_obfs", translate("Obfs")) + for a, t in ipairs(ssr_obfs_list) do o:value(t) end + o:depends({ protocol = "shadowsocksr" }) + + o = s:option(Value, "ssr_obfs_param", translate("Obfs_param")) + o:depends({ protocol = "shadowsocksr" }) +end + +o = s:option(Flag, "uot", translate("UDP over TCP")) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "naive" }) + +o = s:option(Value, "alter_id", "Alter ID") +o.datatype = "uinteger" +o.default = "0" +o:depends({ protocol = "vmess" }) + +o = s:option(Flag, "global_padding", "global_padding", translate("Protocol parameter. Will waste traffic randomly if enabled.")) +o.default = "0" +o:depends({ protocol = "vmess" }) + +o = s:option(Flag, "authenticated_length", "authenticated_length", translate("Protocol parameter. Enable length block encryption.")) +o.default = "0" +o:depends({ protocol = "vmess" }) + +o = s:option(ListValue, "flow", translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:depends({ protocol = "vless", tls = true }) + +if singbox_tags:find("with_quic") then + o = s:option(Value, "hysteria_hop", translate("Port hopping range")) + o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_hop_interval", translate("Hop Interval(second)"), translate("Example:") .. "30 (≥5)") + o.datatype = "uinteger" + o.placeholder = "30" + o.default = "30" + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_obfs", translate("Obfs Password")) + o:depends({ protocol = "hysteria" }) + + o = s:option(ListValue, "hysteria_auth_type", translate("Auth Type")) + o:value("disable", translate("Disable")) + o:value("string", translate("STRING")) + o:value("base64", translate("BASE64")) + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_auth_password", translate("Auth Password")) + o.password = true + o:depends({ protocol = "hysteria", hysteria_auth_type = "string"}) + o:depends({ protocol = "hysteria", hysteria_auth_type = "base64"}) + + o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps")) + o.default = "10" + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps")) + o.default = "50" + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window")) + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_recv_window", translate("QUIC connection receive window")) + o:depends({ protocol = "hysteria" }) + + o = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection")) + o:depends({ protocol = "hysteria" }) +end + +if singbox_tags:find("with_quic") then + o = s:option(ListValue, "tuic_congestion_control", translate("Congestion control algorithm")) + o.default = "cubic" + o:value("bbr", translate("BBR")) + o:value("cubic", translate("CUBIC")) + o:value("new_reno", translate("New Reno")) + o:depends({ protocol = "tuic" }) + + o = s:option(ListValue, "tuic_udp_relay_mode", translate("UDP relay mode")) + o.default = "native" + o:value("native", translate("native")) + o:value("quic", translate("QUIC")) + o:depends({ protocol = "tuic" }) + + --[[ + o = s:option(Flag, "tuic_udp_over_stream", translate("UDP over stream")) + o:depends({ protocol = "tuic" }) + ]]-- + + o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake")) + o.default = 0 + o:depends({ protocol = "tuic" }) + + o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)")) + o.datatype = "uinteger" + o.default = "3" + o:depends({ protocol = "tuic" }) + + o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN")) + o.default = "default" + o:value("default", translate("Default")) + o:value("h3") + o:value("h2") + o:value("h3,h2") + o:value("http/1.1") + o:value("h2,http/1.1") + o:value("h3,h2,http/1.1") + o:value("spdy/3.1") + o:value("h3,spdy/3.1") + o:depends({ protocol = "tuic" }) +end + +if singbox_tags:find("with_quic") then + o = s:option(Value, "hysteria2_hop", translate("Port hopping range")) + o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") + o:depends({ protocol = "hysteria2", hysteria2_realms = false }) + + o = s:option(Value, "hysteria2_hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) + o.datatype = "or(uinteger,portrange)" + o.placeholder = "30" + o.default = "30" + o:depends({ protocol = "hysteria2", hysteria2_realms = false }) + + o = s:option(Flag, "hysteria2_realms", translate("Realms")) + o.default = "0" + if api.compare_versions(singbox_version, ">=", "1.14.0") then + o:depends({ protocol = "hysteria2"}) + else + o:depends({ protocol = "__hide"}) + end + + o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") + o:depends({ hysteria2_realms = "1" }) + o.validate = function(self, value) + value = api.trim(value) + local realm = api.parse_realm_uri(value) + if realm then return value end + return nil, translate("Invalid Realm URL.") + end + + o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) + o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } + o:depends({ hysteria2_realms = "1" }) + + o = s:option(Value, "hysteria2_auth_password", translate("Auth Password")) + o.password = true + o:depends({ protocol = "hysteria2"}) + + o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) + o:value("", translate("Disable")) + o:value("salamander") + o:value("gecko") + o:depends({ protocol = "hysteria2" }) + + o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) + o:depends({ hysteria2_obfs_type = "salamander" }) + o:depends({ hysteria2_obfs_type = "gecko" }) + + o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) + o.datatype = "uinteger" + o.placeholder = "512" + o.default = "512" + o:depends({ hysteria2_obfs_type = "gecko" }) + + o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) + o.datatype = "uinteger" + o.placeholder = "1200" + o.default = "1200" + o:depends({ hysteria2_obfs_type = "gecko" }) + + o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) + o:depends({ protocol = "hysteria2" }) + + o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) + o:depends({ protocol = "hysteria2" }) + + o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") + o.datatype = "range(4,120)" + o:depends({ protocol = "hysteria2"}) + + o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") + o.datatype = "range(2,60)" + o:depends({ protocol = "hysteria2"}) + + o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection")) + o.default = "0" + o:depends({ protocol = "hysteria2"}) +end + +-- [[ SSH config start ]] -- +o = s:option(TextValue, "ssh_priv_key", translate("Private Key")) +o.rows = 5 +o.wrap = "off" +o:depends({ protocol = "ssh" }) +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") + return value +end + +o = s:option(Value, "ssh_priv_key_pp", translate("Private Key Passphrase")) +o.password = true +o:depends({ protocol = "ssh" }) + +o = s:option(DynamicList, "ssh_host_key", translate("Host Key"), translate("Accept any if empty.")) +o:depends({ protocol = "ssh" }) + +o = s:option(DynamicList, "ssh_host_key_algo", translate("Host Key Algorithms")) +o:depends({ protocol = "ssh" }) + +o = s:option(Value, "ssh_client_version", translate("Client Version"), translate("Random version will be used if empty.")) +o:depends({ protocol = "ssh" }) +-- [[ SSH config end ]] -- + +-- [[ naive start ]] -- +o = s:option(Value, "naive_insecure_concurrency", translate("Concurrent Tunnels")) +o.datatype = "uinteger" +o.placeholder = "0" +o.default = "0" +o:depends({ protocol = "naive" }) + +o = s:option(Flag, "naive_quic", translate("QUIC")) +o.default = 0 +o:depends({ protocol = "naive" }) + +o = s:option(ListValue, "naive_congestion_control", translate("Congestion control algorithm")) +o.default = "bbr" +o:value("bbr", translate("BBR")) +o:value("bbr2", translate("BBRv2")) +o:value("cubic", translate("CUBIC")) +o:value("reno", translate("New Reno")) +o:depends({ naive_quic = "1" }) +-- [[ naive end ]] -- + +o = s:option(Flag, "tls", translate("TLS")) +o.default = 0 +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "anytls" }) + +o = s:option(ListValue, "alpn", translate("ALPN")) +o.default = "default" +o:value("default", translate("Default")) +o:value("h3") +o:value("h2") +o:value("h3,h2") +o:value("http/1.1") +o:value("h2,http/1.1") +o:value("h3,h2,http/1.1") +o:depends({ tls = true }) +o:depends({ protocol = "hysteria" }) + +o = s:option(Flag, "tls_disable_sni", translate("Disable SNI"), translate("Do not send server name in ClientHello.")) +o.default = "0" +o:depends({ tls = true }) +o:depends({ protocol = "hysteria"}) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) + +o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain")) +o:depends({ tls = true }) +o:depends({ protocol = "hysteria"}) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "naive" }) + +o = s:option(Flag, "tls_allowInsecure", translate("allowInsecure"), translate("Whether unsafe connections are allowed. When checked, Certificate validation will be skipped.")) +o.default = "0" +o:depends({ tls = true }) +o:depends({ protocol = "hysteria"}) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) + +o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)")) +o.default = "0" +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria"}) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "naive" }) + +o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format.")) +o.default = "" +o.rows = 5 +o.wrap = "off" +o:depends({ tls_certificate = true }) +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") + return value +end + +o = s:option(DynamicList, "cipherSuites", translate("Cipher Suites"), '***' .. " " .. translate("Configures the list of supported cipher suites.")) +o:value("TLS_AES_128_GCM_SHA256") +o:value("TLS_AES_256_GCM_SHA384") +o:value("TLS_CHACHA20_POLY1305_SHA256") +o:value("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA") +o:value("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA") +o:value("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA") +o:value("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA") +o:value("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") +o:value("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384") +o:value("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") +o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") +o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") +o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") +o:depends({ tls = true }) + +o = s:option(Flag, "ech", translate("ECH")) +o.default = "0" +o:depends({ tls = true, flow = "", reality = false }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) +o:depends({ protocol = "naive" }) + +o = s:option(TextValue, "ech_config", translate("ECH Config")) +o.default = "" +o.rows = 5 +o.wrap = "off" +o:depends({ ech = true }) +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") + return value +end + +o = s:option(Value, "ech_query_server_name", translate("ECH Query Domain"), translate("Overrides the domain name used for ECH HTTPS record queries.")) +o:depends({ ech = true }) + +if singbox_tags:find("with_utls") then + o = s:option(Flag, "utls", translate("uTLS")) + o.default = "0" + o:depends({ tls = true }) + + o = s:option(ListValue, "fingerprint", translate("Finger Print")) + o:value("chrome") + o:value("firefox") + o:value("edge") + o:value("safari") + o:value("360") + o:value("qq") + o:value("ios") + o:value("android") + o:value("random") + o:value("randomized") + o.default = "chrome" + o:depends({ utls = true }) + + -- [[ REALITY ]] -- + o = s:option(Flag, "reality", translate("REALITY")) + o.default = 0 + o:depends({ protocol = "vless", tls = true }) + o:depends({ protocol = "vmess", tls = true }) + o:depends({ protocol = "shadowsocks", tls = true }) + o:depends({ protocol = "socks", tls = true }) + o:depends({ protocol = "trojan", tls = true }) + o:depends({ protocol = "anytls", tls = true }) + + o = s:option(Value, "reality_publicKey", translate("Public Key")) + o:depends({ reality = true }) + + o = s:option(Value, "reality_shortId", translate("Short Id")) + o:depends({ reality = true }) +end + +o = s:option(Flag, "anytls_disable_reuse", translate("Disable TLS Reuse")) +o.default = 0 +o:depends({ protocol = "anytls" }) + +o = s:option(ListValue, "transport", translate("Transport")) +o:value("tcp", "TCP") +o:value("http", "HTTP") +o:value("ws", "WebSocket") +o:value("httpupgrade", "HTTPUpgrade") +if singbox_tags:find("with_quic") then + o:value("quic", "QUIC") +end +if singbox_tags:find("with_grpc") then + o:value("grpc", "gRPC") +else o:value("grpc", "gRPC-lite") +end +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) + +if singbox_tags:find("with_wireguard") then + o = s:option(Value, "wireguard_public_key", translate("Public Key")) + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_secret_key", translate("Private Key")) + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key")) + o:depends({ protocol = "wireguard" }) + + o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_mtu", translate("MTU")) + o.default = "1420" + o:depends({ protocol = "wireguard" }) + + o = s:option(Flag, "wireguard_system_interface", translate("System interface")) + o.default = 0 + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_interface_name", translate("System interface name")) + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) + o:depends({ protocol = "wireguard" }) +end + +-- [[ TCP ]]-- +o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) +o:value("none", "none") +o:value("http", "http") +o:depends({ transport = "tcp" }) + +o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) +o:depends({ tcp_guise = "http" }) + +o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) +o.placeholder = "/" +o:depends({ tcp_guise = "http" }) + +-- [[ HTTP ]]-- +o = s:option(DynamicList, "http_host", translate("HTTP Host")) +o:depends({ transport = "http" }) + +o = s:option(Value, "http_path", translate("HTTP Path")) +o.placeholder = "/" +o:depends({ transport = "http" }) + +o = s:option(Flag, "http_h2_health_check", translate("Health check")) +o:depends({ tls = true, transport = "http" }) + +o = s:option(Value, "http_h2_read_idle_timeout", translate("Idle timeout")) +o.default = "15" +o:depends({ http_h2_health_check = true }) + +o = s:option(Value, "http_h2_health_check_timeout", translate("Health check timeout")) +o.default = "15" +o:depends({ http_h2_health_check = true }) + +-- [[ WebSocket ]]-- +o = s:option(Value, "ws_host", translate("WebSocket Host")) +o:depends({ transport = "ws" }) + +o = s:option(Value, "ws_path", translate("WebSocket Path")) +o.placeholder = "/" +o:depends({ transport = "ws" }) + +o = s:option(Flag, "ws_enableEarlyData", translate("Enable early data")) +o:depends({ transport = "ws" }) + +o = s:option(Value, "ws_maxEarlyData", translate("Early data length")) +o.default = "1024" +o:depends({ ws_enableEarlyData = true }) + +o = s:option(Value, "ws_earlyDataHeaderName", translate("Early data header name"), translate("Recommended value: Sec-WebSocket-Protocol")) +o:depends({ ws_enableEarlyData = true }) + +-- [[ HTTPUpgrade ]]-- +o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host")) +o:depends({ transport = "httpupgrade" }) + +o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path")) +o.placeholder = "/" +o:depends({ transport = "httpupgrade" }) + +-- [[ gRPC ]]-- +o = s:option(Value, "grpc_serviceName", "ServiceName") +o:depends({ transport = "grpc" }) + +o = s:option(Flag, "grpc_health_check", translate("Health check")) +o:depends({ transport = "grpc" }) + +o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout")) +o.default = "15" +o:depends({ grpc_health_check = true }) + +o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout")) +o.default = "15" +o:depends({ grpc_health_check = true }) + +o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream")) +o.default = "0" +o:depends({ grpc_health_check = true }) + +-- [[ User-Agent ]]-- +o = s:option(Value, "user_agent", translate("User-Agent")) +o.default = "" +o:value("", translate("default")) +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox") +o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") +o:value("Go-http-client/1.1", "golang") +o:value("curl/7.68.0", "curl") +o:depends({ tcp_guise = "http" }) +o:depends({ transport = "http" }) +o:depends({ transport = "ws" }) +o:depends({ transport = "httpupgrade" }) +o:depends({ protocol = "naive" }) + +-- [[ Mux ]]-- +o = s:option(Flag, "mux", translate("Mux")) +o.rmempty = false +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless", flow = "" }) +o:depends({ protocol = "shadowsocks", uot = "" }) +o:depends({ protocol = "trojan" }) + +o = s:option(ListValue, "mux_type", translate("Mux")) +o:value("smux") +o:value("yamux") +o:value("h2mux") +o:depends({ mux = true }) + +o = s:option(Value, "mux_concurrency", translate("Mux concurrency")) +o.default = 4 +o:depends({ mux = true, tcpbrutal = false }) + +o = s:option(Flag, "mux_padding", translate("Padding")) +o.default = 0 +o:depends({ mux = true }) + +-- [[ TCP Brutal ]]-- +o = s:option(Flag, "tcpbrutal", translate("TCP Brutal")) +o.default = 0 +o:depends({ mux = true }) + +o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps")) +o.default = "10" +o:depends({ tcpbrutal = true }) + +o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps")) +o.default = "50" +o:depends({ tcpbrutal = true }) + +o = s:option(Flag, "shadowtls", "ShadowTLS") +o.default = 0 +o:depends({ protocol = "vmess", tls = false }) +o:depends({ protocol = "shadowsocks", tls = false }) + +o = s:option(ListValue, "shadowtls_version", "ShadowTLS " .. translate("Version")) +o.default = "1" +o:value("1", "ShadowTLS v1") +o:value("2", "ShadowTLS v2") +o:value("3", "ShadowTLS v3") +o:depends({ shadowtls = true }) + +o = s:option(Value, "shadowtls_password", "ShadowTLS " .. translate("Password")) +o.password = true +o:depends({ shadowtls = true, shadowtls_version = "2" }) +o:depends({ shadowtls = true, shadowtls_version = "3" }) + +o = s:option(Value, "shadowtls_serverName", "ShadowTLS " .. translate("Domain")) +o:depends({ shadowtls = true }) + +if singbox_tags:find("with_utls") then + o = s:option(Flag, "shadowtls_utls", "ShadowTLS " .. translate("uTLS")) + o.default = "0" + o:depends({ shadowtls = true }) + + o = s:option(ListValue, "shadowtls_fingerprint", "ShadowTLS " .. translate("Finger Print")) + o:value("chrome") + o:value("firefox") + o:value("edge") + o:value("safari") + -- o:value("360") + o:value("qq") + o:value("ios") + -- o:value("android") + o:value("random") + -- o:value("randomized") + o.default = "chrome" + o:depends({ shadowtls = true, shadowtls_utls = true }) +end + +-- [[ SIP003 plugin ]]-- +o = s:option(Flag, "plugin_enabled", translate("plugin")) +o.default = 0 +o:depends({ protocol = "shadowsocks" }) + +o = s:option(ListValue, "plugin", "SIP003 " .. translate("plugin")) +o.default = "obfs-local" +o:depends({ plugin_enabled = true }) +o:value("obfs-local") +o:value("v2ray-plugin") + +o = s:option(Value, "plugin_opts", translate("opts")) +o:depends({ plugin_enabled = true }) + +o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution.")) +o:value("", translate("Auto")) +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("https", "HTTPS") + +o = s:option(Value, "domain_resolver_dns", "DNS") +o.datatype = "or(ipaddr,ipaddrport)" +o:value("114.114.114.114") +o:value("223.5.5.5:53") +o.default = o.keylist[1] +o:depends({ domain_resolver = "tcp" }) +o:depends({ domain_resolver = "udp" }) + +o = s:option(Value, "domain_resolver_dns_https", "DNS") +o:value("https://120.53.53.53/dns-query", "DNSPod") +o:value("https://223.5.5.5/dns-query", "AliDNS") +o.default = o.keylist[1] +o:depends({ domain_resolver = "https" }) + +o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) +o.default = "" +o:value("", translate("Auto")) +o:value("prefer_ipv4", translate("Prefer IPv4")) +o:value("prefer_ipv6", translate("Prefer IPv6")) +o:value("ipv4_only", translate("IPv4 Only")) +o:value("ipv6_only", translate("IPv6 Only")) + +local protocols = s.fields["protocol"].keylist +if #protocols > 0 then + for i, v in ipairs(protocols) do + if not v:find("^_") then + local depends_condition = { protocol = v } + if v == "hysteria2" then + depends_condition["hysteria2_realms"] = false + end + s.fields["address"]:depends(depends_condition) + s.fields["port"]:depends(depends_condition) + s.fields["domain_resolver"]:depends(depends_condition) + s.fields["domain_strategy"]:depends(depends_condition) + end + end +end +end +-- [[ Normal single node End ]] + +if not load_shunt_options then + o = s:option(ListValue, "chain_proxy", translate("Chain Proxy")) + o:value("", translate("Close(Not use)")) + if not (load_iface_options or load_urltest_options) then + -- Special node cannot be use pre-proxy. + o:value("1", translate("Preproxy Node")) + o:value("3", translate("Outbound Interface")) + end + o:value("2", translate("Landing Node")) + + o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"), translate("Only support a layer of proxy.")) + o1:depends({ chain_proxy = "1", hysteria2_realms = false }) + o1.template = m:template_path("/cbi/nodes_listvalue") + o1.group = {} + + o3 = s:option(Value, "outbound_iface", translate("Outbound Interface")) + o3:depends({ chain_proxy = "3" }) + o3:value("", translate("All")) + for _, d in ipairs(netdev_list) do + o3:value(d.name, d.label) + end + + o2 = s:option(ListValue, "to_node", translate("Landing Node"), translate("Only support a layer of proxy.")) + o2:depends({ chain_proxy = "2", hysteria2_realms = false }) + o2.template = m:template_path("/cbi/nodes_listvalue") + o2.group = {} + + for k1, v1 in pairs(node_list) do + if k1 ~= "shunt_list" and k1 ~= "iface_list" then + for i, v in ipairs(v1) do + if v.id ~= arg[1] then + o1:value(v.id, v.remark) + o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + if k1 == "normal_list" then + -- Landing Node not support use special node. + o2:value(v.id, v.remark) + o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + end + end + end + end + end +end + +api.luci_types(s1, s) + +if load_shunt_options then + local current_node = m:get(arg[1]) or {} + local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua") + setfenv(shunt_lua, getfenv(1))(m, s1, { + node_id = arg[1], + node = current_node, + node_list = node_list, + }) +end diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua index d7827092..e0d75ec3 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/2_xray.lua @@ -1,901 +1,901 @@ -if not api.finded_com("xray") then - return -end - --- [[ Xray ]] -local m, s1 = ... -local type_name = "Xray" - -s1.fields["type"]:value(type_name, "Xray") -if not s1.fields["type"].default then - s1.fields["type"].default = type_name -end - -if s1.val["type"] ~= type_name then - return -end - -local s = NamedSection(m, arg[1], "server") -s.type_name = type_name -s.option_prefix = "xray_" - -local formvalue_proto = luci.http.formvalue(formvalue_key .. "protocol") - -if formvalue_proto then s1.val["protocol"] = formvalue_proto end - -local arg_select_proto = luci.http.formvalue("select_proto") or "" - -local ss_method_list = { - "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305", "xchacha20-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" -} - -local security_list = { "auto", "aes-128-gcm", "chacha20-poly1305" } - -local header_type_list = { - "none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns" -} - -local xray_version = api.get_app_version("xray") - -o = s:option(ListValue, "protocol", translate("Protocol")) -o:value("vmess", translate("Vmess")) -o:value("vless", translate("VLESS")) -o:value("http", translate("HTTP")) -o:value("socks", translate("Socks")) -o:value("shadowsocks", translate("Shadowsocks")) -o:value("trojan", translate("Trojan")) -o:value("wireguard", translate("WireGuard")) -if api.compare_versions(xray_version, ">=", "26.1.13") then - o:value("hysteria2", translate("Hysteria2")) -end -if api.compare_versions(xray_version, ">=", "1.8.12") then - o:value("_balancing", translate("Balancing")) -end -o:value("_shunt", translate("Shunt")) -o:value("_iface", translate("Custom Interface")) -function o.custom_cfgvalue(self, section) - if arg_select_proto ~= "" then - return arg_select_proto - else - return m:get(section, self.config_option) - end -end - -local load_balancing_options = s1.val["protocol"] == "_balancing" or arg_select_proto == "_balancing" -local load_shunt_options = s1.val["protocol"] == "_shunt" or arg_select_proto == "_shunt" -local load_iface_options = s1.val["protocol"] == "_iface" or arg_select_proto == "_iface" -local load_normal_options = true -if load_balancing_options or load_shunt_options or load_iface_options then - load_normal_options = nil -end -if not arg_select_proto:find("_") then - load_normal_options = true -end - -local netdev_list = api.get_network_devices() -local node_list = api.get_node_list() -local fallback_list = {} -local is_balancer = nil -for k, e in ipairs(node_list.balancing_list or {}) do - if e.id ~= arg[1] then - fallback_list[#fallback_list + 1] = { - id = e["id"], - remark = e["remark"], - group = e["group"], - fallback = e.o["fallback_node"], - } - else - is_balancer = true - end -end - -if load_balancing_options then -- [[ Load balancing Start ]] - o = s:option(ListValue, "node_add_mode", translate("Node Addition Method")) - o:depends({ protocol = "_balancing" }) - o.default = "manual" - o:value("manual", translate("Manual")) - o:value("batch", translate("Batch")) - - o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, document")) - o:depends({ node_add_mode = "manual" }) - o.widget = "checkbox" - o.template = m:template_path("/cbi/nodes_multivalue") - o.group = {} - for k1, v1 in pairs(node_list) do - if k1 == "socks_list" or k1 == "normal_list" then - for i, v in ipairs(v1) do - o:value(v.id, v.remark) - o.group[#o.group+1] = v.group or "" - end - end - end - -- Reading the old DynamicList - function o.custom_cfgvalue(self, section) - return table.concat(m:get(section, "balancing_node") or {}, " ") - end - -- Write-and-hold DynamicList - function o.custom_write(self, section, value) - local old = m:get(section, "balancing_node") or {} - local new, set = {}, {} - for v in value:gmatch("%S+") do - new[#new + 1] = v - set[v] = 1 - end - for _, v in ipairs(old) do - if not set[v] then - m:set(section, "balancing_node", new) - return - end - set[v] = nil - end - for _ in pairs(set) do - m:set(section, "balancing_node", new) - return - end - end - - o = s:option(MultiValue, "node_group", translate("Select Group")) - o:depends({ node_add_mode = "batch" }) - o.widget = "checkbox" - o:value("default", translate("default")) - for k, v in pairs(groups) do - o:value(api.UrlEncode(k), k) - end - - o = s:option(Value, "node_match_rule", translate("Node Matching Rules")) - o:depends({ node_add_mode = "batch" }) - local descrStr = "Example: ^A && B && !C && D$
" - descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
" - descrStr = descrStr .. "Conditions are joined by && (AND), and their order does not affect the result.
" - descrStr = descrStr .. "Multiple groups can be separated by || (OR), matching succeeds if any group matches.
" - descrStr = descrStr .. "Example: A && B || C && D means (A AND B) OR (C AND D)." - o.description = translate(descrStr) - - o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy")) - o:depends({ protocol = "_balancing" }) - o:value("random") - o:value("roundRobin") - o:value("leastPing") - o:value("leastLoad") - o.default = "random" - - -- Fallback Node - o = s:option(ListValue, "fallback_node", translate("Fallback Node")) - o.group = {"",""} - o:value("", translate("Close(Not use)")) - o:value("_direct", translate("Direct Connection")) - o:depends({ protocol = "_balancing" }) - o.template = m:template_path("/cbi/nodes_listvalue") - -- Maximum number of fallback nesting layers - local MAX_FALLBACK_DEPTH = 3 - -- Check if a loop will form. - local function will_loop(start_id, target_id, depth) - depth = depth or 0 - -- Recursion stops after the maximum depth is exceeded. - if depth >= MAX_FALLBACK_DEPTH then - return false - end - for _, v in ipairs(fallback_list) do - if v.id == target_id then - local fb = v.fallback - -- No fallback - if not fb or fb == "" or fb == "_direct" then - return false - end - -- Loopback detected - if fb == start_id then - return true - end - -- Continue recursive checking - return will_loop(start_id, fb, depth + 1) - end - end - return false - end - -- Get fallback chain depth - local function get_fallback_depth(id, depth) - depth = depth or 0 - if depth >= MAX_FALLBACK_DEPTH then - return depth - end - for _, v in ipairs(fallback_list) do - if v.id == id then - local fb = v.fallback - if not fb or fb == "" or fb == "_direct" then - return depth - end - return get_fallback_depth(fb, depth + 1) - end - end - return depth - end - for _, v in ipairs(fallback_list) do - local depth = get_fallback_depth(v.id) - -- Once the maximum number of nested doll layers is exceeded, further selection of the balancer is not allowed. - if depth < MAX_FALLBACK_DEPTH - and not will_loop(arg[1], v.id) - then - o:value(v.id, v.remark) - o.group[#o.group + 1] = (v.group and v.group ~= "") and v.group or translate("default") - end - end - for k1, v1 in pairs(node_list) do - if k1 == "socks_list" or k1 == "normal_list" or k1 == "urltest_list" then - for i, v in ipairs(v1) do - o:value(v.id, v.remark) - o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") - end - end - end - - o = s:option(Flag, "useCustomProbeUrl", translate("Use Custom Probe URL"), translate("By default the built-in probe URL will be used, enable this option to use a custom probe URL.")) - o:depends({ protocol = "_balancing" }) - - o = s:option(Value, "probeUrl", translate("Probe URL")) - o:depends({ useCustomProbeUrl = true }) - o:value("https://cp.cloudflare.com/", "Cloudflare") - o:value("https://www.gstatic.com/generate_204", "Gstatic") - o:value("https://www.google.com/generate_204", "Google") - o:value("https://www.youtube.com/generate_204", "YouTube") - o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)") - o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)") - o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)") - o.default = o.keylist[3] - o.description = translate("The URL used to detect the connection status.") - - o = s:option(Value, "probeInterval", translate("Probe Interval")) - o:depends({ protocol = "_balancing" }) - o.default = "1m" - o.placeholder = "1m" - o.description = translate("The interval between initiating probes.") .. "
" .. - translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. - translate("When the unit is not filled in, it defaults to seconds.") - - o = s:option(Value, "expected", translate("Preferred Node Count")) - o:depends({ balancingStrategy = "leastLoad" }) - o.datatype = "uinteger" - o.default = "2" - o.placeholder = "2" - o.description = translate("The load balancer selects the optimal number of nodes, and traffic is randomly distributed among them.") - - o = s:option(Value, "tolerance", translate("Failure Tolerance (%)")) - o:depends({ balancingStrategy = "leastLoad" }) - o.datatype = "uinteger" - o.default = "10" - o.placeholder = "10" - o.description = translate("The maximum acceptable speed test failure rate. For example, 1 means allowing a 1% failure rate.") -end -- [[ Load balancing End ]] - -if load_iface_options then -- [[ Custom Interface Start ]] - o = s:option(Value, "iface", translate("Interface")) - o:depends({ protocol = "_iface" }) - for _, d in ipairs(netdev_list) do - o:value(d.name, d.label) - end -end -- [[ Custom Interface End ]] - - --- [[ Normal single node Start ]] -if load_normal_options then - -o = s:option(Value, "address", translate("Address (Support Domain Name)")) - -o = s:option(Value, "port", translate("Port")) -o.datatype = "port" - -o = s:option(Value, "uuid", translate("ID")) -o.password = true -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) - -o = s:option(Value, "username", translate("Username")) -o:depends({ protocol = "http" }) -o:depends({ protocol = "socks" }) - -o = s:option(Value, "password", translate("Password")) -o.password = true -o:depends({ protocol = "http" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "trojan" }) - -o = s:option(ListValue, "security", translate("Encrypt Method")) -for a, t in ipairs(security_list) do o:value(t) end -o:depends({ protocol = "vmess" }) - -o = s:option(Value, "encryption", translate("Encrypt Method") .. " (encryption)") -o.default = "none" -o.placeholder = "none" -o:depends({ protocol = "vless" }) -o.validate = function(self, value) - value = api.trim(value) - return (value == "" and "none" or value) -end - -o = s:option(ListValue, "ss_method", translate("Encrypt Method")) -for a, t in ipairs(ss_method_list) do o:value(t) end -o:depends({ protocol = "shadowsocks" }) - -o = s:option(ListValue, "flow", translate("flow")) -o.default = "" -o:value("", translate("Disable")) -o:value("xtls-rprx-vision") -o:value("xtls-rprx-vision-udp443") -o:depends({ protocol = "vless" }) - ----- [[hysteria2]] -o = s:option(Value, "hysteria2_hop", translate("Port hopping range")) -o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") -o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - -o = s:option(Value, "hysteria2_hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) -o.datatype = "or(uinteger,portrange)" -o.placeholder = "30" -o.default = "30" -o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - -o = s:option(Flag, "hysteria2_realms", translate("Realms")) -o.default = "0" -if api.compare_versions(xray_version, ">", "26.5.9") then - o:depends({ protocol = "hysteria2"}) -else - o:depends({ protocol = "__hide"}) -end - -o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") -o:depends({ hysteria2_realms = "1" }) -o.validate = function(self, value) - value = api.trim(value) - local realm = api.parse_realm_uri(value) - if realm then return value end - return nil, translate("Invalid Realm URL.") -end - -o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) -o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } -o:depends({ hysteria2_realms = "1" }) - -o = s:option(Value, "hysteria2_auth_password", translate("Auth Password")) -o.password = true -o:depends({ protocol = "hysteria2"}) - -o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) -o:value("", translate("Disable")) -o:value("salamander") -o:value("gecko") -o:depends({ protocol = "hysteria2" }) - -o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) -o:depends({ hysteria2_obfs_type = "salamander" }) -o:depends({ hysteria2_obfs_type = "gecko" }) - -o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) -o.datatype = "uinteger" -o.placeholder = "512" -o.default = "512" -o:depends({ hysteria2_obfs_type = "gecko" }) - -o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) -o.datatype = "uinteger" -o.placeholder = "1200" -o.default = "1200" -o:depends({ hysteria2_obfs_type = "gecko" }) - -o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) -o:depends({ protocol = "hysteria2" }) - -o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) -o:depends({ protocol = "hysteria2" }) - -o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") -o.datatype = "range(4,120)" -o:depends({ protocol = "hysteria2"}) - -o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") -o.datatype = "range(2,60)" -o:depends({ protocol = "hysteria2"}) - -o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection")) -o.default = "0" -o:depends({ protocol = "hysteria2"}) ----- [[hysteria2 end]] - -o = s:option(Flag, "tls", translate("TLS")) -o.default = 0 -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "http" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "trojan" }) -o:depends({ protocol = "shadowsocks" }) - -o = s:option(Flag, "reality", translate("REALITY")) -o.default = 0 -o:depends({ tls = true, transport = "raw" }) -o:depends({ tls = true, transport = "ws" }) -o:depends({ tls = true, transport = "grpc" }) -o:depends({ tls = true, transport = "httpupgrade" }) -o:depends({ tls = true, transport = "xhttp" }) - -o = s:option(ListValue, "alpn", translate("alpn")) -o.default = "default" -o:value("default", translate("Default")) -o:value("h3") -o:value("h2") -o:value("h3,h2") -o:value("http/1.1") -o:value("h2,http/1.1") -o:value("h3,h2,http/1.1") -o:depends({ tls = true, reality = false }) - --- o = s:option(Value, "minversion", translate("minversion")) --- o.default = "1.3" --- o:value("1.3") --- o:depends({ tls = true }) - -o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain")) -o:depends({ tls = true }) -o:depends({ protocol = "hysteria2" }) - -o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)")) -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "hysteria2" }) -o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") .. - string.format("%s", "→ " .. translate("Fetch Manually")) - -o = s:option(Value, "tls_CertByName", translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name.")) -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "hysteria2" }) - -o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)")) -o.default = "0" -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "hysteria2" }) - -o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format.")) -o.default = "" -o.rows = 5 -o.wrap = "off" -o:depends({ tls_certificate = true }) -o.validate = function(self, value) - value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") - return value -end - -o = s:option(Flag, "ech", translate("ECH")) -o.default = "0" -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - -o = s:option(TextValue, "ech_config", translate("ECH Config")) -o.default = "" -o.rows = 5 -o.wrap = "soft" -o:depends({ ech = true }) -o.validate = function(self, value) - return api.trim(value:gsub("[\r\n]", "")) -end - --- [[ REALITY ]] -- -o = s:option(Value, "reality_publicKey", translate("Public Key")) -o:depends({ tls = true, reality = true }) - -o = s:option(Value, "reality_shortId", translate("Short Id")) -o:depends({ tls = true, reality = true }) - -o = s:option(Value, "reality_spiderX", translate("Spider X")) -o.placeholder = "/" -o:depends({ tls = true, reality = true }) - -o = s:option(Flag, "utls", translate("uTLS")) -o.default = "0" -o:depends({ tls = true, reality = false }) - -o = s:option(ListValue, "fingerprint", translate("Finger Print")) -o:value("chrome") -o:value("firefox") -o:value("edge") -o:value("safari") -o:value("360") -o:value("qq") -o:value("ios") -o:value("android") -o:value("random") -o:value("randomized") -o:value("unsafe") -o.default = "chrome" -o:depends({ tls = true, utls = true }) -o:depends({ tls = true, reality = true }) - -o = s:option(Flag, "use_mldsa65Verify", translate("ML-DSA-65")) -o.default = "0" -o:depends({ tls = true, reality = true }) - -o = s:option(DynamicList, "cipherSuites", translate("Cipher Suites"), '***' .. " " .. translate("Configures the list of supported cipher suites.")) -o:value("TLS_AES_128_GCM_SHA256") -o:value("TLS_AES_256_GCM_SHA384") -o:value("TLS_CHACHA20_POLY1305_SHA256") -o:value("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA") -o:value("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA") -o:value("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA") -o:value("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA") -o:value("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") -o:value("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384") -o:value("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") -o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") -o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") -o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") -o:depends({ tls = true, reality = false }) - -o = s:option(TextValue, "reality_mldsa65Verify", "ML-DSA-65 " .. translate("Public key")) -o.default = "" -o.rows = 5 -o.wrap = "soft" -o:depends({ use_mldsa65Verify = true }) -o.validate = function(self, value) - return api.trim(value:gsub("[\r\n]", "")) -end - -o = s:option(ListValue, "transport", translate("Transport")) -o:value("raw", "RAW (TCP)") -o:value("mkcp", "mKCP") -o:value("ws", "WebSocket") -o:value("grpc", "gRPC") -o:value("httpupgrade", "HttpUpgrade") -o:value("xhttp", "XHTTP") -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "trojan" }) - -o = s:option(Value, "wireguard_public_key", translate("Public Key")) -o:depends({ protocol = "wireguard" }) - -o = s:option(Value, "wireguard_secret_key", translate("Private Key")) -o:depends({ protocol = "wireguard" }) - -o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key")) -o:depends({ protocol = "wireguard" }) - -o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) -o:depends({ protocol = "wireguard" }) - -o = s:option(Value, "wireguard_mtu", translate("MTU")) -o.default = "1420" -o:depends({ protocol = "wireguard" }) - -if api.compare_versions(xray_version, ">=", "1.8.0") then - o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) - o:depends({ protocol = "wireguard" }) -end - -o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive")) -o.default = "0" -o:depends({ protocol = "wireguard" }) - --- [[ RAW ]]-- -o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) -o:value("none", "none") -o:value("http", "http") -o:depends({ transport = "raw" }) - -o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) -o:depends({ tcp_guise = "http" }) - -o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) -o.placeholder = "/" -o:depends({ tcp_guise = "http" }) - --- [[ mKCP ]]-- -o = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.')) -for a, t in ipairs(header_type_list) do o:value(t) end -o:depends({ transport = "mkcp" }) - -o = s:option(Value, "mkcp_domain", translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) -o:depends({ mkcp_guise = "dns" }) - -o = s:option(Value, "mkcp_mtu", translate("KCP MTU")) -o.datatype = "uinteger" -o.default = 1350 -o:depends({ transport = "mkcp" }) - -o = s:option(Value, "mkcp_seed", translate("KCP Seed")) -o:depends({ transport = "mkcp" }) - --- [[ WebSocket ]]-- -o = s:option(Value, "ws_host", translate("WebSocket Host")) -o:depends({ transport = "ws" }) - -o = s:option(Value, "ws_path", translate("WebSocket Path")) -o.placeholder = "/" -o:depends({ transport = "ws" }) - -o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)")) -o.datatype = "integer" -o:depends({ transport = "ws" }) - --- [[ gRPC ]]-- -o = s:option(Value, "grpc_serviceName", "ServiceName") -o:depends({ transport = "grpc" }) - -o = s:option(ListValue, "grpc_mode", "gRPC " .. translate("Transfer mode")) -o:value("gun") -o:value("multi") -o:depends({ transport = "grpc" }) - -o = s:option(Flag, "grpc_health_check", translate("Health check")) -o:depends({ transport = "grpc" }) - -o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout")) -o.default = "10" -o:depends({ grpc_health_check = true }) - -o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout")) -o.default = "20" -o:depends({ grpc_health_check = true }) - -o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream")) -o.default = "0" -o:depends({ grpc_health_check = true }) - -o = s:option(Value, "grpc_initial_windows_size", translate("Initial Windows Size")) -o.default = "0" -o:depends({ transport = "grpc" }) - --- [[ HttpUpgrade ]]-- -o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host")) -o:depends({ transport = "httpupgrade" }) - -o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path")) -o.placeholder = "/" -o:depends({ transport = "httpupgrade" }) - --- [[ XHTTP ]]-- -o = s:option(ListValue, "xhttp_mode", "XHTTP " .. translate("Mode")) -o:depends({ transport = "xhttp" }) -o.default = "auto" -o:value("auto") -o:value("packet-up") -o:value("stream-up") -o:value("stream-one") - -o = s:option(Value, "xhttp_host", translate("XHTTP Host")) -o:depends({ transport = "xhttp" }) - -o = s:option(Value, "xhttp_path", translate("XHTTP Path")) -o.placeholder = "/" -o:depends({ transport = "xhttp" }) - -o = s:option(Flag, "use_xhttp_extra", translate("XHTTP Extra")) -o.default = "0" -o:depends({ transport = "xhttp" }) - -o = s:option(TextValue, "xhttp_extra", " ", translate("An XHttpObject in JSON format, used for sharing.")) -o:depends({ use_xhttp_extra = true }) -o.rows = 10 -o.wrap = "off" -o.datatype = "json" -local o_validate = o.validate -o.validate = function(self, value) - value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n") - local v = o_validate(self, value) - if v then return v end - return nil, "XHTTP Extra " .. translate("Must be JSON text!") -end -o.custom_cfgvalue = function(self, section, value) - local raw = m:get(section, "xhttp_extra") - if raw then - return api.base64Decode(raw) - end -end -o.custom_write = function(self, section, value) - m:set(section, "xhttp_extra", api.base64Encode(value) or "") - local success, data = pcall(api.jsonc.parse, value) - if success and data then - local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address) - or (data.downloadSettings and data.downloadSettings.address) - if address and address ~= "" then - address = address:gsub("^%[", ""):gsub("%]$", "") - m:set(section, "download_address", address) - else - m:del(section, "download_address") - end - else - m:del(section, "download_address") - end -end -o.custom_remove = function(self, section, value) - m:del(section, "xhttp_extra") - m:del(section, "download_address") -end - --- [[ User-Agent ]]-- -o = s:option(Value, "user_agent", translate("User-Agent")) -o.default = "" -o:value("", translate("default")) -o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") -o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox") -o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari") -o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") -o:value("Go-http-client/1.1", "golang") -o:value("curl/7.68.0", "curl") -o:depends({ tcp_guise = "http" }) -o:depends({ transport = "ws" }) -o:depends({ transport = "httpupgrade" }) -o:depends({ transport = "xhttp" }) -o:depends({ transport = "grpc" }) - --- [[ Mux.Cool ]]-- -o = s:option(Flag, "mux", "Mux", translate("Enable Mux.Cool")) -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless", transport = "raw" }) -o:depends({ protocol = "vless", transport = "ws" }) -o:depends({ protocol = "vless", transport = "grpc" }) -o:depends({ protocol = "vless", transport = "httpupgrade" }) -o:depends({ protocol = "http" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "trojan" }) - -o = s:option(Value, "mux_concurrency", translate("Mux concurrency")) -o.default = -1 -o:depends({ mux = true }) - -o = s:option(Value, "xudp_concurrency", translate("XUDP Mux concurrency")) -o.default = 8 -o:depends({ mux = true }) - ---[[FinalMask]] -o = s:option(Flag, "use_finalmask", "FinalMask") -o.default = "0" -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "trojan" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "wireguard" }) -o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - -o = s:option(TextValue, "finalmask", " ") -o:depends({ use_finalmask = true }) -o.rows = 10 -o.wrap = "off" -o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "
" .. - translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.") -o.datatype = "json" -local o_validate = o.validate -o.validate = function(self, value) - value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n") - local v = o_validate(self, value) - if v then return v end - return nil, "FinalMask " .. translate("Must be JSON text!") -end -o.custom_cfgvalue = function(self, section, value) - local raw = m:get(section, "finalmask") - if raw then - return api.base64Decode(raw) - end -end -o.custom_write = function(self, section, value) - m:set(section, "finalmask", api.base64Encode(value) or "") -end - ---[[Fast Open]] -o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) -o.default = 0 - ---[[tcpMptcp]] -o = s:option(Flag, "tcpMptcp", "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration.")) -o.default = 0 - -o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution.")) -o:value("", translate("Auto")) -o:value("tcp", "TCP") -o:value("udp", "UDP") -o:value("https", "HTTPS") - -o = s:option(Value, "domain_resolver_dns", "DNS") -o.datatype = "or(ipaddr,ipaddrport)" -o:value("114.114.114.114") -o:value("223.5.5.5:53") -o.default = o.keylist[1] -o:depends({ domain_resolver = "tcp" }) -o:depends({ domain_resolver = "udp" }) - -o = s:option(Value, "domain_resolver_dns_https", "DNS") -o:value("https://120.53.53.53/dns-query", "DNSPod") -o:value("https://223.5.5.5/dns-query", "AliDNS") -o.default = o.keylist[1] -o:depends({ domain_resolver = "https" }) - -o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) -o.default = "" -o:value("", translate("Auto")) -o:value("UseIPv4", translate("IPv4 Only")) -o:value("UseIPv6", translate("IPv6 Only")) - -o = s:option(Flag, "happy_eyeballs", translate("Enable Happy Eyeballs"), translate("Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection.")) -o.default = 0 - -local protocols = s.fields["protocol"].keylist -if #protocols > 0 then - for i, v in ipairs(protocols) do - if not v:find("^_") then - local depends_condition = { protocol = v } - if v == "hysteria2" then - depends_condition["hysteria2_realms"] = false - end - s.fields["address"]:depends(depends_condition) - s.fields["port"]:depends(depends_condition) - s.fields["domain_resolver"]:depends(depends_condition) - s.fields["happy_eyeballs"]:depends(depends_condition) - - local strategy_depends = api.clone(depends_condition) - strategy_depends["happy_eyeballs"] = false - s.fields["domain_strategy"]:depends(strategy_depends) - - if v ~= "hysteria2" then - s.fields["tcp_fast_open"]:depends({ protocol = v }) - s.fields["tcpMptcp"]:depends({ protocol = v }) - end - end - end -end -end --- [[ Normal single node End ]] - -if not load_shunt_options then - o = s:option(ListValue, "chain_proxy", translate("Chain Proxy")) - o:value("", translate("Close(Not use)")) - if not (load_iface_options or load_balancing_options) then - -- Special node cannot be use pre-proxy. - o:value("1", translate("Preproxy Node")) - o:value("3", translate("Outbound Interface")) - end - o:value("2", translate("Landing Node")) - - o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"), translate("Only support a layer of proxy.")) - o1:depends({ chain_proxy = "1", hysteria2_realms = false }) - o1.template = m:template_path("/cbi/nodes_listvalue") - o1.group = {} - - o3 = s:option(Value, "outbound_iface", translate("Outbound Interface")) - o3:depends({ chain_proxy = "3" }) - o3:value("", translate("All")) - for _, d in ipairs(netdev_list) do - o3:value(d.name, d.label) - end - - o2 = s:option(ListValue, "to_node", translate("Landing Node"), translate("Only support a layer of proxy.")) - o2:depends({ chain_proxy = "2", hysteria2_realms = false }) - o2.template = m:template_path("/cbi/nodes_listvalue") - o2.group = {} - - for k1, v1 in pairs(node_list) do - if k1 ~= "shunt_list" and k1 ~= "iface_list" then - for i, v in ipairs(v1) do - if v.id ~= arg[1] then - o1:value(v.id, v.remark) - o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default") - if k1 == "normal_list" then - -- Landing Node not support use special node. - o2:value(v.id, v.remark) - o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default") - end - end - end - end - end -end - -api.luci_types(s1, s) - -if load_shunt_options then - local current_node = m:get(arg[1]) or {} - local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua") - setfenv(shunt_lua, getfenv(1))(m, s1, { - node_id = arg[1], - node = current_node, - node_list = node_list, - }) -end +if not api.finded_com("xray") then + return +end + +-- [[ Xray ]] +local m, s1 = ... +local type_name = "Xray" + +s1.fields["type"]:value(type_name, "Xray") +if not s1.fields["type"].default then + s1.fields["type"].default = type_name +end + +if s1.val["type"] ~= type_name then + return +end + +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "xray_" + +local formvalue_proto = luci.http.formvalue(formvalue_key .. "protocol") + +if formvalue_proto then s1.val["protocol"] = formvalue_proto end + +local arg_select_proto = luci.http.formvalue("select_proto") or "" + +local ss_method_list = { + "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "chacha20-ietf-poly1305", "xchacha20-poly1305", "xchacha20-ietf-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" +} + +local security_list = { "auto", "aes-128-gcm", "chacha20-poly1305" } + +local header_type_list = { + "none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns" +} + +local xray_version = api.get_app_version("xray") + +o = s:option(ListValue, "protocol", translate("Protocol")) +o:value("vmess", translate("Vmess")) +o:value("vless", translate("VLESS")) +o:value("http", translate("HTTP")) +o:value("socks", translate("Socks")) +o:value("shadowsocks", translate("Shadowsocks")) +o:value("trojan", translate("Trojan")) +o:value("wireguard", translate("WireGuard")) +if api.compare_versions(xray_version, ">=", "26.1.13") then + o:value("hysteria2", translate("Hysteria2")) +end +if api.compare_versions(xray_version, ">=", "1.8.12") then + o:value("_balancing", translate("Balancing")) +end +o:value("_shunt", translate("Shunt")) +o:value("_iface", translate("Custom Interface")) +function o.custom_cfgvalue(self, section) + if arg_select_proto ~= "" then + return arg_select_proto + else + return m:get(section, self.config_option) + end +end + +local load_balancing_options = s1.val["protocol"] == "_balancing" or arg_select_proto == "_balancing" +local load_shunt_options = s1.val["protocol"] == "_shunt" or arg_select_proto == "_shunt" +local load_iface_options = s1.val["protocol"] == "_iface" or arg_select_proto == "_iface" +local load_normal_options = true +if load_balancing_options or load_shunt_options or load_iface_options then + load_normal_options = nil +end +if not arg_select_proto:find("_") then + load_normal_options = true +end + +local netdev_list = api.get_network_devices() +local node_list = api.get_node_list() +local fallback_list = {} +local is_balancer = nil +for k, e in ipairs(node_list.balancing_list or {}) do + if e.id ~= arg[1] then + fallback_list[#fallback_list + 1] = { + id = e["id"], + remark = e["remark"], + group = e["group"], + fallback = e.o["fallback_node"], + } + else + is_balancer = true + end +end + +if load_balancing_options then -- [[ Load balancing Start ]] + o = s:option(ListValue, "node_add_mode", translate("Node Addition Method")) + o:depends({ protocol = "_balancing" }) + o.default = "manual" + o:value("manual", translate("Manual")) + o:value("batch", translate("Batch")) + + o = s:option(MultiValue, "balancing_node", translate("Load balancing node list"), translate("Load balancing node list, document")) + o:depends({ node_add_mode = "manual" }) + o.widget = "checkbox" + o.template = m:template_path("/cbi/nodes_multivalue") + o.group = {} + for k1, v1 in pairs(node_list) do + if k1 == "socks_list" or k1 == "normal_list" then + for i, v in ipairs(v1) do + o:value(v.id, v.remark) + o.group[#o.group+1] = v.group or "" + end + end + end + -- Reading the old DynamicList + function o.custom_cfgvalue(self, section) + return table.concat(m:get(section, "balancing_node") or {}, " ") + end + -- Write-and-hold DynamicList + function o.custom_write(self, section, value) + local old = m:get(section, "balancing_node") or {} + local new, set = {}, {} + for v in value:gmatch("%S+") do + new[#new + 1] = v + set[v] = 1 + end + for _, v in ipairs(old) do + if not set[v] then + m:set(section, "balancing_node", new) + return + end + set[v] = nil + end + for _ in pairs(set) do + m:set(section, "balancing_node", new) + return + end + end + + o = s:option(MultiValue, "node_group", translate("Select Group")) + o:depends({ node_add_mode = "batch" }) + o.widget = "checkbox" + o:value("default", translate("default")) + for k, v in pairs(groups) do + o:value(api.UrlEncode(k), k) + end + + o = s:option(Value, "node_match_rule", translate("Node Matching Rules")) + o:depends({ node_add_mode = "batch" }) + local descrStr = "Example: ^A && B && !C && D$
" + descrStr = descrStr .. "This means the node remark must start with A (^), include B, exclude C (!), and end with D ($).
" + descrStr = descrStr .. "Conditions are joined by && (AND), and their order does not affect the result.
" + descrStr = descrStr .. "Multiple groups can be separated by || (OR), matching succeeds if any group matches.
" + descrStr = descrStr .. "Example: A && B || C && D means (A AND B) OR (C AND D)." + o.description = translate(descrStr) + + o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy")) + o:depends({ protocol = "_balancing" }) + o:value("random") + o:value("roundRobin") + o:value("leastPing") + o:value("leastLoad") + o.default = "random" + + -- Fallback Node + o = s:option(ListValue, "fallback_node", translate("Fallback Node")) + o.group = {"",""} + o:value("", translate("Close(Not use)")) + o:value("_direct", translate("Direct Connection")) + o:depends({ protocol = "_balancing" }) + o.template = m:template_path("/cbi/nodes_listvalue") + -- Maximum number of fallback nesting layers + local MAX_FALLBACK_DEPTH = 3 + -- Check if a loop will form. + local function will_loop(start_id, target_id, depth) + depth = depth or 0 + -- Recursion stops after the maximum depth is exceeded. + if depth >= MAX_FALLBACK_DEPTH then + return false + end + for _, v in ipairs(fallback_list) do + if v.id == target_id then + local fb = v.fallback + -- No fallback + if not fb or fb == "" or fb == "_direct" then + return false + end + -- Loopback detected + if fb == start_id then + return true + end + -- Continue recursive checking + return will_loop(start_id, fb, depth + 1) + end + end + return false + end + -- Get fallback chain depth + local function get_fallback_depth(id, depth) + depth = depth or 0 + if depth >= MAX_FALLBACK_DEPTH then + return depth + end + for _, v in ipairs(fallback_list) do + if v.id == id then + local fb = v.fallback + if not fb or fb == "" or fb == "_direct" then + return depth + end + return get_fallback_depth(fb, depth + 1) + end + end + return depth + end + for _, v in ipairs(fallback_list) do + local depth = get_fallback_depth(v.id) + -- Once the maximum number of nested doll layers is exceeded, further selection of the balancer is not allowed. + if depth < MAX_FALLBACK_DEPTH + and not will_loop(arg[1], v.id) + then + o:value(v.id, v.remark) + o.group[#o.group + 1] = (v.group and v.group ~= "") and v.group or translate("default") + end + end + for k1, v1 in pairs(node_list) do + if k1 == "socks_list" or k1 == "normal_list" or k1 == "urltest_list" then + for i, v in ipairs(v1) do + o:value(v.id, v.remark) + o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + end + end + end + + o = s:option(Flag, "useCustomProbeUrl", translate("Use Custom Probe URL"), translate("By default the built-in probe URL will be used, enable this option to use a custom probe URL.")) + o:depends({ protocol = "_balancing" }) + + o = s:option(Value, "probeUrl", translate("Probe URL")) + o:depends({ useCustomProbeUrl = true }) + o:value("https://cp.cloudflare.com/", "Cloudflare") + o:value("https://www.gstatic.com/generate_204", "Gstatic") + o:value("https://www.google.com/generate_204", "Google") + o:value("https://www.youtube.com/generate_204", "YouTube") + o:value("https://connect.rom.miui.com/generate_204", "MIUI (CN)") + o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud (CN)") + o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)") + o.default = o.keylist[3] + o.description = translate("The URL used to detect the connection status.") + + o = s:option(Value, "probeInterval", translate("Probe Interval")) + o:depends({ protocol = "_balancing" }) + o.default = "1m" + o.placeholder = "1m" + o.description = translate("The interval between initiating probes.") .. "
" .. + translate("The time format is numbers + units, such as '10s', '2h45m', and the supported time units are s, m, h, which correspond to seconds, minutes, and hours, respectively.") .. "
" .. + translate("When the unit is not filled in, it defaults to seconds.") + + o = s:option(Value, "expected", translate("Preferred Node Count")) + o:depends({ balancingStrategy = "leastLoad" }) + o.datatype = "uinteger" + o.default = "2" + o.placeholder = "2" + o.description = translate("The load balancer selects the optimal number of nodes, and traffic is randomly distributed among them.") + + o = s:option(Value, "tolerance", translate("Failure Tolerance (%)")) + o:depends({ balancingStrategy = "leastLoad" }) + o.datatype = "uinteger" + o.default = "10" + o.placeholder = "10" + o.description = translate("The maximum acceptable speed test failure rate. For example, 1 means allowing a 1% failure rate.") +end -- [[ Load balancing End ]] + +if load_iface_options then -- [[ Custom Interface Start ]] + o = s:option(Value, "iface", translate("Interface")) + o:depends({ protocol = "_iface" }) + for _, d in ipairs(netdev_list) do + o:value(d.name, d.label) + end +end -- [[ Custom Interface End ]] + + +-- [[ Normal single node Start ]] +if load_normal_options then + +o = s:option(Value, "address", translate("Address (Support Domain Name)")) + +o = s:option(Value, "port", translate("Port")) +o.datatype = "port" + +o = s:option(Value, "uuid", translate("ID")) +o.password = true +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) + +o = s:option(Value, "username", translate("Username")) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) + +o = s:option(Value, "password", translate("Password")) +o.password = true +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) + +o = s:option(ListValue, "security", translate("Encrypt Method")) +for a, t in ipairs(security_list) do o:value(t) end +o:depends({ protocol = "vmess" }) + +o = s:option(Value, "encryption", translate("Encrypt Method") .. " (encryption)") +o.default = "none" +o.placeholder = "none" +o:depends({ protocol = "vless" }) +o.validate = function(self, value) + value = api.trim(value) + return (value == "" and "none" or value) +end + +o = s:option(ListValue, "ss_method", translate("Encrypt Method")) +for a, t in ipairs(ss_method_list) do o:value(t) end +o:depends({ protocol = "shadowsocks" }) + +o = s:option(ListValue, "flow", translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:value("xtls-rprx-vision-udp443") +o:depends({ protocol = "vless" }) + +---- [[hysteria2]] +o = s:option(Value, "hysteria2_hop", translate("Port hopping range")) +o.description = translate("Format as 1000:2000 or 1000-2000 Multiple groups are separated by commas (,).") +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) + +o = s:option(Value, "hysteria2_hop_interval", translate("Hop Interval(second)"), translate("Supports a fixed value or a random range (e.g., 30, 5-30), minimum 5.")) +o.datatype = "or(uinteger,portrange)" +o.placeholder = "30" +o.default = "30" +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) + +o = s:option(Flag, "hysteria2_realms", translate("Realms")) +o.default = "0" +if api.compare_versions(xray_version, ">", "26.5.9") then + o:depends({ protocol = "hysteria2"}) +else + o:depends({ protocol = "__hide"}) +end + +o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") +o:depends({ hysteria2_realms = "1" }) +o.validate = function(self, value) + value = api.trim(value) + local realm = api.parse_realm_uri(value) + if realm then return value end + return nil, translate("Invalid Realm URL.") +end + +o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) +o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } +o:depends({ hysteria2_realms = "1" }) + +o = s:option(Value, "hysteria2_auth_password", translate("Auth Password")) +o.password = true +o:depends({ protocol = "hysteria2"}) + +o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) +o:value("", translate("Disable")) +o:value("salamander") +o:value("gecko") +o:depends({ protocol = "hysteria2" }) + +o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) +o:depends({ hysteria2_obfs_type = "salamander" }) +o:depends({ hysteria2_obfs_type = "gecko" }) + +o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) +o.datatype = "uinteger" +o.placeholder = "512" +o.default = "512" +o:depends({ hysteria2_obfs_type = "gecko" }) + +o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) +o.datatype = "uinteger" +o.placeholder = "1200" +o.default = "1200" +o:depends({ hysteria2_obfs_type = "gecko" }) + +o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) +o:depends({ protocol = "hysteria2" }) + +o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) +o:depends({ protocol = "hysteria2" }) + +o = s:option(Value, "hysteria2_idle_timeout", translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)") +o.datatype = "range(4,120)" +o:depends({ protocol = "hysteria2"}) + +o = s:option(Value, "hysteria2_keep_alive_period", translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)") +o.datatype = "range(2,60)" +o:depends({ protocol = "hysteria2"}) + +o = s:option(Flag, "hysteria2_disable_mtu_discovery", translate("Disable MTU detection")) +o.default = "0" +o:depends({ protocol = "hysteria2"}) +---- [[hysteria2 end]] + +o = s:option(Flag, "tls", translate("TLS")) +o.default = 0 +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "shadowsocks" }) + +o = s:option(Flag, "reality", translate("REALITY")) +o.default = 0 +o:depends({ tls = true, transport = "raw" }) +o:depends({ tls = true, transport = "ws" }) +o:depends({ tls = true, transport = "grpc" }) +o:depends({ tls = true, transport = "httpupgrade" }) +o:depends({ tls = true, transport = "xhttp" }) + +o = s:option(ListValue, "alpn", translate("alpn")) +o.default = "default" +o:value("default", translate("Default")) +o:value("h3") +o:value("h2") +o:value("h3,h2") +o:value("http/1.1") +o:value("h2,http/1.1") +o:value("h3,h2,http/1.1") +o:depends({ tls = true, reality = false }) + +-- o = s:option(Value, "minversion", translate("minversion")) +-- o.default = "1.3" +-- o:value("1.3") +-- o:depends({ tls = true }) + +o = s:option(Value, "tls_serverName", "SNI " .. translate("Domain")) +o:depends({ tls = true }) +o:depends({ protocol = "hysteria2" }) + +o = s:option(Value, "tls_pinSHA256", translate("TLS Chain Fingerprint (SHA256)")) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2" }) +o.description = translate("Once set, connects only when the server’s chain fingerprint matches.") .. + string.format("%s", "→ " .. translate("Fetch Manually")) + +o = s:option(Value, "tls_CertByName", translate("TLS Certificate Name (CertName)"), translate("TLS is used to verify the leaf certificate name.")) +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2" }) + +o = s:option(Flag, "tls_certificate", translate("TLS Certificate (PEM)")) +o.default = "0" +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2" }) + +o = s:option(TextValue, "tls_certificate_pem", " ", translate("Full certificate (chain), PEM format.")) +o.default = "" +o.rows = 5 +o.wrap = "off" +o:depends({ tls_certificate = true }) +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n") + return value +end + +o = s:option(Flag, "ech", translate("ECH")) +o.default = "0" +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) + +o = s:option(TextValue, "ech_config", translate("ECH Config")) +o.default = "" +o.rows = 5 +o.wrap = "soft" +o:depends({ ech = true }) +o.validate = function(self, value) + return api.trim(value:gsub("[\r\n]", "")) +end + +-- [[ REALITY ]] -- +o = s:option(Value, "reality_publicKey", translate("Public Key")) +o:depends({ tls = true, reality = true }) + +o = s:option(Value, "reality_shortId", translate("Short Id")) +o:depends({ tls = true, reality = true }) + +o = s:option(Value, "reality_spiderX", translate("Spider X")) +o.placeholder = "/" +o:depends({ tls = true, reality = true }) + +o = s:option(Flag, "utls", translate("uTLS")) +o.default = "0" +o:depends({ tls = true, reality = false }) + +o = s:option(ListValue, "fingerprint", translate("Finger Print")) +o:value("chrome") +o:value("firefox") +o:value("edge") +o:value("safari") +o:value("360") +o:value("qq") +o:value("ios") +o:value("android") +o:value("random") +o:value("randomized") +o:value("unsafe") +o.default = "chrome" +o:depends({ tls = true, utls = true }) +o:depends({ tls = true, reality = true }) + +o = s:option(Flag, "use_mldsa65Verify", translate("ML-DSA-65")) +o.default = "0" +o:depends({ tls = true, reality = true }) + +o = s:option(DynamicList, "cipherSuites", translate("Cipher Suites"), '***' .. " " .. translate("Configures the list of supported cipher suites.")) +o:value("TLS_AES_128_GCM_SHA256") +o:value("TLS_AES_256_GCM_SHA384") +o:value("TLS_CHACHA20_POLY1305_SHA256") +o:value("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA") +o:value("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA") +o:value("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA") +o:value("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA") +o:value("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256") +o:value("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384") +o:value("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256") +o:value("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384") +o:value("TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256") +o:value("TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256") +o:depends({ tls = true, reality = false }) + +o = s:option(TextValue, "reality_mldsa65Verify", "ML-DSA-65 " .. translate("Public key")) +o.default = "" +o.rows = 5 +o.wrap = "soft" +o:depends({ use_mldsa65Verify = true }) +o.validate = function(self, value) + return api.trim(value:gsub("[\r\n]", "")) +end + +o = s:option(ListValue, "transport", translate("Transport")) +o:value("raw", "RAW (TCP)") +o:value("mkcp", "mKCP") +o:value("ws", "WebSocket") +o:value("grpc", "gRPC") +o:value("httpupgrade", "HttpUpgrade") +o:value("xhttp", "XHTTP") +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) + +o = s:option(Value, "wireguard_public_key", translate("Public Key")) +o:depends({ protocol = "wireguard" }) + +o = s:option(Value, "wireguard_secret_key", translate("Private Key")) +o:depends({ protocol = "wireguard" }) + +o = s:option(Value, "wireguard_preSharedKey", translate("Pre shared key")) +o:depends({ protocol = "wireguard" }) + +o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) +o:depends({ protocol = "wireguard" }) + +o = s:option(Value, "wireguard_mtu", translate("MTU")) +o.default = "1420" +o:depends({ protocol = "wireguard" }) + +if api.compare_versions(xray_version, ">=", "1.8.0") then + o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) + o:depends({ protocol = "wireguard" }) +end + +o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive")) +o.default = "0" +o:depends({ protocol = "wireguard" }) + +-- [[ RAW ]]-- +o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) +o:value("none", "none") +o:value("http", "http") +o:depends({ transport = "raw" }) + +o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) +o:depends({ tcp_guise = "http" }) + +o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) +o.placeholder = "/" +o:depends({ tcp_guise = "http" }) + +-- [[ mKCP ]]-- +o = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.')) +for a, t in ipairs(header_type_list) do o:value(t) end +o:depends({ transport = "mkcp" }) + +o = s:option(Value, "mkcp_domain", translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) +o:depends({ mkcp_guise = "dns" }) + +o = s:option(Value, "mkcp_mtu", translate("KCP MTU")) +o.datatype = "uinteger" +o.default = 1350 +o:depends({ transport = "mkcp" }) + +o = s:option(Value, "mkcp_seed", translate("KCP Seed")) +o:depends({ transport = "mkcp" }) + +-- [[ WebSocket ]]-- +o = s:option(Value, "ws_host", translate("WebSocket Host")) +o:depends({ transport = "ws" }) + +o = s:option(Value, "ws_path", translate("WebSocket Path")) +o.placeholder = "/" +o:depends({ transport = "ws" }) + +o = s:option(Value, "ws_heartbeatPeriod", translate("HeartbeatPeriod(second)")) +o.datatype = "integer" +o:depends({ transport = "ws" }) + +-- [[ gRPC ]]-- +o = s:option(Value, "grpc_serviceName", "ServiceName") +o:depends({ transport = "grpc" }) + +o = s:option(ListValue, "grpc_mode", "gRPC " .. translate("Transfer mode")) +o:value("gun") +o:value("multi") +o:depends({ transport = "grpc" }) + +o = s:option(Flag, "grpc_health_check", translate("Health check")) +o:depends({ transport = "grpc" }) + +o = s:option(Value, "grpc_idle_timeout", translate("Idle timeout")) +o.default = "10" +o:depends({ grpc_health_check = true }) + +o = s:option(Value, "grpc_health_check_timeout", translate("Health check timeout")) +o.default = "20" +o:depends({ grpc_health_check = true }) + +o = s:option(Flag, "grpc_permit_without_stream", translate("Permit without stream")) +o.default = "0" +o:depends({ grpc_health_check = true }) + +o = s:option(Value, "grpc_initial_windows_size", translate("Initial Windows Size")) +o.default = "0" +o:depends({ transport = "grpc" }) + +-- [[ HttpUpgrade ]]-- +o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host")) +o:depends({ transport = "httpupgrade" }) + +o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path")) +o.placeholder = "/" +o:depends({ transport = "httpupgrade" }) + +-- [[ XHTTP ]]-- +o = s:option(ListValue, "xhttp_mode", "XHTTP " .. translate("Mode")) +o:depends({ transport = "xhttp" }) +o.default = "auto" +o:value("auto") +o:value("packet-up") +o:value("stream-up") +o:value("stream-one") + +o = s:option(Value, "xhttp_host", translate("XHTTP Host")) +o:depends({ transport = "xhttp" }) + +o = s:option(Value, "xhttp_path", translate("XHTTP Path")) +o.placeholder = "/" +o:depends({ transport = "xhttp" }) + +o = s:option(Flag, "use_xhttp_extra", translate("XHTTP Extra")) +o.default = "0" +o:depends({ transport = "xhttp" }) + +o = s:option(TextValue, "xhttp_extra", " ", translate("An XHttpObject in JSON format, used for sharing.")) +o:depends({ use_xhttp_extra = true }) +o.rows = 10 +o.wrap = "off" +o.datatype = "json" +local o_validate = o.validate +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n") + local v = o_validate(self, value) + if v then return v end + return nil, "XHTTP Extra " .. translate("Must be JSON text!") +end +o.custom_cfgvalue = function(self, section, value) + local raw = m:get(section, "xhttp_extra") + if raw then + return api.base64Decode(raw) + end +end +o.custom_write = function(self, section, value) + m:set(section, "xhttp_extra", api.base64Encode(value) or "") + local success, data = pcall(api.jsonc.parse, value) + if success and data then + local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address) + or (data.downloadSettings and data.downloadSettings.address) + if address and address ~= "" then + address = address:gsub("^%[", ""):gsub("%]$", "") + m:set(section, "download_address", address) + else + m:del(section, "download_address") + end + else + m:del(section, "download_address") + end +end +o.custom_remove = function(self, section, value) + m:del(section, "xhttp_extra") + m:del(section, "download_address") +end + +-- [[ User-Agent ]]-- +o = s:option(Value, "user_agent", translate("User-Agent")) +o.default = "" +o:value("", translate("default")) +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox") +o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari") +o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge") +o:value("Go-http-client/1.1", "golang") +o:value("curl/7.68.0", "curl") +o:depends({ tcp_guise = "http" }) +o:depends({ transport = "ws" }) +o:depends({ transport = "httpupgrade" }) +o:depends({ transport = "xhttp" }) +o:depends({ transport = "grpc" }) + +-- [[ Mux.Cool ]]-- +o = s:option(Flag, "mux", "Mux", translate("Enable Mux.Cool")) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless", transport = "raw" }) +o:depends({ protocol = "vless", transport = "ws" }) +o:depends({ protocol = "vless", transport = "grpc" }) +o:depends({ protocol = "vless", transport = "httpupgrade" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) + +o = s:option(Value, "mux_concurrency", translate("Mux concurrency")) +o.default = -1 +o:depends({ mux = true }) + +o = s:option(Value, "xudp_concurrency", translate("XUDP Mux concurrency")) +o.default = 8 +o:depends({ mux = true }) + +--[[FinalMask]] +o = s:option(Flag, "use_finalmask", "FinalMask") +o.default = "0" +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "wireguard" }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) + +o = s:option(TextValue, "finalmask", " ") +o:depends({ use_finalmask = true }) +o.rows = 10 +o.wrap = "off" +o.description = translate("An FinalMaskObject in JSON format, used for sharing.") .. "
" .. + translate("Custom finalmask overrides mkcp, hysteria2, fragment, noise, and related settings.") +o.datatype = "json" +local o_validate = o.validate +o.validate = function(self, value) + value = api.trim(value):gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n") + local v = o_validate(self, value) + if v then return v end + return nil, "FinalMask " .. translate("Must be JSON text!") +end +o.custom_cfgvalue = function(self, section, value) + local raw = m:get(section, "finalmask") + if raw then + return api.base64Decode(raw) + end +end +o.custom_write = function(self, section, value) + m:set(section, "finalmask", api.base64Encode(value) or "") +end + +--[[Fast Open]] +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) +o.default = 0 + +--[[tcpMptcp]] +o = s:option(Flag, "tcpMptcp", "tcpMptcp", translate("Enable Multipath TCP, need to be enabled in both server and client configuration.")) +o.default = 0 + +o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution.")) +o:value("", translate("Auto")) +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("https", "HTTPS") + +o = s:option(Value, "domain_resolver_dns", "DNS") +o.datatype = "or(ipaddr,ipaddrport)" +o:value("114.114.114.114") +o:value("223.5.5.5:53") +o.default = o.keylist[1] +o:depends({ domain_resolver = "tcp" }) +o:depends({ domain_resolver = "udp" }) + +o = s:option(Value, "domain_resolver_dns_https", "DNS") +o:value("https://120.53.53.53/dns-query", "DNSPod") +o:value("https://223.5.5.5/dns-query", "AliDNS") +o.default = o.keylist[1] +o:depends({ domain_resolver = "https" }) + +o = s:option(ListValue, "domain_strategy", translate("Domain Strategy"), translate("If is domain name, The requested domain name will be resolved to IP before connect.")) +o.default = "" +o:value("", translate("Auto")) +o:value("UseIPv4", translate("IPv4 Only")) +o:value("UseIPv6", translate("IPv6 Only")) + +o = s:option(Flag, "happy_eyeballs", translate("Enable Happy Eyeballs"), translate("Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection.")) +o.default = 0 + +local protocols = s.fields["protocol"].keylist +if #protocols > 0 then + for i, v in ipairs(protocols) do + if not v:find("^_") then + local depends_condition = { protocol = v } + if v == "hysteria2" then + depends_condition["hysteria2_realms"] = false + end + s.fields["address"]:depends(depends_condition) + s.fields["port"]:depends(depends_condition) + s.fields["domain_resolver"]:depends(depends_condition) + s.fields["happy_eyeballs"]:depends(depends_condition) + + local strategy_depends = api.clone(depends_condition) + strategy_depends["happy_eyeballs"] = false + s.fields["domain_strategy"]:depends(strategy_depends) + + if v ~= "hysteria2" then + s.fields["tcp_fast_open"]:depends({ protocol = v }) + s.fields["tcpMptcp"]:depends({ protocol = v }) + end + end + end +end +end +-- [[ Normal single node End ]] + +if not load_shunt_options then + o = s:option(ListValue, "chain_proxy", translate("Chain Proxy")) + o:value("", translate("Close(Not use)")) + if not (load_iface_options or load_balancing_options) then + -- Special node cannot be use pre-proxy. + o:value("1", translate("Preproxy Node")) + o:value("3", translate("Outbound Interface")) + end + o:value("2", translate("Landing Node")) + + o1 = s:option(ListValue, "preproxy_node", translate("Preproxy Node"), translate("Only support a layer of proxy.")) + o1:depends({ chain_proxy = "1", hysteria2_realms = false }) + o1.template = m:template_path("/cbi/nodes_listvalue") + o1.group = {} + + o3 = s:option(Value, "outbound_iface", translate("Outbound Interface")) + o3:depends({ chain_proxy = "3" }) + o3:value("", translate("All")) + for _, d in ipairs(netdev_list) do + o3:value(d.name, d.label) + end + + o2 = s:option(ListValue, "to_node", translate("Landing Node"), translate("Only support a layer of proxy.")) + o2:depends({ chain_proxy = "2", hysteria2_realms = false }) + o2.template = m:template_path("/cbi/nodes_listvalue") + o2.group = {} + + for k1, v1 in pairs(node_list) do + if k1 ~= "shunt_list" and k1 ~= "iface_list" then + for i, v in ipairs(v1) do + if v.id ~= arg[1] then + o1:value(v.id, v.remark) + o1.group[#o1.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + if k1 == "normal_list" then + -- Landing Node not support use special node. + o2:value(v.id, v.remark) + o2.group[#o2.group+1] = (v.group and v.group ~= "") and v.group or translate("default") + end + end + end + end + end +end + +api.luci_types(s1, s) + +if load_shunt_options then + local current_node = m:get(arg[1]) or {} + local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua") + setfenv(shunt_lua, getfenv(1))(m, s1, { + node_id = arg[1], + node = current_node, + node_list = node_list, + }) +end diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/3_ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/3_ss-rust.lua index e3bc6907..f4fe81f2 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/3_ss-rust.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/3_ss-rust.lua @@ -1,68 +1,68 @@ -if not api.is_finded("sslocal") then - return -end - --- [[ Shadowsocks Rust ]] -local m, s1 = ... -local type_name = "SS-Rust" - -s1.fields["type"]:value(type_name, "Shadowsocks Rust") - -if s1.val["type"] ~= type_name then - return -end - -local s = NamedSection(m, arg[1], "server") -s.type_name = type_name -s.option_prefix = "ssrust_" - -local ssrust_encrypt_method_list = { - "none", "plain", - "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", - "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305" -} - -o = s:option(Value, "address", translate("Address (Support Domain Name)")) - -o = s:option(Value, "port", translate("Port")) -o.datatype = "port" - -o = s:option(Value, "password", translate("Password")) -o.password = true - -o = s:option(Value, "method", translate("Encrypt Method")) -for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end - -o = s:option(Value, "timeout", translate("Connection Timeout")) -o.datatype = "uinteger" -o.default = 300 - -o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) -o.default = 0 - -o = s:option(Flag, "plugin_enabled", translate("plugin")) -o.default = 0 - -o = s:option(Value, "plugin", "SIP003 " .. translate("plugin"), translate("Supports custom SIP003 plugins, Make sure the plugin is installed.")) -o.default = "none" -o:value("none", translate("none")) -if api.is_finded("xray-plugin") then o:value("xray-plugin") end -if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end -if api.is_finded("obfs-local") then o:value("obfs-local") end -if api.is_finded("shadow-tls") then o:value("shadow-tls") end -o:depends({ plugin_enabled = true }) -o.validate = function(self, value, t) - if value and value ~= "" and value ~= "none" then - if not api.is_finded(value) then - return nil, value .. ": " .. translate("Can't find this file!") - else - return value - end - end - return nil -end - -o = s:option(Value, "plugin_opts", translate("opts")) -o:depends({ plugin_enabled = true }) - +if not api.is_finded("sslocal") then + return +end + +-- [[ Shadowsocks Rust ]] +local m, s1 = ... +local type_name = "SS-Rust" + +s1.fields["type"]:value(type_name, "Shadowsocks Rust") + +if s1.val["type"] ~= type_name then + return +end + +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "ssrust_" + +local ssrust_encrypt_method_list = { + "none", "plain", + "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", + "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305" +} + +o = s:option(Value, "address", translate("Address (Support Domain Name)")) + +o = s:option(Value, "port", translate("Port")) +o.datatype = "port" + +o = s:option(Value, "password", translate("Password")) +o.password = true + +o = s:option(Value, "method", translate("Encrypt Method")) +for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end + +o = s:option(Value, "timeout", translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 + +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) +o.default = 0 + +o = s:option(Flag, "plugin_enabled", translate("plugin")) +o.default = 0 + +o = s:option(Value, "plugin", "SIP003 " .. translate("plugin"), translate("Supports custom SIP003 plugins, Make sure the plugin is installed.")) +o.default = "none" +o:value("none", translate("none")) +if api.is_finded("xray-plugin") then o:value("xray-plugin") end +if api.is_finded("v2ray-plugin") then o:value("v2ray-plugin") end +if api.is_finded("obfs-local") then o:value("obfs-local") end +if api.is_finded("shadow-tls") then o:value("shadow-tls") end +o:depends({ plugin_enabled = true }) +o.validate = function(self, value, t) + if value and value ~= "" and value ~= "none" then + if not api.is_finded(value) then + return nil, value .. ": " .. translate("Can't find this file!") + else + return value + end + end + return nil +end + +o = s:option(Value, "plugin_opts", translate("opts")) +o:depends({ plugin_enabled = true }) + api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/4_ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/4_ssr.lua index 4d18c36e..c9f281e1 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/4_ssr.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/4_ssr.lua @@ -1,66 +1,66 @@ -if not api.is_finded("ssr-local") then - return -end - --- [[ ShadowsocksR Libev ]] -local m, s1 = ... -local type_name = "SSR" - -s1.fields["type"]:value(type_name, "ShadowsocksR Libev") - -if s1.val["type"] ~= type_name then - return -end - -local s = NamedSection(m, arg[1], "server") -s.type_name = type_name -s.option_prefix = "ssr_" - -local ssr_encrypt_method_list = { - "none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb", - "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", - "bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", - "cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20", - "chacha20-ietf" -} - -local ssr_protocol_list = { - "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", - "auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5", - "auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c", - "auth_chain_d", "auth_chain_e", "auth_chain_f" -} -local ssr_obfs_list = { - "plain", "http_simple", "http_post", "random_head", "tls_simple", - "tls1.0_session_auth", "tls1.2_ticket_auth" -} - -o = s:option(Value, "address", translate("Address (Support Domain Name)")) - -o = s:option(Value, "port", translate("Port")) -o.datatype = "port" - -o = s:option(Value, "password", translate("Password")) -o.password = true - -o = s:option(ListValue, "method", translate("Encrypt Method")) -for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end - -o = s:option(ListValue, "protocol", translate("Protocol")) -for a, t in ipairs(ssr_protocol_list) do o:value(t) end - -o = s:option(Value, "protocol_param", translate("Protocol_param")) - -o = s:option(ListValue, "obfs", translate("Obfs")) -for a, t in ipairs(ssr_obfs_list) do o:value(t) end - -o = s:option(Value, "obfs_param", translate("Obfs_param")) - -o = s:option(Value, "timeout", translate("Connection Timeout")) -o.datatype = "uinteger" -o.default = 300 - -o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) -o.default = 0 - +if not api.is_finded("ssr-local") then + return +end + +-- [[ ShadowsocksR Libev ]] +local m, s1 = ... +local type_name = "SSR" + +s1.fields["type"]:value(type_name, "ShadowsocksR Libev") + +if s1.val["type"] ~= type_name then + return +end + +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "ssr_" + +local ssr_encrypt_method_list = { + "none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb", + "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", + "bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", + "cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20", + "chacha20-ietf" +} + +local ssr_protocol_list = { + "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", + "auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5", + "auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c", + "auth_chain_d", "auth_chain_e", "auth_chain_f" +} +local ssr_obfs_list = { + "plain", "http_simple", "http_post", "random_head", "tls_simple", + "tls1.0_session_auth", "tls1.2_ticket_auth" +} + +o = s:option(Value, "address", translate("Address (Support Domain Name)")) + +o = s:option(Value, "port", translate("Port")) +o.datatype = "port" + +o = s:option(Value, "password", translate("Password")) +o.password = true + +o = s:option(ListValue, "method", translate("Encrypt Method")) +for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end + +o = s:option(ListValue, "protocol", translate("Protocol")) +for a, t in ipairs(ssr_protocol_list) do o:value(t) end + +o = s:option(Value, "protocol_param", translate("Protocol_param")) + +o = s:option(ListValue, "obfs", translate("Obfs")) +for a, t in ipairs(ssr_obfs_list) do o:value(t) end + +o = s:option(Value, "obfs_param", translate("Obfs_param")) + +o = s:option(Value, "timeout", translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 + +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open"), translate("Need node support required")) +o.default = 0 + api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/server_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/server_config.lua index 816d911d..4b5a9c5c 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/server_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/server_config.lua @@ -39,7 +39,7 @@ for filename in api.fs.dir(types_dir) do table.insert(type_table, filename) end table.sort(type_table, function(a, b) - return a < b + return a < b end) for index, value in ipairs(type_table) do diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_sing-box.lua index fd345466..972af357 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/1_sing-box.lua @@ -1,534 +1,534 @@ -local singbox_bin = api.finded_com("sing-box") - -if not singbox_bin then - return -end - --- [[ Sing-Box ]] -local m, s1 = ... -local type_name = "sing-box" - -s1.fields["type"]:value(type_name, "Sing-Box") -if not s1.fields["type"].default then - s1.fields["type"].default = type_name -end - -if not s1.val["type"] then - s1.val["type"] = type_name -end - -if s1.val["type"] and s1.val["type"] ~= type_name then - return -end - -local s = NamedSection(m, arg[1], "server") -s.type_name = type_name -s.option_prefix = "singbox_" - -local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'") - -local ss_method_list = { - "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", - "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" -} - -o = s:option(Flag, "custom", translate("Use Custom Config")) - -o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") -o.rows = 10 -o.wrap = "off" -o:depends({ custom = true }) -o.datatype = "json" -local o_validate = o.validate -o.validate = function(self, value) - local v = o_validate(self, value) - if v then return v end - return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") -end -o.custom_cfgvalue = function(self, section, value) - local config_str = m:get(section, "config_str") - if config_str then - return api.base64Decode(config_str) - end -end -o.custom_write = function(self, section, value) - m:set(section, "config_str", api.base64Encode(value) or "") -end - -o = s:option(ListValue, "protocol", translate("Protocol")) -o:value("mixed", "Mixed") -o:value("socks", "Socks") -o:value("http", "HTTP") -o:value("shadowsocks", "Shadowsocks") -o:value("vmess", "Vmess") -o:value("vless", "VLESS") -o:value("trojan", "Trojan") -o:value("naive", "Naive") -if singbox_tags:find("with_quic") then - o:value("hysteria", "Hysteria") -end -if singbox_tags:find("with_quic") then - o:value("tuic", "TUIC") -end -if singbox_tags:find("with_quic") then - o:value("hysteria2", "Hysteria2") -end -o:value("anytls", "AnyTLS") -if singbox_tags:find("with_wireguard") then - o:value("wireguard", "WireGuard") -end -o:value("direct", "Direct") -o:depends({ custom = false }) - -o = s:option(DummyValue, "is_endpoint", "") -o.not_rewrite = true -o.template = m:template_path("/cbi/hidevalue") -o.value = "1" -o:depends({ custom = false, protocol = "wireguard" }) - -o = s:option(Value, "port", translate("Listen Port")) -o.datatype = "port" -o:depends({ custom = false }) - -o = s:option(DynamicList, "users", translate("User")) -for i, v in ipairs(user_list) do - o:value(v[".name"], v.username) -end -o:depends({ protocol = "mixed" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "http" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "trojan" }) -o:depends({ protocol = "naive" }) -o:depends({ protocol = "hysteria" }) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria2" }) -o:depends({ protocol = "anytls" }) -o:depends({ protocol = "wireguard" }) - -if singbox_tags:find("with_quic") then - o = s:option(Value, "hysteria_obfs", translate("Obfs Password")) - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps")) - o.default = "100" - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps")) - o.default = "100" - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window")) - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_recv_window_client", translate("QUIC connection receive window")) - o:depends({ protocol = "hysteria" }) - - o = s:option(Value, "hysteria_max_conn_client", translate("QUIC concurrent bidirectional streams")) - o.default = "1024" - o:depends({ protocol = "hysteria" }) - - o = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection")) - o:depends({ protocol = "hysteria" }) -end - -if singbox_tags:find("with_quic") then - o = s:option(ListValue, "tuic_congestion_control", translate("Congestion control algorithm")) - o.default = "cubic" - o:value("bbr", translate("BBR")) - o:value("cubic", translate("CUBIC")) - o:value("new_reno", translate("New Reno")) - o:depends({ protocol = "tuic" }) - - o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake")) - o.default = 0 - o:depends({ protocol = "tuic" }) - - o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)")) - o.datatype = "uinteger" - o.default = "3" - o:depends({ protocol = "tuic" }) - - o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN")) - o.default = "default" - o:value("default", translate("Default")) - o:value("h3") - o:value("h2") - o:value("h3,h2") - o:value("http/1.1") - o:value("h2,http/1.1") - o:value("h3,h2,http/1.1") - o:value("spdy/3.1") - o:value("h3,spdy/3.1") - o:depends({ protocol = "tuic" }) -end - -if singbox_tags:find("with_quic") then - o = s:option(Flag, "hysteria2_realms", translate("Realms")) - o.default = "0" - o:depends({ protocol = "hysteria2"}) - - o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") - o:depends({ hysteria2_realms = "1" }) - o.validate = function(self, value) - value = api.trim(value) - local realm = api.parse_realm_uri(value) - if realm then return value end - return nil, translate("Invalid Realm URL.") - end - - o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) - o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } - o:depends({ hysteria2_realms = "1" }) - - o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) - o:value("", translate("Disable")) - o:value("salamander") - o:value("gecko") - o:depends({ protocol = "hysteria2" }) - - o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) - o:depends({ hysteria2_obfs_type = "salamander" }) - o:depends({ hysteria2_obfs_type = "gecko" }) - - o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) - o.datatype = "uinteger" - o.placeholder = "512" - o.default = "512" - o:depends({ hysteria2_obfs_type = "gecko" }) - - o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) - o.datatype = "uinteger" - o.placeholder = "1200" - o.default = "1200" - o:depends({ hysteria2_obfs_type = "gecko" }) - - o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control"), translate("Commands the client to use the BBR flow control algorithm")) - o.default = 0 - o:depends({ protocol = "hysteria2" }) - - o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) - o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) - - o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) - o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) -end - -o = s:option(ListValue, "d_protocol", translate("Destination protocol")) -o:value("tcp", "TCP") -o:value("udp", "UDP") -o:value("tcp,udp", "TCP,UDP") -o:depends({ protocol = "direct" }) - -o = s:option(Value, "d_address", translate("Destination address")) -o:depends({ protocol = "direct" }) - -o = s:option(Value, "d_port", translate("Destination port")) -o.datatype = "port" -o:depends({ protocol = "direct" }) - -o = s:option(Value, "decryption", translate("Encrypt Method")) -o.default = "none" -o:depends({ protocol = "vless" }) - -o = s:option(ListValue, "ss_method", translate("Encrypt Method")) -for a, t in ipairs(ss_method_list) do o:value(t) end -o:depends({ protocol = "shadowsocks" }) - -o = s:option(Value, "ss_password", translate("Password")) -o:depends({ protocol = "shadowsocks" }) - -o = s:option(ListValue, "flow", translate("flow")) -o.default = "" -o:value("", translate("Disable")) -o:value("xtls-rprx-vision") -o:depends({ protocol = "vless" , tls = true }) - -o = s:option(Flag, "tls", translate("TLS")) -o.default = 0 -o.validate = function(self, value, t) - if value then - local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil - if reality and reality == "1" then return value end - if value == "1" then - local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or "" - local key = s.fields["tls_keyFile"] and s.fields["tls_keyFile"]:formvalue(t) or "" - if ca == "" or key == "" then - return nil, translate("Public key and Private key path can not be empty!") - end - end - return value - end -end -o:depends({ protocol = "http" }) -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "trojan" }) -o:depends({ protocol = "anytls" }) - --- https://github.com/SagerNet/sing-box/commit/d2a04c4e41e6cef0937331cb6d10211f431caaab -if singbox_tags:find("with_utls") then - -- [[ REALITY ]] -- - o = s:option(Flag, "reality", translate("REALITY")) - o.default = 0 - o:depends({ protocol = "http", tls = true }) - o:depends({ protocol = "vmess", tls = true }) - o:depends({ protocol = "vless", tls = true }) - o:depends({ protocol = "trojan", tls = true }) - o:depends({ protocol = "anytls", tls = true }) - - o = s:option(Value, "reality_private_key", translate("Private Key")) - o:depends({ reality = true }) - - o = s:option(Value, "reality_shortId", translate("Short Id")) - o:depends({ reality = true }) - - o = s:option(Value, "reality_handshake_server", translate("Handshake Server")) - o.default = "google.com" - o:depends({ reality = true }) - - o = s:option(Value, "reality_handshake_server_port", translate("Handshake Server Port")) - o.datatype = "port" - o.default = "443" - o:depends({ reality = true }) -end - -o = s:option(ListValue, "alpn", translate("ALPN")) -o.default = "default" -o:value("default", translate("Default")) -o:value("h3") -o:value("h2") -o:value("h3,h2") -o:value("http/1.1") -o:value("h2,http/1.1") -o:value("h3,h2,http/1.1") -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "hysteria" }) - --- [[ TLS ]] -- - -o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") -o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" -if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "naive" }) -o:depends({ protocol = "hysteria" }) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria2" }) -o.validate = function(self, value, t) - if value and value ~= "" then - if not api.fs.access(value) then - return nil, translate("Can't find this file!") - else - return value - end - end - return nil -end - -o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") -o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" -if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "naive" }) -o:depends({ protocol = "hysteria" }) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria2" }) -o.validate = function(self, value, t) - if value and value ~= "" then - if not api.fs.access(value) then - return nil, translate("Can't find this file!") - else - return value - end - end - return nil -end - -o = s:option(Flag, "ech", translate("ECH")) -o.default = "0" -o:depends({ tls = true, flow = "", reality = false }) -o:depends({ protocol = "naive" }) -o:depends({ protocol = "hysteria" }) -o:depends({ protocol = "tuic" }) -o:depends({ protocol = "hysteria2", hysteria2_realms = false }) - -o = s:option(TextValue, "ech_key", translate("ECH Key")) -o.default = "" -o.rows = 5 -o.wrap = "off" -o:depends({ ech = true }) -o.validate = function(self, value) - value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n") - value = value:gsub("^%s*\n", "") - if value:sub(-1) == "\n" then - value = value:sub(1, -2) - end - return value -end - -o = s:option(ListValue, "transport", translate("Transport")) -o:value("tcp", "TCP") -o:value("http", "HTTP") -o:value("ws", "WebSocket") -o:value("httpupgrade", "HTTPUpgrade") -o:value("quic", "QUIC") -o:value("grpc", "gRPC") -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "trojan" }) - --- [[ HTTP ]]-- - -o = s:option(DynamicList, "http_host", translate("HTTP Host")) -o:depends({ transport = "http" }) - -o = s:option(Value, "http_path", translate("HTTP Path")) -o:depends({ transport = "http" }) - --- [[ WebSocket ]]-- - -o = s:option(Value, "ws_host", translate("WebSocket Host")) -o:depends({ transport = "ws" }) - -o = s:option(Value, "ws_path", translate("WebSocket Path")) -o:depends({ transport = "ws" }) - --- [[ HTTPUpgrade ]]-- - -o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host")) -o:depends({ transport = "httpupgrade" }) - -o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path")) -o:depends({ transport = "httpupgrade" }) - --- [[ gRPC ]]-- -o = s:option(Value, "grpc_serviceName", "ServiceName") -o:depends({ transport = "grpc" }) - --- [[ Mux ]]-- -o = s:option(Flag, "mux", translate("Mux")) -o.rmempty = false -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless", flow = "" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "trojan" }) - --- [[ TCP Brutal ]]-- -o = s:option(Flag, "tcpbrutal", translate("TCP Brutal")) -o.default = 0 -o:depends({ mux = true }) - -o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps")) -o.default = "10" -o:depends({ tcpbrutal = true }) - -o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps")) -o.default = "50" -o:depends({ tcpbrutal = true }) - -if singbox_tags:find("with_wireguard") then - o = s:option(Flag, "wireguard_system_interface", translate("System interface")) - o.default = 0 - o:depends({ protocol = "wireguard" }) - - o = s:option(Value, "wireguard_mtu", "MTU") - o.default = "1408" - o:depends({ protocol = "wireguard" }) - - o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) - o:depends({ protocol = "wireguard" }) - - o = s:option(Value, "wireguard_private_key", translate("Private Key")) - o.datatype = "base64" - o:depends({ protocol = "wireguard" }) - - o = s:option(Value, "wireguard_public_key", translate("Public Key")) - o.datatype = "base64" - o:depends({ protocol = "wireguard" }) - - o = s:option(DummyValue, "gen_wireguard_key") - o.template = m:template_path("/server/gen_wireguard_key") - o:depends({ protocol = "wireguard" }) -end - -o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) -o.default = "0" -o:depends({ custom = false }) - -o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.nocreate = true -o.allowany = true -o.default = "wan" -o.template = "cbi/firewall_zonelist" -o:depends({ custom = false, firewall_allow = true }) - -o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) -o.default = "0" -o:depends({ custom = false }) - -local nodes_table = {} -for k, e in ipairs(api.get_valid_nodes()) do - if e.node_type == "normal" and e.type == type_name then - nodes_table[#nodes_table + 1] = { - id = e[".name"], - remarks = e["remark"], - group = e["group"] - } - end -end - -o = s:option(ListValue, "outbound_node", translate("outbound node")) -o:value("", translate("Close")) -o:value("_socks", translate("Custom Socks")) -o:value("_http", translate("Custom HTTP")) -o:value("_iface", translate("Custom Interface")) -o.template = m:template_path("/cbi/nodes_listvalue") -o.group = {"","","",""} -for k, v in pairs(nodes_table) do - o:value(v.id, v.remarks) - o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") -end -o:depends({ custom = false }) - -o = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)")) -o:depends({ outbound_node = "_socks" }) -o:depends({ outbound_node = "_http" }) - -o = s:option(Value, "outbound_node_port", translate("Port")) -o.datatype = "port" -o:depends({ outbound_node = "_socks" }) -o:depends({ outbound_node = "_http" }) - -o = s:option(Value, "outbound_node_username", translate("Username")) -o:depends({ outbound_node = "_socks" }) -o:depends({ outbound_node = "_http" }) - -o = s:option(Value, "outbound_node_password", translate("Password")) -o.password = true -o:depends({ outbound_node = "_socks" }) -o:depends({ outbound_node = "_http" }) - -o = s:option(Value, "outbound_node_iface", translate("Interface")) -o:depends({ outbound_node = "_iface" }) -local netdev_list = api.get_network_devices() -for _, d in ipairs(netdev_list) do - o:value(d.name, d.label) -end - -o = s:option(Flag, "log", translate("Log")) -o.default = "1" -o.rmempty = false - -o = s:option(ListValue, "loglevel", translate("Log Level")) -o.default = "info" -o:value("debug") -o:value("info") -o:value("warn") -o:value("error") -o:depends({ log = true }) - +local singbox_bin = api.finded_com("sing-box") + +if not singbox_bin then + return +end + +-- [[ Sing-Box ]] +local m, s1 = ... +local type_name = "sing-box" + +s1.fields["type"]:value(type_name, "Sing-Box") +if not s1.fields["type"].default then + s1.fields["type"].default = type_name +end + +if not s1.val["type"] then + s1.val["type"] = type_name +end + +if s1.val["type"] and s1.val["type"] ~= type_name then + return +end + +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "singbox_" + +local singbox_tags = luci.sys.exec(singbox_bin .. " version | grep 'Tags:' | awk '{print $2}'") + +local ss_method_list = { + "none", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "xchacha20-ietf-poly1305", + "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" +} + +o = s:option(Flag, "custom", translate("Use Custom Config")) + +o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") +o.rows = 10 +o.wrap = "off" +o:depends({ custom = true }) +o.datatype = "json" +local o_validate = o.validate +o.validate = function(self, value) + local v = o_validate(self, value) + if v then return v end + return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") +end +o.custom_cfgvalue = function(self, section, value) + local config_str = m:get(section, "config_str") + if config_str then + return api.base64Decode(config_str) + end +end +o.custom_write = function(self, section, value) + m:set(section, "config_str", api.base64Encode(value) or "") +end + +o = s:option(ListValue, "protocol", translate("Protocol")) +o:value("mixed", "Mixed") +o:value("socks", "Socks") +o:value("http", "HTTP") +o:value("shadowsocks", "Shadowsocks") +o:value("vmess", "Vmess") +o:value("vless", "VLESS") +o:value("trojan", "Trojan") +o:value("naive", "Naive") +if singbox_tags:find("with_quic") then + o:value("hysteria", "Hysteria") +end +if singbox_tags:find("with_quic") then + o:value("tuic", "TUIC") +end +if singbox_tags:find("with_quic") then + o:value("hysteria2", "Hysteria2") +end +o:value("anytls", "AnyTLS") +if singbox_tags:find("with_wireguard") then + o:value("wireguard", "WireGuard") +end +o:value("direct", "Direct") +o:depends({ custom = false }) + +o = s:option(DummyValue, "is_endpoint", "") +o.not_rewrite = true +o.template = m:template_path("/cbi/hidevalue") +o.value = "1" +o:depends({ custom = false, protocol = "wireguard" }) + +o = s:option(Value, "port", translate("Listen Port")) +o.datatype = "port" +o:depends({ custom = false }) + +o = s:option(DynamicList, "users", translate("User")) +for i, v in ipairs(user_list) do + o:value(v[".name"], v.username) +end +o:depends({ protocol = "mixed" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "naive" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "anytls" }) +o:depends({ protocol = "wireguard" }) + +if singbox_tags:find("with_quic") then + o = s:option(Value, "hysteria_obfs", translate("Obfs Password")) + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_up_mbps", translate("Max upload Mbps")) + o.default = "100" + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_down_mbps", translate("Max download Mbps")) + o.default = "100" + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_recv_window_conn", translate("QUIC stream receive window")) + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_recv_window_client", translate("QUIC connection receive window")) + o:depends({ protocol = "hysteria" }) + + o = s:option(Value, "hysteria_max_conn_client", translate("QUIC concurrent bidirectional streams")) + o.default = "1024" + o:depends({ protocol = "hysteria" }) + + o = s:option(Flag, "hysteria_disable_mtu_discovery", translate("Disable MTU detection")) + o:depends({ protocol = "hysteria" }) +end + +if singbox_tags:find("with_quic") then + o = s:option(ListValue, "tuic_congestion_control", translate("Congestion control algorithm")) + o.default = "cubic" + o:value("bbr", translate("BBR")) + o:value("cubic", translate("CUBIC")) + o:value("new_reno", translate("New Reno")) + o:depends({ protocol = "tuic" }) + + o = s:option(Flag, "tuic_zero_rtt_handshake", translate("Enable 0-RTT QUIC handshake")) + o.default = 0 + o:depends({ protocol = "tuic" }) + + o = s:option(Value, "tuic_heartbeat", translate("Heartbeat interval(second)")) + o.datatype = "uinteger" + o.default = "3" + o:depends({ protocol = "tuic" }) + + o = s:option(ListValue, "tuic_alpn", translate("QUIC TLS ALPN")) + o.default = "default" + o:value("default", translate("Default")) + o:value("h3") + o:value("h2") + o:value("h3,h2") + o:value("http/1.1") + o:value("h2,http/1.1") + o:value("h3,h2,http/1.1") + o:value("spdy/3.1") + o:value("h3,spdy/3.1") + o:depends({ protocol = "tuic" }) +end + +if singbox_tags:find("with_quic") then + o = s:option(Flag, "hysteria2_realms", translate("Realms")) + o.default = "0" + o:depends({ protocol = "hysteria2"}) + + o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") + o:depends({ hysteria2_realms = "1" }) + o.validate = function(self, value) + value = api.trim(value) + local realm = api.parse_realm_uri(value) + if realm then return value end + return nil, translate("Invalid Realm URL.") + end + + o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) + o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } + o:depends({ hysteria2_realms = "1" }) + + o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) + o:value("", translate("Disable")) + o:value("salamander") + o:value("gecko") + o:depends({ protocol = "hysteria2" }) + + o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) + o:depends({ hysteria2_obfs_type = "salamander" }) + o:depends({ hysteria2_obfs_type = "gecko" }) + + o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) + o.datatype = "uinteger" + o.placeholder = "512" + o.default = "512" + o:depends({ hysteria2_obfs_type = "gecko" }) + + o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) + o.datatype = "uinteger" + o.placeholder = "1200" + o.default = "1200" + o:depends({ hysteria2_obfs_type = "gecko" }) + + o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control"), translate("Commands the client to use the BBR flow control algorithm")) + o.default = 0 + o:depends({ protocol = "hysteria2" }) + + o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) + o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) + + o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) + o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) +end + +o = s:option(ListValue, "d_protocol", translate("Destination protocol")) +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("tcp,udp", "TCP,UDP") +o:depends({ protocol = "direct" }) + +o = s:option(Value, "d_address", translate("Destination address")) +o:depends({ protocol = "direct" }) + +o = s:option(Value, "d_port", translate("Destination port")) +o.datatype = "port" +o:depends({ protocol = "direct" }) + +o = s:option(Value, "decryption", translate("Encrypt Method")) +o.default = "none" +o:depends({ protocol = "vless" }) + +o = s:option(ListValue, "ss_method", translate("Encrypt Method")) +for a, t in ipairs(ss_method_list) do o:value(t) end +o:depends({ protocol = "shadowsocks" }) + +o = s:option(Value, "ss_password", translate("Password")) +o:depends({ protocol = "shadowsocks" }) + +o = s:option(ListValue, "flow", translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:depends({ protocol = "vless" , tls = true }) + +o = s:option(Flag, "tls", translate("TLS")) +o.default = 0 +o.validate = function(self, value, t) + if value then + local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil + if reality and reality == "1" then return value end + if value == "1" then + local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or "" + local key = s.fields["tls_keyFile"] and s.fields["tls_keyFile"]:formvalue(t) or "" + if ca == "" or key == "" then + return nil, translate("Public key and Private key path can not be empty!") + end + end + return value + end +end +o:depends({ protocol = "http" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "anytls" }) + +-- https://github.com/SagerNet/sing-box/commit/d2a04c4e41e6cef0937331cb6d10211f431caaab +if singbox_tags:find("with_utls") then + -- [[ REALITY ]] -- + o = s:option(Flag, "reality", translate("REALITY")) + o.default = 0 + o:depends({ protocol = "http", tls = true }) + o:depends({ protocol = "vmess", tls = true }) + o:depends({ protocol = "vless", tls = true }) + o:depends({ protocol = "trojan", tls = true }) + o:depends({ protocol = "anytls", tls = true }) + + o = s:option(Value, "reality_private_key", translate("Private Key")) + o:depends({ reality = true }) + + o = s:option(Value, "reality_shortId", translate("Short Id")) + o:depends({ reality = true }) + + o = s:option(Value, "reality_handshake_server", translate("Handshake Server")) + o.default = "google.com" + o:depends({ reality = true }) + + o = s:option(Value, "reality_handshake_server_port", translate("Handshake Server Port")) + o.datatype = "port" + o.default = "443" + o:depends({ reality = true }) +end + +o = s:option(ListValue, "alpn", translate("ALPN")) +o.default = "default" +o:value("default", translate("Default")) +o:value("h3") +o:value("h2") +o:value("h3,h2") +o:value("http/1.1") +o:value("h2,http/1.1") +o:value("h3,h2,http/1.1") +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria" }) + +-- [[ TLS ]] -- + +o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" +if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "naive" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) +o.validate = function(self, value, t) + if value and value ~= "" then + if not api.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end + +o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" +if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "naive" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2" }) +o.validate = function(self, value, t) + if value and value ~= "" then + if not api.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end + +o = s:option(Flag, "ech", translate("ECH")) +o.default = "0" +o:depends({ tls = true, flow = "", reality = false }) +o:depends({ protocol = "naive" }) +o:depends({ protocol = "hysteria" }) +o:depends({ protocol = "tuic" }) +o:depends({ protocol = "hysteria2", hysteria2_realms = false }) + +o = s:option(TextValue, "ech_key", translate("ECH Key")) +o.default = "" +o.rows = 5 +o.wrap = "off" +o:depends({ ech = true }) +o.validate = function(self, value) + value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n") + value = value:gsub("^%s*\n", "") + if value:sub(-1) == "\n" then + value = value:sub(1, -2) + end + return value +end + +o = s:option(ListValue, "transport", translate("Transport")) +o:value("tcp", "TCP") +o:value("http", "HTTP") +o:value("ws", "WebSocket") +o:value("httpupgrade", "HTTPUpgrade") +o:value("quic", "QUIC") +o:value("grpc", "gRPC") +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) + +-- [[ HTTP ]]-- + +o = s:option(DynamicList, "http_host", translate("HTTP Host")) +o:depends({ transport = "http" }) + +o = s:option(Value, "http_path", translate("HTTP Path")) +o:depends({ transport = "http" }) + +-- [[ WebSocket ]]-- + +o = s:option(Value, "ws_host", translate("WebSocket Host")) +o:depends({ transport = "ws" }) + +o = s:option(Value, "ws_path", translate("WebSocket Path")) +o:depends({ transport = "ws" }) + +-- [[ HTTPUpgrade ]]-- + +o = s:option(Value, "httpupgrade_host", translate("HTTPUpgrade Host")) +o:depends({ transport = "httpupgrade" }) + +o = s:option(Value, "httpupgrade_path", translate("HTTPUpgrade Path")) +o:depends({ transport = "httpupgrade" }) + +-- [[ gRPC ]]-- +o = s:option(Value, "grpc_serviceName", "ServiceName") +o:depends({ transport = "grpc" }) + +-- [[ Mux ]]-- +o = s:option(Flag, "mux", translate("Mux")) +o.rmempty = false +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless", flow = "" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) + +-- [[ TCP Brutal ]]-- +o = s:option(Flag, "tcpbrutal", translate("TCP Brutal")) +o.default = 0 +o:depends({ mux = true }) + +o = s:option(Value, "tcpbrutal_up_mbps", translate("Max upload Mbps")) +o.default = "10" +o:depends({ tcpbrutal = true }) + +o = s:option(Value, "tcpbrutal_down_mbps", translate("Max download Mbps")) +o.default = "50" +o:depends({ tcpbrutal = true }) + +if singbox_tags:find("with_wireguard") then + o = s:option(Flag, "wireguard_system_interface", translate("System interface")) + o.default = 0 + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_mtu", "MTU") + o.default = "1408" + o:depends({ protocol = "wireguard" }) + + o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_private_key", translate("Private Key")) + o.datatype = "base64" + o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_public_key", translate("Public Key")) + o.datatype = "base64" + o:depends({ protocol = "wireguard" }) + + o = s:option(DummyValue, "gen_wireguard_key") + o.template = m:template_path("/server/gen_wireguard_key") + o:depends({ protocol = "wireguard" }) +end + +o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Value, "firewall_allow_src", translate("Source zone")) +o.nocreate = true +o.allowany = true +o.default = "wan" +o.template = "cbi/firewall_zonelist" +o:depends({ custom = false, firewall_allow = true }) + +o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +o.default = "0" +o:depends({ custom = false }) + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" and e.type == type_name then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remarks = e["remark"], + group = e["group"] + } + end +end + +o = s:option(ListValue, "outbound_node", translate("outbound node")) +o:value("", translate("Close")) +o:value("_socks", translate("Custom Socks")) +o:value("_http", translate("Custom HTTP")) +o:value("_iface", translate("Custom Interface")) +o.template = m:template_path("/cbi/nodes_listvalue") +o.group = {"","","",""} +for k, v in pairs(nodes_table) do + o:value(v.id, v.remarks) + o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") +end +o:depends({ custom = false }) + +o = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)")) +o:depends({ outbound_node = "_socks" }) +o:depends({ outbound_node = "_http" }) + +o = s:option(Value, "outbound_node_port", translate("Port")) +o.datatype = "port" +o:depends({ outbound_node = "_socks" }) +o:depends({ outbound_node = "_http" }) + +o = s:option(Value, "outbound_node_username", translate("Username")) +o:depends({ outbound_node = "_socks" }) +o:depends({ outbound_node = "_http" }) + +o = s:option(Value, "outbound_node_password", translate("Password")) +o.password = true +o:depends({ outbound_node = "_socks" }) +o:depends({ outbound_node = "_http" }) + +o = s:option(Value, "outbound_node_iface", translate("Interface")) +o:depends({ outbound_node = "_iface" }) +local netdev_list = api.get_network_devices() +for _, d in ipairs(netdev_list) do + o:value(d.name, d.label) +end + +o = s:option(Flag, "log", translate("Log")) +o.default = "1" +o.rmempty = false + +o = s:option(ListValue, "loglevel", translate("Log Level")) +o.default = "info" +o:value("debug") +o:value("info") +o:value("warn") +o:value("error") +o:depends({ log = true }) + api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua index 440102ac..30f99f39 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/2_xray.lua @@ -1,552 +1,552 @@ -if not api.finded_com("xray") then - return -end - --- [[ Xray ]] -local m, s1 = ... -local type_name = "Xray" - -s1.fields["type"]:value(type_name, "Xray") -if not s1.fields["type"].default then - s1.fields["type"].default = type_name -end - -if not s1.val["type"] then - s1.val["type"] = type_name -end - -if s1.val["type"] and s1.val["type"] ~= type_name then - return -end - -local s = NamedSection(m, arg[1], "server") -s.type_name = type_name -s.option_prefix = "xray_" - -local ss_method_list = { - "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" -} - -local header_type_list = { - "none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns" -} - -o = s:option(Flag, "custom", translate("Use Custom Config")) - -o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") -o.rows = 10 -o.wrap = "off" -o:depends({ custom = true }) -o.datatype = "json" -local o_validate = o.validate -o.validate = function(self, value) - local v = o_validate(self, value) - if v then return v end - return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") -end -o.custom_cfgvalue = function(self, section, value) - local config_str = m:get(section, "config_str") - if config_str then - return api.base64Decode(config_str) - end -end -o.custom_write = function(self, section, value) - m:set(section, "config_str", api.base64Encode(value) or "") -end - -o = s:option(ListValue, "protocol", translate("Protocol")) -o:value("vmess", "Vmess") -o:value("vless", "VLESS") -o:value("http", "HTTP") -o:value("socks", "Socks") -o:value("shadowsocks", "Shadowsocks") -o:value("trojan", "Trojan") -o:value("hysteria2", "Hysteria2") -o:value("wireguard", "WireGuard") -o:value("tunnel", "Tunnel") -o:depends({ custom = false }) - -o = s:option(Value, "port", translate("Listen Port")) -o.datatype = "port" -o:depends({ custom = false }) - -o = s:option(DynamicList, "users", translate("User")) -for i, v in ipairs(user_list) do - o:value(v[".name"], v.username) -end -o:depends({ protocol = "http" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "trojan" }) -o:depends({ protocol = "hysteria2" }) -o:depends({ protocol = "wireguard" }) - -o = s:option(ListValue, "d_protocol", translate("Destination protocol")) -o:value("tcp", "TCP") -o:value("udp", "UDP") -o:value("tcp,udp", "TCP,UDP") -o:depends({ protocol = "tunnel" }) - -o = s:option(Value, "d_address", translate("Destination address")) -o:depends({ protocol = "tunnel" }) - -o = s:option(Value, "d_port", translate("Destination port")) -o.datatype = "port" -o:depends({ protocol = "tunnel" }) - -o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)") -o.default = "none" -o.placeholder = "none" -o:depends({ protocol = "vless" }) -o.validate = function(self, value) - value = api.trim(value) - return (value == "" and "none" or value) -end - -o = s:option(ListValue, "ss_method", translate("Encrypt Method")) -for a, t in ipairs(ss_method_list) do o:value(t) end -o:depends({ protocol = "shadowsocks" }) - -o = s:option(Value, "ss_password", translate("Password")) -o:depends({ protocol = "shadowsocks" }) - -o = s:option(ListValue, "ss_network", translate("Transport")) -o.default = "tcp,udp" -o:value("tcp", "TCP") -o:value("udp", "UDP") -o:value("tcp,udp", "TCP,UDP") -o:depends({ protocol = "shadowsocks" }) - -o = s:option(Flag, "udp_forward", translate("UDP Forward")) -o.default = "1" -o.rmempty = false -o:depends({ protocol = "socks" }) - -o = s:option(ListValue, "flow", translate("flow")) -o.default = "" -o:value("", translate("Disable")) -o:value("xtls-rprx-vision") -o:depends({ protocol = "vless" }) - ----- [[ hysteria2 ]] -o = s:option(Flag, "hysteria2_realms", translate("Realms")) -o.default = "0" -o:depends({ protocol = "hysteria2"}) - -o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") -o:depends({ hysteria2_realms = "1" }) -o.validate = function(self, value) - value = api.trim(value) - local realm = api.parse_realm_uri(value) - if realm then return value end - return nil, translate("Invalid Realm URL.") -end - -o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) -o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } -o:depends({ hysteria2_realms = "1" }) - -o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) -o:value("", translate("Disable")) -o:value("salamander") -o:value("gecko") -o:depends({ protocol = "hysteria2" }) - -o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) -o:depends({ hysteria2_obfs_type = "salamander" }) -o:depends({ hysteria2_obfs_type = "gecko" }) - -o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) -o.datatype = "uinteger" -o.placeholder = "512" -o.default = "512" -o:depends({ hysteria2_obfs_type = "gecko" }) - -o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) -o.datatype = "uinteger" -o.placeholder = "1200" -o.default = "1200" -o:depends({ hysteria2_obfs_type = "gecko" }) - -o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control")) -o.default = 0 -o:depends({ protocol = "hysteria2" }) - -o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) -o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) - -o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) -o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) - ----- [[ TLS ]] -o = s:option(Flag, "tls", "TLS") -o.default = 0 -o.validate = function(self, value, t) - if value then - local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil - if reality and reality == "1" then return value end - if value == "1" then - local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or "" - local key = s.fields["tls_keyFile"] and s.fields["tls_keyFile"]:formvalue(t) or "" - if ca == "" or key == "" then - return nil, translate("Public key and Private key path can not be empty!") - end - end - return value - end -end -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "http" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "trojan" }) - --- [[ REALITY ]] -- -o = s:option(Flag, "reality", translate("REALITY")) -o.default = 0 -o:depends({ tls = true }) - -o = s:option(Value, "reality_private_key", translate("Private Key")) -o:depends({ reality = true }) - -o = s:option(DynamicList, "reality_shortId", translate("Short Id")) -o:depends({ reality = true }) - -o = s:option(Value, "reality_dest", translate("Dest")) -o.default = "google.com:443" -o:depends({ reality = true }) - -o = s:option(DynamicList, "reality_serverNames", translate("serverNames")) -o:depends({ reality = true }) - -o = s:option(ListValue, "alpn", translate("alpn")) -o.default = "default" -o:value("default", translate("Default")) -o:value("h3") -o:value("h2") -o:value("h3,h2") -o:value("http/1.1") -o:value("h2,http/1.1") -o:value("h3,h2,http/1.1") -o:depends({ tls = true, reality = false }) - -o = s:option(Flag, "use_mldsa65Seed", translate("ML-DSA-65")) -o.default = "0" -o:depends({ reality = true }) - -o = s:option(TextValue, "reality_mldsa65Seed", "ML-DSA-65 " .. translate("Private Key")) -o.default = "" -o.rows = 5 -o.wrap = "soft" -o:depends({ use_mldsa65Seed = true }) -o.validate = function(self, value) - return api.trim(value:gsub("[\r\n]", "")) -end - --- o = s:option(Value, "minversion", translate("minversion")) --- o.default = "1.3" --- o:value("1.3") ---o:depends({ tls = true }) - --- [[ TLS ]] -- -o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") -o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" -if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "hysteria2"}) -o.validate = function(self, value, t) - if value and value ~= "" then - if not api.fs.access(value) then - return nil, translate("Can't find this file!") - else - return value - end - end - return nil -end - -o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") -o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" -if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end -o:depends({ tls = true, reality = false }) -o:depends({ protocol = "hysteria2"}) -o.validate = function(self, value, t) - if value and value ~= "" then - if not api.fs.access(value) then - return nil, translate("Can't find this file!") - else - return value - end - end - return nil -end - -o = s:option(Flag, "ech", translate("ECH")) -o.default = "0" -o:depends({ tls = true, reality = false }) - -o = s:option(TextValue, "ech_key", translate("ECH Key")) -o.default = "" -o.rows = 5 -o.wrap = "soft" -o:depends({ ech = true }) -o.validate = function(self, value) - return api.trim(value:gsub("[\r\n]", "")) -end - -o = s:option(ListValue, "transport", translate("Transport")) -o:value("raw", "RAW") -o:value("mkcp", "mKCP") -o:value("ws", "WebSocket") -o:value("grpc", "gRPC") -o:value("httpupgrade", "HttpUpgrade") -o:value("xhttp", "XHTTP") -o:depends({ protocol = "vmess" }) -o:depends({ protocol = "vless" }) -o:depends({ protocol = "socks" }) -o:depends({ protocol = "shadowsocks" }) -o:depends({ protocol = "trojan" }) - --- [[ WebSocket ]]-- -o = s:option(Value, "ws_host", translate("WebSocket Host")) -o:depends({ transport = "ws" }) - -o = s:option(Value, "ws_path", translate("WebSocket Path")) -o:depends({ transport = "ws" }) - --- [[ HttpUpgrade ]]-- -o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host")) -o:depends({ transport = "httpupgrade" }) - -o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path")) -o.placeholder = "/" -o:depends({ transport = "httpupgrade" }) - --- [[ XHTTP ]]-- -o = s:option(Value, "xhttp_host", translate("XHTTP Host")) -o:depends({ transport = "xhttp" }) - -o = s:option(Value, "xhttp_path", translate("XHTTP Path")) -o.placeholder = "/" -o:depends({ transport = "xhttp" }) - -o = s:option(Value, "xhttp_maxuploadsize", translate("maxUploadSize")) -o.default = "1000000" -o:depends({ transport = "xhttp" }) - -o = s:option(Value, "xhttp_maxconcurrentuploads", translate("maxConcurrentUploads")) -o.default = "10" -o:depends({ transport = "xhttp" }) - -o = s:option(Value, "splithttp_maxuploadsize", translate("maxUploadSize")) -o.default = "1000000" -o:depends({ transport = "splithttp" }) - -o = s:option(Value, "splithttp_maxconcurrentuploads", translate("maxConcurrentUploads")) -o.default = "10" -o:depends({ transport = "splithttp" }) - --- [[ TCP ]]-- - -o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) -o:value("none", "none") -o:value("http", "http") -o:depends({ transport = "raw" }) - -o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) -o:depends({ tcp_guise = "http" }) - -o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) -o:depends({ tcp_guise = "http" }) - --- [[ mKCP ]]-- - -o = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.')) -for a, t in ipairs(header_type_list) do o:value(t) end -o:depends({ transport = "mkcp" }) - -o = s:option(Value, "mkcp_domain", translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) -o:depends({ mkcp_guise = "dns" }) - -o = s:option(Value, "mkcp_mtu", translate("KCP MTU")) -o.datatype = "uinteger" -o.default = 1350 -o:depends({ transport = "mkcp" }) - -o = s:option(Value, "mkcp_seed", translate("KCP Seed")) -o:depends({ transport = "mkcp" }) - --- [[ gRPC ]]-- -o = s:option(Value, "grpc_serviceName", "ServiceName") -o:depends({ transport = "grpc" }) - ---[[FinalMask]] -o = s:option(Flag, "use_finalmask", "FinalMask") -o.default = "0" -o:depends({ custom = false, protocol = "vmess" }) -o:depends({ custom = false, protocol = "vless" }) -o:depends({ custom = false, protocol = "trojan" }) -o:depends({ custom = false, protocol = "shadowsocks" }) -o:depends({ custom = false, protocol = "hysteria2", hysteria2_realms = false }) - -o = s:option(TextValue, "finalmask", "FinalMask JSON") -o:depends({ use_finalmask = true }) -o.rows = 10 -o.wrap = "off" -o.datatype = "json" -o.custom_cfgvalue = function(self, section, value) - local raw = m:get(section, "finalmask") - if raw then - return api.base64Decode(raw) - end -end -o.custom_write = function(self, section, value) - m:set(section, "finalmask", api.base64Encode(value) or "") -end - ---[[acceptProxyProtocol]] -o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) -o.default = "0" -o:depends({ transport = "raw" }) -o:depends({ transport = "ws" }) - ---[[Fast Open]] -o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) -o.default = "0" -o:depends({ protocol = "vmess", custom = false }) -o:depends({ protocol = "vless", custom = false }) -o:depends({ protocol = "shadowsocks", custom = false }) -o:depends({ protocol = "trojan", custom = false }) - --- [[ Fallback ]]-- -o = s:option(Flag, "fallback", translate("Fallback")) -o:depends({ protocol = "vless", transport = "raw" }) -o:depends({ protocol = "trojan", transport = "raw" }) - ---[[ -o = s:option(Value, "fallback_alpn", "Fallback alpn") -o:depends({ fallback = true }) - -o = s:option(Value, "fallback_path", "Fallback path") -o:depends({ fallback = true }) - -o = s:option(Value, "fallback_dest", "Fallback dest") -o:depends({ fallback = true }) - -o = s:option(Value, "fallback_xver", "Fallback xver") -o.default = 0 -o:depends({ fallback = true }) -]]-- - -o = s:option(DynamicList, "fallback_list", "Fallback", translate("format: dest,path,xver")) -o:depends({ fallback = true }) - --- Not supported at present ---[[ -o = s:option(Flag, "wireguard_system_interface", translate("System interface")) -o.default = 0 -o:depends({ protocol = "wireguard" }) -]]-- - -o = s:option(Value, "wireguard_mtu", "MTU") -o.default = "1420" -o:depends({ protocol = "wireguard" }) - --- Not supported at present ---[[ -o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) -o:depends({ protocol = "wireguard" }) -]]-- - -o = s:option(Value, "wireguard_private_key", translate("Private Key")) -o.datatype = "base64" -o:depends({ protocol = "wireguard" }) - -o = s:option(Value, "wireguard_public_key", translate("Public Key")) -o.datatype = "base64" -o:depends({ protocol = "wireguard" }) - -o = s:option(DummyValue, "gen_wireguard_key") -o.template = m:template_path("/server/gen_wireguard_key") -o:depends({ protocol = "wireguard" }) - -o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) -o.default = "0" -o:depends({ custom = false }) - -o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.nocreate = true -o.allowany = true -o.default = "wan" -o.template = "cbi/firewall_zonelist" -o:depends({ custom = false, firewall_allow = true }) - -o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) -o.default = "0" -o:depends({ custom = false }) - -local nodes_table = {} -for k, e in ipairs(api.get_valid_nodes()) do - if e.node_type == "normal" and e.type == type_name then - nodes_table[#nodes_table + 1] = { - id = e[".name"], - remarks = e["remark"], - group = e["group"] - } - end -end - -o = s:option(ListValue, "outbound_node", translate("outbound node")) -o:value("", translate("Close")) -o:value("_socks", translate("Custom Socks")) -o:value("_http", translate("Custom HTTP")) -o:value("_iface", translate("Custom Interface")) -o.template = m:template_path("/cbi/nodes_listvalue") -o.group = {"","","",""} -for k, v in pairs(nodes_table) do - o:value(v.id, v.remarks) - o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") -end -o:depends({ custom = false }) - -o = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)")) -o:depends({ outbound_node = "_socks"}) -o:depends({ outbound_node = "_http"}) - -o = s:option(Value, "outbound_node_port", translate("Port")) -o.datatype = "port" -o:depends({ outbound_node = "_socks"}) -o:depends({ outbound_node = "_http"}) - -o = s:option(Value, "outbound_node_username", translate("Username")) -o:depends({ outbound_node = "_socks"}) -o:depends({ outbound_node = "_http"}) - -o = s:option(Value, "outbound_node_password", translate("Password")) -o.password = true -o:depends({ outbound_node = "_socks"}) -o:depends({ outbound_node = "_http"}) - -o = s:option(Value, "outbound_node_iface", translate("Interface")) -o:depends({ outbound_node = "_iface"}) -local netdev_list = api.get_network_devices() -for _, d in ipairs(netdev_list) do - o:value(d.name, d.label) -end - -o = s:option(Flag, "log", translate("Log")) -o.default = "1" -o.rmempty = false - -o = s:option(ListValue, "loglevel", translate("Log Level")) -o.default = "warning" -o:value("debug") -o:value("info") -o:value("warning") -o:value("error") -o:depends({ log = true }) - +if not api.finded_com("xray") then + return +end + +-- [[ Xray ]] +local m, s1 = ... +local type_name = "Xray" + +s1.fields["type"]:value(type_name, "Xray") +if not s1.fields["type"].default then + s1.fields["type"].default = type_name +end + +if not s1.val["type"] then + s1.val["type"] = type_name +end + +if s1.val["type"] and s1.val["type"] ~= type_name then + return +end + +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "xray_" + +local ss_method_list = { + "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", "xchacha20-poly1305", "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha20-poly1305" +} + +local header_type_list = { + "none", "srtp", "utp", "wechat-video", "dtls", "wireguard", "dns" +} + +o = s:option(Flag, "custom", translate("Use Custom Config")) + +o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") +o.rows = 10 +o.wrap = "off" +o:depends({ custom = true }) +o.datatype = "json" +local o_validate = o.validate +o.validate = function(self, value) + local v = o_validate(self, value) + if v then return v end + return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") +end +o.custom_cfgvalue = function(self, section, value) + local config_str = m:get(section, "config_str") + if config_str then + return api.base64Decode(config_str) + end +end +o.custom_write = function(self, section, value) + m:set(section, "config_str", api.base64Encode(value) or "") +end + +o = s:option(ListValue, "protocol", translate("Protocol")) +o:value("vmess", "Vmess") +o:value("vless", "VLESS") +o:value("http", "HTTP") +o:value("socks", "Socks") +o:value("shadowsocks", "Shadowsocks") +o:value("trojan", "Trojan") +o:value("hysteria2", "Hysteria2") +o:value("wireguard", "WireGuard") +o:value("tunnel", "Tunnel") +o:depends({ custom = false }) + +o = s:option(Value, "port", translate("Listen Port")) +o.datatype = "port" +o:depends({ custom = false }) + +o = s:option(DynamicList, "users", translate("User")) +for i, v in ipairs(user_list) do + o:value(v[".name"], v.username) +end +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "trojan" }) +o:depends({ protocol = "hysteria2" }) +o:depends({ protocol = "wireguard" }) + +o = s:option(ListValue, "d_protocol", translate("Destination protocol")) +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("tcp,udp", "TCP,UDP") +o:depends({ protocol = "tunnel" }) + +o = s:option(Value, "d_address", translate("Destination address")) +o:depends({ protocol = "tunnel" }) + +o = s:option(Value, "d_port", translate("Destination port")) +o.datatype = "port" +o:depends({ protocol = "tunnel" }) + +o = s:option(Value, "decryption", translate("Encrypt Method") .. " (decryption)") +o.default = "none" +o.placeholder = "none" +o:depends({ protocol = "vless" }) +o.validate = function(self, value) + value = api.trim(value) + return (value == "" and "none" or value) +end + +o = s:option(ListValue, "ss_method", translate("Encrypt Method")) +for a, t in ipairs(ss_method_list) do o:value(t) end +o:depends({ protocol = "shadowsocks" }) + +o = s:option(Value, "ss_password", translate("Password")) +o:depends({ protocol = "shadowsocks" }) + +o = s:option(ListValue, "ss_network", translate("Transport")) +o.default = "tcp,udp" +o:value("tcp", "TCP") +o:value("udp", "UDP") +o:value("tcp,udp", "TCP,UDP") +o:depends({ protocol = "shadowsocks" }) + +o = s:option(Flag, "udp_forward", translate("UDP Forward")) +o.default = "1" +o.rmempty = false +o:depends({ protocol = "socks" }) + +o = s:option(ListValue, "flow", translate("flow")) +o.default = "" +o:value("", translate("Disable")) +o:value("xtls-rprx-vision") +o:depends({ protocol = "vless" }) + +---- [[ hysteria2 ]] +o = s:option(Flag, "hysteria2_realms", translate("Realms")) +o.default = "0" +o:depends({ protocol = "hysteria2"}) + +o = s:option(Value, "hysteria2_realm_url", translate("Realm URL"), translate("Example:") .. "realm://public@realm.hy2.io/your-realm-name") +o:depends({ hysteria2_realms = "1" }) +o.validate = function(self, value) + value = api.trim(value) + local realm = api.parse_realm_uri(value) + if realm then return value end + return nil, translate("Invalid Realm URL.") +end + +o = s:option(DynamicList, "hysteria2_realm_stun", translate("Realm STUN")) +o.default = { "stun.sip.us:3478", "stun.nextcloud.com:3478", "global.stun.twilio.com:3478" } +o:depends({ hysteria2_realms = "1" }) + +o = s:option(ListValue, "hysteria2_obfs_type", translate("Obfs Type")) +o:value("", translate("Disable")) +o:value("salamander") +o:value("gecko") +o:depends({ protocol = "hysteria2" }) + +o = s:option(Value, "hysteria2_obfs_password", translate("Obfs Password")) +o:depends({ hysteria2_obfs_type = "salamander" }) +o:depends({ hysteria2_obfs_type = "gecko" }) + +o = s:option(Value, "hysteria2_obfs_MinPacketSize", translate("Gecko Packet Size (min)")) +o.datatype = "uinteger" +o.placeholder = "512" +o.default = "512" +o:depends({ hysteria2_obfs_type = "gecko" }) + +o = s:option(Value, "hysteria2_obfs_MaxPacketSize", translate("Gecko Packet Size (max)")) +o.datatype = "uinteger" +o.placeholder = "1200" +o.default = "1200" +o:depends({ hysteria2_obfs_type = "gecko" }) + +o = s:option(Flag, "hysteria2_ignore_client_bandwidth", translate("Client BBR Flow Control")) +o.default = 0 +o:depends({ protocol = "hysteria2" }) + +o = s:option(Value, "hysteria2_up_mbps", translate("Max upload Mbps")) +o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) + +o = s:option(Value, "hysteria2_down_mbps", translate("Max download Mbps")) +o:depends({ protocol = "hysteria2", hysteria2_ignore_client_bandwidth = false }) + +---- [[ TLS ]] +o = s:option(Flag, "tls", "TLS") +o.default = 0 +o.validate = function(self, value, t) + if value then + local reality = s.fields["reality"] and s.fields["reality"]:formvalue(t) or nil + if reality and reality == "1" then return value end + if value == "1" then + local ca = s.fields["tls_certificateFile"] and s.fields["tls_certificateFile"]:formvalue(t) or "" + local key = s.fields["tls_keyFile"] and s.fields["tls_keyFile"]:formvalue(t) or "" + if ca == "" or key == "" then + return nil, translate("Public key and Private key path can not be empty!") + end + end + return value + end +end +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "http" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) + +-- [[ REALITY ]] -- +o = s:option(Flag, "reality", translate("REALITY")) +o.default = 0 +o:depends({ tls = true }) + +o = s:option(Value, "reality_private_key", translate("Private Key")) +o:depends({ reality = true }) + +o = s:option(DynamicList, "reality_shortId", translate("Short Id")) +o:depends({ reality = true }) + +o = s:option(Value, "reality_dest", translate("Dest")) +o.default = "google.com:443" +o:depends({ reality = true }) + +o = s:option(DynamicList, "reality_serverNames", translate("serverNames")) +o:depends({ reality = true }) + +o = s:option(ListValue, "alpn", translate("alpn")) +o.default = "default" +o:value("default", translate("Default")) +o:value("h3") +o:value("h2") +o:value("h3,h2") +o:value("http/1.1") +o:value("h2,http/1.1") +o:value("h3,h2,http/1.1") +o:depends({ tls = true, reality = false }) + +o = s:option(Flag, "use_mldsa65Seed", translate("ML-DSA-65")) +o.default = "0" +o:depends({ reality = true }) + +o = s:option(TextValue, "reality_mldsa65Seed", "ML-DSA-65 " .. translate("Private Key")) +o.default = "" +o.rows = 5 +o.wrap = "soft" +o:depends({ use_mldsa65Seed = true }) +o.validate = function(self, value) + return api.trim(value:gsub("[\r\n]", "")) +end + +-- o = s:option(Value, "minversion", translate("minversion")) +-- o.default = "1.3" +-- o:value("1.3") +--o:depends({ tls = true }) + +-- [[ TLS ]] -- +o = s:option(FileUpload, "tls_certificateFile", translate("Public key absolute path"), translate("as:") .. "/etc/ssl/fullchain.pem") +o.default = m:get(s.section, "tls_certificateFile") or "/etc/config/ssl/" .. arg[1] .. ".pem" +if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2"}) +o.validate = function(self, value, t) + if value and value ~= "" then + if not api.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end + +o = s:option(FileUpload, "tls_keyFile", translate("Private key absolute path"), translate("as:") .. "/etc/ssl/private.key") +o.default = m:get(s.section, "tls_keyFile") or "/etc/config/ssl/" .. arg[1] .. ".key" +if o and o:formvalue(arg[1]) then o.default = o:formvalue(arg[1]) end +o:depends({ tls = true, reality = false }) +o:depends({ protocol = "hysteria2"}) +o.validate = function(self, value, t) + if value and value ~= "" then + if not api.fs.access(value) then + return nil, translate("Can't find this file!") + else + return value + end + end + return nil +end + +o = s:option(Flag, "ech", translate("ECH")) +o.default = "0" +o:depends({ tls = true, reality = false }) + +o = s:option(TextValue, "ech_key", translate("ECH Key")) +o.default = "" +o.rows = 5 +o.wrap = "soft" +o:depends({ ech = true }) +o.validate = function(self, value) + return api.trim(value:gsub("[\r\n]", "")) +end + +o = s:option(ListValue, "transport", translate("Transport")) +o:value("raw", "RAW") +o:value("mkcp", "mKCP") +o:value("ws", "WebSocket") +o:value("grpc", "gRPC") +o:value("httpupgrade", "HttpUpgrade") +o:value("xhttp", "XHTTP") +o:depends({ protocol = "vmess" }) +o:depends({ protocol = "vless" }) +o:depends({ protocol = "socks" }) +o:depends({ protocol = "shadowsocks" }) +o:depends({ protocol = "trojan" }) + +-- [[ WebSocket ]]-- +o = s:option(Value, "ws_host", translate("WebSocket Host")) +o:depends({ transport = "ws" }) + +o = s:option(Value, "ws_path", translate("WebSocket Path")) +o:depends({ transport = "ws" }) + +-- [[ HttpUpgrade ]]-- +o = s:option(Value, "httpupgrade_host", translate("HttpUpgrade Host")) +o:depends({ transport = "httpupgrade" }) + +o = s:option(Value, "httpupgrade_path", translate("HttpUpgrade Path")) +o.placeholder = "/" +o:depends({ transport = "httpupgrade" }) + +-- [[ XHTTP ]]-- +o = s:option(Value, "xhttp_host", translate("XHTTP Host")) +o:depends({ transport = "xhttp" }) + +o = s:option(Value, "xhttp_path", translate("XHTTP Path")) +o.placeholder = "/" +o:depends({ transport = "xhttp" }) + +o = s:option(Value, "xhttp_maxuploadsize", translate("maxUploadSize")) +o.default = "1000000" +o:depends({ transport = "xhttp" }) + +o = s:option(Value, "xhttp_maxconcurrentuploads", translate("maxConcurrentUploads")) +o.default = "10" +o:depends({ transport = "xhttp" }) + +o = s:option(Value, "splithttp_maxuploadsize", translate("maxUploadSize")) +o.default = "1000000" +o:depends({ transport = "splithttp" }) + +o = s:option(Value, "splithttp_maxconcurrentuploads", translate("maxConcurrentUploads")) +o.default = "10" +o:depends({ transport = "splithttp" }) + +-- [[ TCP ]]-- + +o = s:option(ListValue, "tcp_guise", translate("Camouflage Type")) +o:value("none", "none") +o:value("http", "http") +o:depends({ transport = "raw" }) + +o = s:option(DynamicList, "tcp_guise_http_host", translate("HTTP Host")) +o:depends({ tcp_guise = "http" }) + +o = s:option(DynamicList, "tcp_guise_http_path", translate("HTTP Path")) +o:depends({ tcp_guise = "http" }) + +-- [[ mKCP ]]-- + +o = s:option(ListValue, "mkcp_guise", translate("Camouflage Type"), translate('
none: default, no masquerade, data sent is packets with no characteristics.
srtp: disguised as an SRTP packet, it will be recognized as video call data (such as FaceTime).
utp: packets disguised as uTP will be recognized as bittorrent downloaded data.
wechat-video: packets disguised as WeChat video calls.
dtls: disguised as DTLS 1.2 packet.
wireguard: disguised as a WireGuard packet. (not really WireGuard protocol)
dns: Disguising traffic as DNS requests.')) +for a, t in ipairs(header_type_list) do o:value(t) end +o:depends({ transport = "mkcp" }) + +o = s:option(Value, "mkcp_domain", translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain.")) +o:depends({ mkcp_guise = "dns" }) + +o = s:option(Value, "mkcp_mtu", translate("KCP MTU")) +o.datatype = "uinteger" +o.default = 1350 +o:depends({ transport = "mkcp" }) + +o = s:option(Value, "mkcp_seed", translate("KCP Seed")) +o:depends({ transport = "mkcp" }) + +-- [[ gRPC ]]-- +o = s:option(Value, "grpc_serviceName", "ServiceName") +o:depends({ transport = "grpc" }) + +--[[FinalMask]] +o = s:option(Flag, "use_finalmask", "FinalMask") +o.default = "0" +o:depends({ custom = false, protocol = "vmess" }) +o:depends({ custom = false, protocol = "vless" }) +o:depends({ custom = false, protocol = "trojan" }) +o:depends({ custom = false, protocol = "shadowsocks" }) +o:depends({ custom = false, protocol = "hysteria2", hysteria2_realms = false }) + +o = s:option(TextValue, "finalmask", "FinalMask JSON") +o:depends({ use_finalmask = true }) +o.rows = 10 +o.wrap = "off" +o.datatype = "json" +o.custom_cfgvalue = function(self, section, value) + local raw = m:get(section, "finalmask") + if raw then + return api.base64Decode(raw) + end +end +o.custom_write = function(self, section, value) + m:set(section, "finalmask", api.base64Encode(value) or "") +end + +--[[acceptProxyProtocol]] +o = s:option(Flag, "acceptProxyProtocol", translate("acceptProxyProtocol"), translate("Whether to receive PROXY protocol, when this node want to be fallback or forwarded by proxy, it must be enable, otherwise it cannot be used.")) +o.default = "0" +o:depends({ transport = "raw" }) +o:depends({ transport = "ws" }) + +--[[Fast Open]] +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) +o.default = "0" +o:depends({ protocol = "vmess", custom = false }) +o:depends({ protocol = "vless", custom = false }) +o:depends({ protocol = "shadowsocks", custom = false }) +o:depends({ protocol = "trojan", custom = false }) + +-- [[ Fallback ]]-- +o = s:option(Flag, "fallback", translate("Fallback")) +o:depends({ protocol = "vless", transport = "raw" }) +o:depends({ protocol = "trojan", transport = "raw" }) + +--[[ +o = s:option(Value, "fallback_alpn", "Fallback alpn") +o:depends({ fallback = true }) + +o = s:option(Value, "fallback_path", "Fallback path") +o:depends({ fallback = true }) + +o = s:option(Value, "fallback_dest", "Fallback dest") +o:depends({ fallback = true }) + +o = s:option(Value, "fallback_xver", "Fallback xver") +o.default = 0 +o:depends({ fallback = true }) +]]-- + +o = s:option(DynamicList, "fallback_list", "Fallback", translate("format: dest,path,xver")) +o:depends({ fallback = true }) + +-- Not supported at present +--[[ +o = s:option(Flag, "wireguard_system_interface", translate("System interface")) +o.default = 0 +o:depends({ protocol = "wireguard" }) +]]-- + +o = s:option(Value, "wireguard_mtu", "MTU") +o.default = "1420" +o:depends({ protocol = "wireguard" }) + +-- Not supported at present +--[[ +o = s:option(DynamicList, "wireguard_local_address", translate("Local Address")) +o:depends({ protocol = "wireguard" }) +]]-- + +o = s:option(Value, "wireguard_private_key", translate("Private Key")) +o.datatype = "base64" +o:depends({ protocol = "wireguard" }) + +o = s:option(Value, "wireguard_public_key", translate("Public Key")) +o.datatype = "base64" +o:depends({ protocol = "wireguard" }) + +o = s:option(DummyValue, "gen_wireguard_key") +o.template = m:template_path("/server/gen_wireguard_key") +o:depends({ protocol = "wireguard" }) + +o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Value, "firewall_allow_src", translate("Source zone")) +o.nocreate = true +o.allowany = true +o.default = "wan" +o.template = "cbi/firewall_zonelist" +o:depends({ custom = false, firewall_allow = true }) + +o = s:option(Flag, "accept_lan", translate("Accept LAN Access"), translate("When selected, it can accessed lan , this will not be safe!")) +o.default = "0" +o:depends({ custom = false }) + +local nodes_table = {} +for k, e in ipairs(api.get_valid_nodes()) do + if e.node_type == "normal" and e.type == type_name then + nodes_table[#nodes_table + 1] = { + id = e[".name"], + remarks = e["remark"], + group = e["group"] + } + end +end + +o = s:option(ListValue, "outbound_node", translate("outbound node")) +o:value("", translate("Close")) +o:value("_socks", translate("Custom Socks")) +o:value("_http", translate("Custom HTTP")) +o:value("_iface", translate("Custom Interface")) +o.template = m:template_path("/cbi/nodes_listvalue") +o.group = {"","","",""} +for k, v in pairs(nodes_table) do + o:value(v.id, v.remarks) + o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default") +end +o:depends({ custom = false }) + +o = s:option(Value, "outbound_node_address", translate("Address (Support Domain Name)")) +o:depends({ outbound_node = "_socks"}) +o:depends({ outbound_node = "_http"}) + +o = s:option(Value, "outbound_node_port", translate("Port")) +o.datatype = "port" +o:depends({ outbound_node = "_socks"}) +o:depends({ outbound_node = "_http"}) + +o = s:option(Value, "outbound_node_username", translate("Username")) +o:depends({ outbound_node = "_socks"}) +o:depends({ outbound_node = "_http"}) + +o = s:option(Value, "outbound_node_password", translate("Password")) +o.password = true +o:depends({ outbound_node = "_socks"}) +o:depends({ outbound_node = "_http"}) + +o = s:option(Value, "outbound_node_iface", translate("Interface")) +o:depends({ outbound_node = "_iface"}) +local netdev_list = api.get_network_devices() +for _, d in ipairs(netdev_list) do + o:value(d.name, d.label) +end + +o = s:option(Flag, "log", translate("Log")) +o.default = "1" +o.rmempty = false + +o = s:option(ListValue, "loglevel", translate("Log Level")) +o.default = "warning" +o:value("debug") +o:value("info") +o:value("warning") +o:value("error") +o:depends({ log = true }) + api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/3_ss-rust.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/3_ss-rust.lua index df31354e..82d6acac 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/3_ss-rust.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/3_ss-rust.lua @@ -1,90 +1,90 @@ -if not api.is_finded("ssserver") then - return -end - --- [[ Shadowsocks Rust ]] -local m, s1 = ... -local type_name = "SS-Rust" - -s1.fields["type"]:value(type_name, translate("Shadowsocks Rust")) - -if not s1.val["type"] then - s1.val["type"] = type_name -end - -if s1.val["type"] and s1.val["type"] ~= type_name then - return -end - -local s = NamedSection(m, arg[1], "server") -s.type_name = type_name -s.option_prefix = "ssrust_" - -local ssrust_encrypt_method_list = { - "plain", "none", - "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", - "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305" -} - -o = s:option(Flag, "custom", translate("Use Custom Config")) - -o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") -o.rows = 10 -o.wrap = "off" -o:depends({ custom = true }) -o.datatype = "json" -local o_validate = o.validate -o.validate = function(self, value) - local v = o_validate(self, value) - if v then return v end - return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") -end -o.custom_cfgvalue = function(self, section, value) - local config_str = m:get(section, "config_str") - if config_str then - return api.base64Decode(config_str) - end -end -o.custom_write = function(self, section, value) - m:set(section, "config_str", api.base64Encode(value) or "") -end - -o = s:option(Value, "port", translate("Listen Port")) -o.datatype = "port" -o:depends({ custom = false }) - -o = s:option(ListValue, "user", translate("User")) -for i, v in ipairs(user_list) do - o:value(v[".name"], v.username) -end -o:depends({ custom = false }) - -o = s:option(ListValue, "method", translate("Encrypt Method")) -for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end -o:depends({ custom = false }) - -o = s:option(Value, "timeout", translate("Connection Timeout")) -o.datatype = "uinteger" -o.default = 300 -o:depends({ custom = false }) - -o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) -o.default = "0" -o:depends({ custom = false }) - -o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) -o.default = "0" -o:depends({ custom = false }) - -o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.nocreate = true -o.allowany = true -o.default = "wan" -o.template = "cbi/firewall_zonelist" -o:depends({ custom = false, firewall_allow = true }) - -o = s:option(Flag, "log", translate("Log")) -o.default = "1" -o.rmempty = false - +if not api.is_finded("ssserver") then + return +end + +-- [[ Shadowsocks Rust ]] +local m, s1 = ... +local type_name = "SS-Rust" + +s1.fields["type"]:value(type_name, translate("Shadowsocks Rust")) + +if not s1.val["type"] then + s1.val["type"] = type_name +end + +if s1.val["type"] and s1.val["type"] ~= type_name then + return +end + +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "ssrust_" + +local ssrust_encrypt_method_list = { + "plain", "none", + "aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", + "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm", "2022-blake3-chacha8-poly1305", "2022-blake3-chacha20-poly1305" +} + +o = s:option(Flag, "custom", translate("Use Custom Config")) + +o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") +o.rows = 10 +o.wrap = "off" +o:depends({ custom = true }) +o.datatype = "json" +local o_validate = o.validate +o.validate = function(self, value) + local v = o_validate(self, value) + if v then return v end + return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") +end +o.custom_cfgvalue = function(self, section, value) + local config_str = m:get(section, "config_str") + if config_str then + return api.base64Decode(config_str) + end +end +o.custom_write = function(self, section, value) + m:set(section, "config_str", api.base64Encode(value) or "") +end + +o = s:option(Value, "port", translate("Listen Port")) +o.datatype = "port" +o:depends({ custom = false }) + +o = s:option(ListValue, "user", translate("User")) +for i, v in ipairs(user_list) do + o:value(v[".name"], v.username) +end +o:depends({ custom = false }) + +o = s:option(ListValue, "method", translate("Encrypt Method")) +for a, t in ipairs(ssrust_encrypt_method_list) do o:value(t) end +o:depends({ custom = false }) + +o = s:option(Value, "timeout", translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 +o:depends({ custom = false }) + +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Value, "firewall_allow_src", translate("Source zone")) +o.nocreate = true +o.allowany = true +o.default = "wan" +o.template = "cbi/firewall_zonelist" +o:depends({ custom = false, firewall_allow = true }) + +o = s:option(Flag, "log", translate("Log")) +o.default = "1" +o.rmempty = false + api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/4_ssr.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/4_ssr.lua index 3e74f960..12da8994 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/4_ssr.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/server/type/4_ssr.lua @@ -1,121 +1,121 @@ -if not api.is_finded("ssr-server") then - return -end - --- [[ ShadowsocksR ]] -local m, s1 = ... -local type_name = "SSR" - -s1.fields["type"]:value(type_name, translate("ShadowsocksR")) - -if not s1.val["type"] then - s1.val["type"] = type_name -end - -if s1.val["type"] and s1.val["type"] ~= type_name then - return -end - -local s = NamedSection(m, arg[1], "server") -s.type_name = type_name -s.option_prefix = "ssr_" - -local ssr_encrypt_method_list = { - "none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb", - "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", - "bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", - "cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20", - "chacha20-ietf" -} - -local ssr_protocol_list = { - "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", - "auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5", - "auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c", - "auth_chain_d", "auth_chain_e", "auth_chain_f" -} -local ssr_obfs_list = { - "plain", "http_simple", "http_post", "random_head", "tls_simple", - "tls1.0_session_auth", "tls1.2_ticket_auth" -} - -o = s:option(Flag, "custom", translate("Use Custom Config")) - -o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") -o.rows = 10 -o.wrap = "off" -o:depends({ custom = true }) -o.datatype = "json" -local o_validate = o.validate -o.validate = function(self, value) - local v = o_validate(self, value) - if v then return v end - return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") -end -o.custom_cfgvalue = function(self, section, value) - local config_str = m:get(section, "config_str") - if config_str then - return api.base64Decode(config_str) - end -end -o.custom_write = function(self, section, value) - m:set(section, "config_str", api.base64Encode(value) or "") -end - -o = s:option(Value, "port", translate("Listen Port")) -o.datatype = "port" -o:depends({ custom = false }) - -o = s:option(ListValue, "user", translate("User")) -for i, v in ipairs(user_list) do - o:value(v[".name"], v.username) -end -o:depends({ custom = false }) - -o = s:option(ListValue, "method", translate("Encrypt Method")) -for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end -o:depends({ custom = false }) - -o = s:option(ListValue, "protocol", translate("Protocol")) -for a, t in ipairs(ssr_protocol_list) do o:value(t) end -o:depends({ custom = false }) - -o = s:option(Value, "protocol_param", translate("Protocol_param")) -o:depends({ custom = false }) - -o = s:option(ListValue, "obfs", translate("Obfs")) -for a, t in ipairs(ssr_obfs_list) do o:value(t) end -o:depends({ custom = false }) - -o = s:option(Value, "obfs_param", translate("Obfs_param")) -o:depends({ custom = false }) - -o = s:option(Value, "timeout", translate("Connection Timeout")) -o.datatype = "uinteger" -o.default = 300 -o:depends({ custom = false }) - -o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) -o.default = "0" -o:depends({ custom = false }) - -o = s:option(Flag, "udp_forward", translate("UDP Forward")) -o.default = "1" -o.rmempty = false - -o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) -o.default = "0" -o:depends({ custom = false }) - -o = s:option(Value, "firewall_allow_src", translate("Source zone")) -o.nocreate = true -o.allowany = true -o.default = "wan" -o.template = "cbi/firewall_zonelist" -o:depends({ custom = false, firewall_allow = true }) - -o = s:option(Flag, "log", translate("Log")) -o.default = "1" -o.rmempty = false - +if not api.is_finded("ssr-server") then + return +end + +-- [[ ShadowsocksR ]] +local m, s1 = ... +local type_name = "SSR" + +s1.fields["type"]:value(type_name, translate("ShadowsocksR")) + +if not s1.val["type"] then + s1.val["type"] = type_name +end + +if s1.val["type"] and s1.val["type"] ~= type_name then + return +end + +local s = NamedSection(m, arg[1], "server") +s.type_name = type_name +s.option_prefix = "ssr_" + +local ssr_encrypt_method_list = { + "none", "table", "rc2-cfb", "rc4", "rc4-md5", "rc4-md5-6", "aes-128-cfb", + "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", + "bf-cfb", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb", + "cast5-cfb", "des-cfb", "idea-cfb", "seed-cfb", "salsa20", "chacha20", + "chacha20-ietf" +} + +local ssr_protocol_list = { + "origin", "verify_simple", "verify_deflate", "verify_sha1", "auth_simple", + "auth_sha1", "auth_sha1_v2", "auth_sha1_v4", "auth_aes128_md5", + "auth_aes128_sha1", "auth_chain_a", "auth_chain_b", "auth_chain_c", + "auth_chain_d", "auth_chain_e", "auth_chain_f" +} +local ssr_obfs_list = { + "plain", "http_simple", "http_post", "random_head", "tls_simple", + "tls1.0_session_auth", "tls1.2_ticket_auth" +} + +o = s:option(Flag, "custom", translate("Use Custom Config")) + +o = s:option(TextValue, "custom_config", translate("Custom Config") .. " (JSON)") +o.rows = 10 +o.wrap = "off" +o:depends({ custom = true }) +o.datatype = "json" +local o_validate = o.validate +o.validate = function(self, value) + local v = o_validate(self, value) + if v then return v end + return nil, translate("Custom Config") .. " " .. translate("Must be JSON text!") +end +o.custom_cfgvalue = function(self, section, value) + local config_str = m:get(section, "config_str") + if config_str then + return api.base64Decode(config_str) + end +end +o.custom_write = function(self, section, value) + m:set(section, "config_str", api.base64Encode(value) or "") +end + +o = s:option(Value, "port", translate("Listen Port")) +o.datatype = "port" +o:depends({ custom = false }) + +o = s:option(ListValue, "user", translate("User")) +for i, v in ipairs(user_list) do + o:value(v[".name"], v.username) +end +o:depends({ custom = false }) + +o = s:option(ListValue, "method", translate("Encrypt Method")) +for a, t in ipairs(ssr_encrypt_method_list) do o:value(t) end +o:depends({ custom = false }) + +o = s:option(ListValue, "protocol", translate("Protocol")) +for a, t in ipairs(ssr_protocol_list) do o:value(t) end +o:depends({ custom = false }) + +o = s:option(Value, "protocol_param", translate("Protocol_param")) +o:depends({ custom = false }) + +o = s:option(ListValue, "obfs", translate("Obfs")) +for a, t in ipairs(ssr_obfs_list) do o:value(t) end +o:depends({ custom = false }) + +o = s:option(Value, "obfs_param", translate("Obfs_param")) +o:depends({ custom = false }) + +o = s:option(Value, "timeout", translate("Connection Timeout")) +o.datatype = "uinteger" +o.default = 300 +o:depends({ custom = false }) + +o = s:option(Flag, "tcp_fast_open", "TCP " .. translate("Fast Open")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Flag, "udp_forward", translate("UDP Forward")) +o.default = "1" +o.rmempty = false + +o = s:option(Flag, "firewall_allow", translate("Firewall Allow")) +o.default = "0" +o:depends({ custom = false }) + +o = s:option(Value, "firewall_allow_src", translate("Source zone")) +o.nocreate = true +o.allowany = true +o.default = "wan" +o.template = "cbi/firewall_zonelist" +o:depends({ custom = false, firewall_allow = true }) + +o = s:option(Flag, "log", translate("Log")) +o.default = "1" +o.rmempty = false + api.luci_types(s1, s) \ No newline at end of file diff --git a/luci-app-passwall2/luasrc/passwall2/api.lua b/luci-app-passwall2/luasrc/passwall2/api.lua index 491d402f..0bc1ab4d 100644 --- a/luci-app-passwall2/luasrc/passwall2/api.lua +++ b/luci-app-passwall2/luasrc/passwall2/api.lua @@ -464,14 +464,14 @@ datatypes.json = is_json function is_timehhmm(str) local hour, minute = string.match(str, "^(%d?%d):(%d%d)$") - if hour and minute then - hour = tonumber(hour) - minute = tonumber(minute) - if hour >= 0 and hour <= 23 and minute >= 0 and minute <= 59 then - return true - end - end - return false + if hour and minute then + hour = tonumber(hour) + minute = tonumber(minute) + if hour >= 0 and hour <= 23 and minute >= 0 and minute <= 59 then + return true + end + end + return false end datatypes.timehhmm = is_timehhmm diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index a5312e68..eb65f6da 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -922,12 +922,12 @@ function gen_config(var) if xray_settings.fragment == "1" then local lengths, delays = {}, {} api.trim(xray_settings.fragment_lengths):gsub("[^,]+", function(w) - w = w:gsub("%s+", "") - if w ~= "" then lengths[#lengths+1] = w end + w = w:gsub("%s+", "") + if w ~= "" then lengths[#lengths+1] = w end end) api.trim(xray_settings.fragment_delays):gsub("[^,]+", function(w) - w = w:gsub("%s+", "") - if w ~= "" then delays[#delays+1] = w end + w = w:gsub("%s+", "") + if w ~= "" then delays[#delays+1] = w end end) fragment_table = { type = "fragment", diff --git a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value_com.htm b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value_com.htm index d470ea4b..3aa8f39f 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value_com.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/cbi/nodes_value_com.htm @@ -534,7 +534,7 @@ //自定义框 function v_customEnter(cbid, labelSpan, hiddenInput, searchInput, panel, listContainer, customInput) { let inputValue = customInput.value.trim(); - if (!inputValue) { + if (!inputValue) { return; } const existingItems = listContainer.querySelectorAll('li[data-key="' + inputValue + '"]'); diff --git a/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm b/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm index db4d1e31..60044c8a 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/global/faq.htm @@ -3,28 +3,28 @@ local map = self.map local api = map.api -%>