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("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("managedCfgUserCount").checked = !!c.user_count;
document.getElementById("managedCfgBanner").value = c.banner || "";
@@ -444,6 +445,7 @@ function managedConfigFromForm() {
admin_dir: "/opt/sshpanel/admin",
default_limit_mbps_up: parseInt(document.getElementById("managedCfgLimitUp").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,
user_count: document.getElementById("managedCfgUserCount").checked,
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("cfgLimitDown").value = c.default_limit_mbps_down || 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("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_down: parseInt(document.getElementById("cfgLimitDown").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,
user_count: document.getElementById("cfgUserCount").checked,
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>
<!-- 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="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;">
<label>Mode</label>
<select id="wzXHTTPMode">
@@ -670,6 +670,7 @@
<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 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="managedCfgUserCount"/> User Count Display</label>
</div>
@@ -981,6 +982,10 @@
<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."/>
</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 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;">
+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
// Dial timeout for direct-tcpip backend connections.
directTCPIPDialTimeout = 10 * time.Second
// Default post-auth SSH inactivity timeout. This is based on real bytes
// moving in either direction, so live upload/download tunnels are not closed.
defaultSSHIdleTimeout = 5 * time.Minute
// VPN transports must remain connected even when the user is temporarily
// idle. Dead peers are released by transport errors/request cancellation,
// 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 ----------
@@ -97,7 +99,8 @@ type Config struct {
// SSHIdleTimeout controls how long an authenticated SSH connection may
// 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"`
// 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 {
raw = strings.TrimSpace(raw)
if raw == "" {
return defaultSSHIdleTimeout
if raw == "" || raw == "0" || raw == "0s" || strings.EqualFold(raw, "off") || strings.EqualFold(raw, "disabled") {
return 0
}
d, err := time.ParseDuration(raw)
if err != nil {
log.Printf("invalid ssh_idle_timeout %q: %v; using default %s", raw, err, defaultSSHIdleTimeout)
return defaultSSHIdleTimeout
log.Printf("invalid ssh_idle_timeout %q: %v; disabling SSH idle timeout", raw, err)
return 0
}
if d < 0 {
log.Printf("invalid negative ssh_idle_timeout %q; using default %s", raw, defaultSSHIdleTimeout)
return defaultSSHIdleTimeout
log.Printf("invalid negative ssh_idle_timeout %q; disabling SSH idle timeout", raw)
return 0
}
return d
}
+6 -7
View File
@@ -24,13 +24,12 @@ const (
defaultNativeXHTTPMaxSessions = 16384
defaultNativeXHTTPBufferedPosts = 512
// Connected XHTTP sessions are torn down primarily by request-context
// cancellation. This idle timeout is the backstop that reaps a connected
// session whose client vanished without the transport ever reporting it
// (common for XHTTP behind a CDN, where no TCP FIN reaches the origin).
// Matches the SSH idle default so a genuinely idle-but-live tunnel is not
// closed prematurely.
fixedNativeXHTTPIdleMS = 300000
// Do not impose an application-level lifetime on a connected XHTTP VPN
// session. The official Xray server keeps a connected session for the
// lifetime of its stream-down GET; request cancellation and I/O errors own
// cleanup. A fixed five-minute sweeper incorrectly killed healthy but idle
// VPNs. Zero disables the connected-session sweeper.
fixedNativeXHTTPIdleMS = 0
)
var (
+5
View File
@@ -712,6 +712,11 @@ func (ib *nativeInbound) handleXHTTPDownload(w http.ResponseWriter, r *http.Requ
go func() {
select {
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()
case <-sess.done:
}