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("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,
|
||||||
|
|||||||
@@ -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
@@ -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;">
|
||||||
|
|||||||
@@ -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
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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:
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user