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
-%>
- <%:DNS related issues:%>
+ <%:DNS related issues:%>
1. <%:Certain browsers such as Chrome have built-in DNS service, which may affect DNS resolution settings. You can go to 'Settings -> Privacy and security -> Use secure DNS' menu to turn it off.%>
2. <%:If you are unable to access the internet after reboot, please try clearing the cache of your terminal devices (make sure to close all open browser application windows first, this step is especially important):%>
-
◦ <%:For Windows systems, open Command Prompt and run the command 'ipconfig /flushdns'.%>
-
◦ <%:For Mac systems, open Terminal and run the command 'sudo killall -HUP mDNSResponder'.%>
-
◦ <%:For mobile devices, you can clear it by reconnecting to the network, such as toggling Airplane Mode and reconnecting to WiFi.%>
-
+
◦ <%:For Windows systems, open Command Prompt and run the command 'ipconfig /flushdns'.%>
+
◦ <%:For Mac systems, open Terminal and run the command 'sudo killall -HUP mDNSResponder'.%>
+
◦ <%:For mobile devices, you can clear it by reconnecting to the network, such as toggling Airplane Mode and reconnecting to WiFi.%>
+
3. <%:Please make sure your device's network settings point both the DNS server and default gateway to this router, to ensure DNS queries are properly routed.%>
@@ -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
-%>