This commit is contained in:
2026-07-11 01:51:21 -03:00
parent 4e3c99650e
commit 6f7fa2fad1
7 changed files with 60 additions and 18 deletions
+2
View File
@@ -376,6 +376,7 @@ async function loadManagedServerConfig(id) {
document.getElementById("managedCfgLimitUp").value = c.default_limit_mbps_up || 0; document.getElementById("managedCfgLimitUp").value = c.default_limit_mbps_up || 0;
document.getElementById("managedCfgLimitDown").value = c.default_limit_mbps_down || 0; document.getElementById("managedCfgLimitDown").value = c.default_limit_mbps_down || 0;
document.getElementById("managedCfgSSHIdleTimeout").value = c.ssh_idle_timeout || "0s";
document.getElementById("managedCfgQuiet").checked = !!c.quiet; document.getElementById("managedCfgQuiet").checked = !!c.quiet;
document.getElementById("managedCfgUserCount").checked = !!c.user_count; document.getElementById("managedCfgUserCount").checked = !!c.user_count;
document.getElementById("managedCfgBanner").value = c.banner || ""; document.getElementById("managedCfgBanner").value = c.banner || "";
@@ -444,6 +445,7 @@ function managedConfigFromForm() {
admin_dir: "/opt/sshpanel/admin", admin_dir: "/opt/sshpanel/admin",
default_limit_mbps_up: parseInt(document.getElementById("managedCfgLimitUp").value || "0", 10), default_limit_mbps_up: parseInt(document.getElementById("managedCfgLimitUp").value || "0", 10),
default_limit_mbps_down: parseInt(document.getElementById("managedCfgLimitDown").value || "0", 10), default_limit_mbps_down: parseInt(document.getElementById("managedCfgLimitDown").value || "0", 10),
ssh_idle_timeout: document.getElementById("managedCfgSSHIdleTimeout").value.trim() || "0s",
quiet: document.getElementById("managedCfgQuiet").checked, quiet: document.getElementById("managedCfgQuiet").checked,
user_count: document.getElementById("managedCfgUserCount").checked, user_count: document.getElementById("managedCfgUserCount").checked,
banner: document.getElementById("managedCfgBanner").value, banner: document.getElementById("managedCfgBanner").value,
+2
View File
@@ -96,6 +96,7 @@ async function loadServerConfig() {
document.getElementById("cfgLimitUp").value = c.default_limit_mbps_up || 0; document.getElementById("cfgLimitUp").value = c.default_limit_mbps_up || 0;
document.getElementById("cfgLimitDown").value = c.default_limit_mbps_down || 0; document.getElementById("cfgLimitDown").value = c.default_limit_mbps_down || 0;
document.getElementById("cfgMaxTotalConns").value = c.max_total_connections || 0; document.getElementById("cfgMaxTotalConns").value = c.max_total_connections || 0;
document.getElementById("cfgSSHIdleTimeout").value = c.ssh_idle_timeout || "0s";
document.getElementById("cfgQuiet").checked = !!c.quiet; document.getElementById("cfgQuiet").checked = !!c.quiet;
document.getElementById("cfgUserCount").checked = !!c.user_count; document.getElementById("cfgUserCount").checked = !!c.user_count;
@@ -178,6 +179,7 @@ async function saveServerConfig() {
default_limit_mbps_up: parseInt(document.getElementById("cfgLimitUp").value || "0", 10), default_limit_mbps_up: parseInt(document.getElementById("cfgLimitUp").value || "0", 10),
default_limit_mbps_down: parseInt(document.getElementById("cfgLimitDown").value || "0", 10), default_limit_mbps_down: parseInt(document.getElementById("cfgLimitDown").value || "0", 10),
max_total_connections: parseInt(document.getElementById("cfgMaxTotalConns").value || "0", 10), max_total_connections: parseInt(document.getElementById("cfgMaxTotalConns").value || "0", 10),
ssh_idle_timeout: document.getElementById("cfgSSHIdleTimeout").value.trim() || "0s",
quiet: document.getElementById("cfgQuiet").checked, quiet: document.getElementById("cfgQuiet").checked,
user_count: document.getElementById("cfgUserCount").checked, user_count: document.getElementById("cfgUserCount").checked,
banner: document.getElementById("cfgBanner").value, banner: document.getElementById("cfgBanner").value,
+6 -1
View File
@@ -424,7 +424,7 @@
<div class="field" id="wzWSPathField" style="display:none;"><label>Path</label><input type="text" id="wzWSPath" placeholder="/ws"/></div> <div class="field" id="wzWSPathField" style="display:none;"><label>Path</label><input type="text" id="wzWSPath" placeholder="/ws"/></div>
<!-- XHTTP --> <!-- XHTTP -->
<div class="field" id="wzXHTTPPathField" style="display:none;"><label>Path</label><input type="text" id="wzXHTTPPath" placeholder="/xhttp" value="/xhttp"/></div> <div class="field" id="wzXHTTPPathField" style="display:none;"><label>Path</label><input type="text" id="wzXHTTPPath" placeholder="/xhttp" value="/xhttp"/></div>
<div class="field" id="wzXHTTPHostField" style="display:none;"><label>Host <span class="hint">(SNI)</span></label><input type="text" id="wzXHTTPHost" placeholder="example.com"/></div> <div class="field" id="wzXHTTPHostField" style="display:none;"><label>HTTP Host <span class="hint">(separate from TLS SNI)</span></label><input type="text" id="wzXHTTPHost" placeholder="example.com"/></div>
<div class="field" id="wzXHTTPModeField" style="display:none;"> <div class="field" id="wzXHTTPModeField" style="display:none;">
<label>Mode</label> <label>Mode</label>
<select id="wzXHTTPMode"> <select id="wzXHTTPMode">
@@ -670,6 +670,7 @@
<div class="form-grid"> <div class="form-grid">
<div class="field"><label>Default Upload Limit (Mbps)</label><input type="number" id="managedCfgLimitUp" min="0" placeholder="0"/></div> <div class="field"><label>Default Upload Limit (Mbps)</label><input type="number" id="managedCfgLimitUp" min="0" placeholder="0"/></div>
<div class="field"><label>Default Download Limit (Mbps)</label><input type="number" id="managedCfgLimitDown" min="0" placeholder="0"/></div> <div class="field"><label>Default Download Limit (Mbps)</label><input type="number" id="managedCfgLimitDown" min="0" placeholder="0"/></div>
<div class="field"><label>SSH Idle Timeout <span class="hint">0s/off = disabled</span></label><input type="text" id="managedCfgSSHIdleTimeout" placeholder="0s" title="Keep disabled for VPN/XHTTP connections."/></div>
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgQuiet"/> Quiet Logs</label> <label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgQuiet"/> Quiet Logs</label>
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgUserCount"/> User Count Display</label> <label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"><input type="checkbox" id="managedCfgUserCount"/> User Count Display</label>
</div> </div>
@@ -981,6 +982,10 @@
<label>Max Total SSH Connections <span class="hint">0 = default (10000) · -1 = unlimited</span></label> <label>Max Total SSH Connections <span class="hint">0 = default (10000) · -1 = unlimited</span></label>
<input type="number" id="cfgMaxTotalConns" min="-1" placeholder="10000" title="Global cap across all users. New connections past this are rejected before the SSH handshake, so a surge cannot exhaust memory/CPU. Raise it if the server has resources for more; set -1 to remove the cap entirely."/> <input type="number" id="cfgMaxTotalConns" min="-1" placeholder="10000" title="Global cap across all users. New connections past this are rejected before the SSH handshake, so a surge cannot exhaust memory/CPU. Raise it if the server has resources for more; set -1 to remove the cap entirely."/>
</div> </div>
<div class="field">
<label>SSH Idle Timeout <span class="hint">0s/off = disabled</span></label>
<input type="text" id="cfgSSHIdleTimeout" placeholder="0s" title="Closes an authenticated SSH connection only after this period with no bytes in either direction. Keep disabled for VPN/XHTTP connections."/>
</div>
</div> </div>
<div style="display:flex;gap:16px;margin-top:8px;flex-wrap:wrap;"> <div style="display:flex;gap:16px;margin-top:8px;flex-wrap:wrap;">
<label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;"> <label style="font-size:.73rem;display:flex;align-items:center;gap:5px;cursor:pointer;">
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"testing"
"time"
)
func TestSSHIdleTimeoutDisabledByDefault(t *testing.T) {
for _, raw := range []string{"", "0", "0s", "off", "disabled"} {
if got := parseSSHIdleTimeout(raw); got != 0 {
t.Fatalf("parseSSHIdleTimeout(%q) = %s, want disabled", raw, got)
}
}
}
func TestSSHIdleTimeoutExplicitValue(t *testing.T) {
if got := parseSSHIdleTimeout("30m"); got != 30*time.Minute {
t.Fatalf("got %s, want 30m", got)
}
}
func TestNativeXHTTPConnectedIdleSweepDisabled(t *testing.T) {
if got := nativeXHTTPIdleTimeout(); got != 0 {
t.Fatalf("native XHTTP idle timeout = %s, want disabled", got)
}
}
+13 -10
View File
@@ -41,9 +41,11 @@ const (
tlsHandshakeTimeout = 15 * time.Second tlsHandshakeTimeout = 15 * time.Second
// Dial timeout for direct-tcpip backend connections. // Dial timeout for direct-tcpip backend connections.
directTCPIPDialTimeout = 10 * time.Second directTCPIPDialTimeout = 10 * time.Second
// Default post-auth SSH inactivity timeout. This is based on real bytes // VPN transports must remain connected even when the user is temporarily
// moving in either direction, so live upload/download tunnels are not closed. // idle. Dead peers are released by transport errors/request cancellation,
defaultSSHIdleTimeout = 5 * time.Minute // not by a short application-level inactivity timer. Operators may still
// set ssh_idle_timeout explicitly when they intentionally want one.
defaultSSHIdleTimeout time.Duration = 0
) )
// ---------- Config types ---------- // ---------- Config types ----------
@@ -97,7 +99,8 @@ type Config struct {
// SSHIdleTimeout controls how long an authenticated SSH connection may // SSHIdleTimeout controls how long an authenticated SSH connection may
// remain with no bytes moving in either direction before it is closed and // remain with no bytes moving in either direction before it is closed and
// released from the active user count. Empty = default 5m. Use "0s" to disable. // released from the active user count. Empty, "0", or "0s" disables it.
// VPN/XHTTP connections should normally leave this disabled.
SSHIdleTimeout string `json:"ssh_idle_timeout,omitempty"` SSHIdleTimeout string `json:"ssh_idle_timeout,omitempty"`
// MaxTotalConnections caps the total number of concurrent SSH connections // MaxTotalConnections caps the total number of concurrent SSH connections
@@ -575,17 +578,17 @@ func copyWithRateLimit(dst io.Writer, src io.Reader, lim *rate.Limiter) (written
func parseSSHIdleTimeout(raw string) time.Duration { func parseSSHIdleTimeout(raw string) time.Duration {
raw = strings.TrimSpace(raw) raw = strings.TrimSpace(raw)
if raw == "" { if raw == "" || raw == "0" || raw == "0s" || strings.EqualFold(raw, "off") || strings.EqualFold(raw, "disabled") {
return defaultSSHIdleTimeout return 0
} }
d, err := time.ParseDuration(raw) d, err := time.ParseDuration(raw)
if err != nil { if err != nil {
log.Printf("invalid ssh_idle_timeout %q: %v; using default %s", raw, err, defaultSSHIdleTimeout) log.Printf("invalid ssh_idle_timeout %q: %v; disabling SSH idle timeout", raw, err)
return defaultSSHIdleTimeout return 0
} }
if d < 0 { if d < 0 {
log.Printf("invalid negative ssh_idle_timeout %q; using default %s", raw, defaultSSHIdleTimeout) log.Printf("invalid negative ssh_idle_timeout %q; disabling SSH idle timeout", raw)
return defaultSSHIdleTimeout return 0
} }
return d return d
} }
+6 -7
View File
@@ -24,13 +24,12 @@ const (
defaultNativeXHTTPMaxSessions = 16384 defaultNativeXHTTPMaxSessions = 16384
defaultNativeXHTTPBufferedPosts = 512 defaultNativeXHTTPBufferedPosts = 512
// Connected XHTTP sessions are torn down primarily by request-context // Do not impose an application-level lifetime on a connected XHTTP VPN
// cancellation. This idle timeout is the backstop that reaps a connected // session. The official Xray server keeps a connected session for the
// session whose client vanished without the transport ever reporting it // lifetime of its stream-down GET; request cancellation and I/O errors own
// (common for XHTTP behind a CDN, where no TCP FIN reaches the origin). // cleanup. A fixed five-minute sweeper incorrectly killed healthy but idle
// Matches the SSH idle default so a genuinely idle-but-live tunnel is not // VPNs. Zero disables the connected-session sweeper.
// closed prematurely. fixedNativeXHTTPIdleMS = 0
fixedNativeXHTTPIdleMS = 300000
) )
var ( var (
+5
View File
@@ -712,6 +712,11 @@ func (ib *nativeInbound) handleXHTTPDownload(w http.ResponseWriter, r *http.Requ
go func() { go func() {
select { select {
case <-r.Context().Done(): case <-r.Context().Done():
// The stream-down HTTP request is the lifetime owner of an XHTTP
// session. Log the actual transport cancellation so a CDN/proxy
// timeout can be distinguished from a server idle policy.
xrayLogf("native xray: xhttp stream-down ended inbound=%q session=%q remote=%s err=%v",
ib.tag, sessionID, r.RemoteAddr, r.Context().Err())
_ = xc.Close() _ = xc.Close()
case <-sess.done: case <-sess.done:
} }