diff --git a/luci-app-passwall/luasrc/controller/passwall.lua b/luci-app-passwall/luasrc/controller/passwall.lua index 269867b9..93e16e25 100644 --- a/luci-app-passwall/luasrc/controller/passwall.lua +++ b/luci-app-passwall/luasrc/controller/passwall.lua @@ -381,35 +381,36 @@ function connect_status() local e = {} e.use_time = "" local url = http.formvalue("url") + if not url or url == "" then + http_write_json(e) + return + end local aliyun = string.find(url, "aliyun") local chn_list = uci_get("@global[0]", "chn_list") or "direct" local gfw_list = uci_get("@global[0]", "use_gfw_list") or "1" local proxy_mode = uci_get("@global[0]", "tcp_proxy_mode") or "proxy" local localhost_proxy = uci_get("@global[0]", "localhost_proxy") or "1" local socks_server = (localhost_proxy == "0") and api.get_cache_var("GLOBAL_SOCKS_server") or "" - url = "-w %{http_code}:%{time_pretransfer} " .. url + local curl_args = "-w '%{http_code}:%{time_pretransfer}:%{time_starttransfer}' " .. util.shellquote(url) if socks_server and socks_server ~= "" then if (chn_list == "proxy" and gfw_list == "0" and proxy_mode ~= "proxy" and aliyun ~= nil) or (chn_list == "0" and gfw_list == "0" and proxy_mode == "proxy") then -- 中国列表+阿里 or 全局 - url = "-x socks5h://" .. socks_server .. " " .. url + curl_args = "-x " .. util.shellquote("socks5h://" .. socks_server) .. " " .. curl_args elseif aliyun == nil then -- 其他代理模式+阿里以外网站 - url = "-x socks5h://" .. socks_server .. " " .. url + curl_args = "-x " .. util.shellquote("socks5h://" .. socks_server) .. " " .. curl_args end end - local result = luci.sys.exec('/usr/bin/curl --connect-timeout 3 --max-time 5 -o /dev/null -I -sk ' .. url) - local code = tonumber(luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $1}'") or "0") - if code ~= 0 then - local use_time_str = luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $2}'") - local use_time = tonumber(use_time_str) - if use_time then - if use_time_str:find("%.") then - e.use_time = string.format("%.2f", use_time * 1000) - else - e.use_time = string.format("%.2f", use_time / 1000) - end - e.ping_type = "curl" - end + local result = luci.sys.exec('curl_result=$(/usr/bin/curl --connect-timeout 3 --max-time 5 -o /dev/null -I -sk ' .. curl_args .. '); [ $? -eq 0 ] && printf "%s" "$curl_result"') + local code, pretransfer, starttransfer = result:match("^%s*(%d+):([%d%.]+):([%d%.]+)%s*$") + code, pretransfer, starttransfer = tonumber(code), tonumber(pretransfer), tonumber(starttransfer) + if code and code > 0 then e.http_code = code end + -- A completed HTTP response, including 4xx/5xx, confirms homepage connectivity. + if code and code >= 200 and code < 600 and pretransfer and pretransfer >= 0 and starttransfer and starttransfer > 0 and starttransfer >= pretransfer then + -- HTTP request latency excludes connection and TLS handshakes. + e.use_time = string.format("%.2f", (starttransfer - pretransfer) * 1000) + e.handshake_time = string.format("%.2f", pretransfer * 1000) + e.ping_type = "curl" end http_write_json(e) end @@ -438,17 +439,13 @@ function urltest_node() local e = {} e.index = index local result = luci.sys.exec("/usr/share/passwall/test.sh url_test_node " .. util.shellquote(id or "")) - local code = tonumber(luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $1}'") or "0") - if code ~= 0 then - local use_time_str = luci.sys.exec("echo -n '" .. result .. "' | awk -F ':' '{print $2}'") - local use_time = tonumber(use_time_str) - if use_time then - if use_time_str:find("%.") then - e.use_time = string.format("%.2f", use_time * 1000) - else - e.use_time = string.format("%.2f", use_time / 1000) - end - end + local code, pretransfer, starttransfer = result:match("^%s*(%d+):([%d%.]+):([%d%.]+)%s*$") + code, pretransfer, starttransfer = tonumber(code), tonumber(pretransfer), tonumber(starttransfer) + if code and code > 0 then e.http_code = code end + if code and code >= 200 and code < 600 and pretransfer and pretransfer >= 0 and starttransfer and starttransfer > 0 and starttransfer >= pretransfer then + -- Keep node test parsing independent from the homepage test. + e.use_time = string.format("%.2f", (starttransfer - pretransfer) * 1000) + e.handshake_time = string.format("%.2f", pretransfer * 1000) end http_write_json(e) end diff --git a/luci-app-passwall/luasrc/passwall/api.lua b/luci-app-passwall/luasrc/passwall/api.lua index 40c21703..827856af 100644 --- a/luci-app-passwall/luasrc/passwall/api.lua +++ b/luci-app-passwall/luasrc/passwall/api.lua @@ -169,7 +169,7 @@ function del_cache_var(key) end function set_cache_var(key, val) - sys.call(string.format('. /usr/share/passwall/utils.sh ; set_cache_var "%s" "%s"', key, val)) + sys.call(string.format('. /usr/share/passwall/utils.sh ; set_cache_var %s %s', util.shellquote(tostring(key)), util.shellquote(tostring(val)))) end function get_cache_var(key) diff --git a/luci-app-passwall/luasrc/view/passwall/global/status.htm b/luci-app-passwall/luasrc/view/passwall/global/status.htm index ff6e018a..5f79de13 100644 --- a/luci-app-passwall/luasrc/view/passwall/global/status.htm +++ b/luci-app-passwall/luasrc/view/passwall/global/status.htm @@ -139,7 +139,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md -
+
@@ -151,7 +151,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
-
+
@@ -163,7 +163,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
-
+
@@ -175,7 +175,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md
-
+
@@ -246,6 +246,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md if (s) { var div = s.parentNode.parentNode.parentNode.parentNode; div.removeAttribute('onclick'); + div.title = "<%:HTTP request latency, for reference only%>"; s.innerHTML = '<%:Check...%>'; var sendDate = (new Date()).getTime(); XHR.get('<%=api.url("connect_status")%>', { @@ -263,6 +264,7 @@ https://github.com/pure-css/pure/blob/master/LICENSE.md s.className="red"; } s.innerHTML = use_time + " ms"; + div.title += "\n<%:Initial handshake time%>: " + rv.handshake_time + " ms"; } else if (rv.status) { s.className="green"; s.innerHTML = "<%:Working...%>"; 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 f2f692b5..c69a075b 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 @@ -710,6 +710,7 @@ table td, .table .td { function urltest_node(cbi_id, dom) { if (dom.dataset.busy == "1" || !cbi_id) return; dom.dataset.busy = "1"; + dom.title = "<%:HTTP request latency, for reference only%>"; dom.style.color = ""; dom.innerText = "<%:Check...%>"; XHR.get('<%=api.url("urltest_node")%>', { @@ -720,7 +721,7 @@ table td, .table .td { let text = "<%:Error%>"; if (x && x.status == 200) { if (result.use_time == null || result.use_time.trim() == "") { - text = "<%:Timeout%>"; + text = result.http_code >= 400 ? "HTTP " + result.http_code : "<%:Timeout%>"; } else { let use_time = parseInt(result.use_time); if (use_time < 1000) @@ -728,6 +729,7 @@ table td, .table .td { else if (use_time < 2000) color = "#fb9a05"; text = use_time + " ms"; + dom.title += "\n<%:Initial handshake time%>: " + result.handshake_time + " ms"; } } dom.innerText = text; @@ -1417,7 +1419,7 @@ table td, .table .td { innerHTML = innerHTML.split("{{tcping}}").join('---'); } if (o["address"] && _port) { - innerHTML = innerHTML.split("{{url_test}}").join('<%:Test%>'); + innerHTML = innerHTML.split("{{url_test}}").join('<%:Test%>'); } else { innerHTML = innerHTML.split("{{url_test}}").join('---'); } diff --git a/luci-app-passwall/po/zh-cn/passwall.po b/luci-app-passwall/po/zh-cn/passwall.po index 67df60dd..3b285363 100644 --- a/luci-app-passwall/po/zh-cn/passwall.po +++ b/luci-app-passwall/po/zh-cn/passwall.po @@ -22,8 +22,11 @@ msgstr "连接失败" msgid "Touch Check" msgstr "点我检测" -msgid "TLS handshake test, latency for reference only" -msgstr "TLS握手测试,延时仅供参考" +msgid "HTTP request latency, for reference only" +msgstr "HTTP 请求延迟,仅供参考" + +msgid "Initial handshake time" +msgstr "首次握手耗时" msgid "Kernel Unsupported" msgstr "内核不支持" diff --git a/luci-app-passwall/root/usr/share/passwall/haproxy.lua b/luci-app-passwall/root/usr/share/passwall/haproxy.lua index a2ce23b6..8ac9c3f3 100644 --- a/luci-app-passwall/root/usr/share/passwall/haproxy.lua +++ b/luci-app-passwall/root/usr/share/passwall/haproxy.lua @@ -21,7 +21,7 @@ local haproxy_dns = "127.0.0.1" local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1" local health_check_type = api.uci_get_c("@global_haproxy[0]", "health_check_type") or "tcp" -local health_check_inter = api.uci_get_c("@global_haproxy[0]", "health_check_inter") or "20" +local health_check_inter = api.uci_get_c("@global_haproxy[0]", "health_check_inter") or "60" local balancingStrategy = api.uci_get_c("@global_haproxy[0]", "balancingStrategy") or "roundrobin" local console_port = api.uci_get_c("@global_haproxy[0]", "console_port") or "1188" local bind_local = api.uci_get_c("@global_haproxy[0]", "bind_local") or "0" @@ -44,7 +44,7 @@ global nbthread {{nbthread}} external-check insecure-fork-wanted - spread-checks 50 + spread-checks 20 tune.max-checks-per-thread 5 defaults @@ -99,7 +99,7 @@ api.uci_foreach_c("haproxy_config", function(t) local lbss = t.lbss local listen_port = tonumber(t.haproxy_port) or 0 local server_node = api.uci_get_c(lbss) - local hop = (health_check_type == "script_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil + local hop = (health_check_type == "script_logic") and server_node and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil hop = hop and hop:gsub(":", "-") or nil if server_node and server_node.address and (server_node.port or hop) then server_remark = server_node.address .. ":" .. (server_node.port or hop) @@ -153,9 +153,12 @@ api.uci_foreach_c("haproxy_config", function(t) end end) -local inter = tonumber((health_check_inter or "20"):match("^%d+")) -inter = (inter and inter >= 20) and inter or 20 -local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s" +local inter = tonumber(health_check_inter:match("^%d+")) or 60 +inter = math.max(inter, 20) +-- 限制过渡状态的确认间隔,避免正常检测间隔较长时摘除或恢复过慢。 +local fastinter = math.max(10, math.min(15, math.floor(inter / 2))) +local downinter = math.ceil(inter * 1.5) +local inter_arg = string.format("inter %ds fastinter %ds downinter %ds", inter, fastinter, downinter) local sortTable = {} for i in pairs(listens) do @@ -165,7 +168,28 @@ for i in pairs(listens) do end table.sort(sortTable, function(a,b) return (a < b) end) -for i, port in pairs(sortTable) do +local function server_endpoint(o) + local address = o.server_address:gsub("^%[(.*)%]$", "%1") + if address:find(":", 1, true) then address = "[" .. address .. "]" end + return address .. ":" .. o.server_port +end + +local check_counts = {} +if health_check_type == "script_logic" then + for _, port in ipairs(sortTable) do + for _, o in ipairs(listens[port]) do + -- 域名可能解析到不同 IP,仅对相同的实际代理端点共享检测。 + if api.is_ip(o.server_address) then + local endpoint = server_endpoint(o) + check_counts[endpoint] = (check_counts[endpoint] or 0) + 1 + end + end + end +end +local shared_checks = {} +local shared_check_names = {} + +for i, port in ipairs(sortTable) do log(" + 入口 %s:%s" % {bind_address, port}) f_out:write("\n" .. string.format([[ @@ -184,7 +208,7 @@ listen %s local count_M, count_B = 1, 1 for i, o in ipairs(listens[port]) do - local remark = o.server_remark or "" + local remark = (o.server_remark or ""):gsub("[%[%]]", "") -- 防止重名导致无法运行 if tostring(o.backup) ~= "1" then remark = "M" .. count_M .. "-" .. remark @@ -193,8 +217,18 @@ listen %s remark = "B" .. count_B .. "-" .. remark count_B = count_B + 1 end - local server = o.server_address .. ":" .. o.server_port - local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}" + local server = server_endpoint(o) + local check_arg = "check " .. inter_arg .. " rise 2 fall 2" + if (check_counts[server] or 0) > 1 then + local name = shared_check_names[server] + if not name then + name = "check_" .. (#shared_checks + 1) + shared_check_names[server] = name + table.insert(shared_checks, {name = name, server = server}) + end + check_arg = "track " .. appname .. "_health_checks/" .. name + end + local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} {{check_arg}} {{backup}}" server_conf = server_conf:gsub("{{remark}}", remark) server_conf = server_conf:gsub("{{server}}", server) server_conf = server_conf:gsub("{{weight}}", o.lbweight) @@ -203,7 +237,7 @@ listen %s resolvers = "" end server_conf = server_conf:gsub("{{resolvers}}", resolvers) - server_conf = server_conf:gsub("{{inter_arg}}", inter_arg) + server_conf = server_conf:gsub("{{check_arg}}", check_arg) server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "") f_out:write(" " .. api.trim(server_conf) .. "\n") @@ -216,6 +250,20 @@ listen %s end end +-- 独立检测 backend,避免入口服务器的手动维护状态传播到其他入口。 +if #shared_checks > 0 then + f_out:write(string.format([[ + +backend %s_health_checks + mode tcp + option external-check + external-check command "/usr/share/%s/haproxy_check.sh" +]], appname, appname)) + for _, check in ipairs(shared_checks) do + f_out:write(string.format("\tserver %s %s check %s rise 2 fall 2\n", check.name, check.server, inter_arg)) + end +end + --控制台配置 local console_user = api.uci_get_c("@global_haproxy[0]", "console_user") local console_password = api.uci_get_c("@global_haproxy[0]", "console_password") @@ -234,8 +282,7 @@ f_out:close() --passwall内置健康检查URL if health_check_type == "script_logic" then - local probeUrl = api.uci_get_c("@global_haproxy[0]", "health_probe_url") or "https://www.google.com/generate_204" - local f_url = io.open(haproxy_path .. "/Probe_URL", "w") - f_url:write(probeUrl) - f_url:close() + local probeUrl = api.trim(api.uci_get_c("@global_haproxy[0]", "health_probe_url") or "") + if probeUrl == "" then probeUrl = "https://www.google.com/generate_204" end + api.set_cache_var("HAPROXY_PROBE_URL", probeUrl) end diff --git a/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh b/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh index 3b5700f3..4fc3e78a 100755 --- a/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh +++ b/luci-app-passwall/root/usr/share/passwall/haproxy_check.sh @@ -1,7 +1,7 @@ #!/bin/sh export PATH=/usr/sbin:/usr/bin:/sbin:/bin:/root/bin -CONFIG=passwall +. /usr/share/passwall/utils.sh listen_address=$1 listen_port=$2 @@ -13,19 +13,16 @@ busybox pgrep -af "${CONFIG}/" | grep -E 'app\.sh.*(start|stop)|nftables\.sh|ipt exit 0 } -probe_file="/tmp/etc/passwall/haproxy/Probe_URL" -probeUrl="https://www.google.com/generate_204" -if [ -f "$probe_file" ]; then - firstLine=$(head -n 1 "$probe_file" | tr -d ' \t\n') - [ -n "$firstLine" ] && probeUrl="$firstLine" -fi +probeUrl=$(get_cache_var "HAPROXY_PROBE_URL") +probeUrl="${probeUrl:-https://www.google.com/generate_204}" -extra_params="-x socks5h://${server_address}:${server_port}" -if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then - extra_params="${extra_params} --retry-all-errors" -fi +case "$server_address" in + \[*\]) ;; + *:*) server_address="[$server_address]" ;; +esac -status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}") +# 每轮只探测一次,连续成功/失败由 HAProxy 的 rise/fall 判定。 +status=$(/usr/bin/curl -q -I -o /dev/null -sk --noproxy "" -x "socks5h://${server_address}:${server_port}" --connect-timeout 3 --max-time 6 -w "%{http_code}" "$probeUrl") || exit 1 case "$status" in 200|204) diff --git a/luci-app-passwall/root/usr/share/passwall/test.sh b/luci-app-passwall/root/usr/share/passwall/test.sh index 656c700b..141bcc05 100755 --- a/luci-app-passwall/root/usr/share/passwall/test.sh +++ b/luci-app-passwall/root/usr/share/passwall/test.sh @@ -73,7 +73,7 @@ url_test_node() { fi sleep 2s probeUrl=$(config_n_get @global_other[0] url_test_url https://www.google.com/generate_204) - result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -skL -w "%{http_code}:%{time_pretransfer}" "$@" -x "$curlx" "${probeUrl}") + result=$(curl --connect-timeout 3 --max-time 5 -o /dev/null -I -sk -w "%{http_code}:%{time_pretransfer}:%{time_starttransfer}" "$@" -x "$curlx" "${probeUrl}") || result=0 # 结束 SS 插件进程 if [ "$_type" != "socks" ]; then pid_file="${TMP_PATH}/${test_flag}_plugin.pid"