Fix idle
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;">
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user